I'm experiencing with knockout.js ponents and require.js. This is working well so far, but I'm struggling with the following.
Let's say I have one instance of my ponent in a very simple html page :
<div id="exams">
<databound-exam-control></databound-exam-control>
</div>
From the containing viewmodel :
require(['knockout', 'viewModel', 'domReady!'], function (ko, viewModel) {
koponents.register('databound-exam-control', {
viewModel: { require: 'databound-exam-control-viewmodel' },
template: { require: 'text!databound-exam-control-view.html' }
});
ko.applyBindings(new viewModel());
});
I would like to get the child viewmodel content to later save all the data of the page when I click a button.
For now I just trying to display the display the parent/child viewmodels in a pre tag :
<div>
<pre data-bind="text: childViewModel()"></pre>
</div>
With the help of the containing viewmodel :
function childViewModel() {
var model = ko.dataFor($('databound-exam-control').get(0).firstChild);
return ko.toJSON(model, null, 2);
};
I get the following error during the call to ko.dataFor, probably because the the page is not pletely rendered :
Cannot write a value to a koputed unless you specify a 'write' option. If you wish to read the current value, don't pass any parameters.
Is that it ? Or am I pletelty missing the point here ?
Any help appreciated.
I'm experiencing with knockout.js ponents and require.js. This is working well so far, but I'm struggling with the following.
Let's say I have one instance of my ponent in a very simple html page :
<div id="exams">
<databound-exam-control></databound-exam-control>
</div>
From the containing viewmodel :
require(['knockout', 'viewModel', 'domReady!'], function (ko, viewModel) {
ko.ponents.register('databound-exam-control', {
viewModel: { require: 'databound-exam-control-viewmodel' },
template: { require: 'text!databound-exam-control-view.html' }
});
ko.applyBindings(new viewModel());
});
I would like to get the child viewmodel content to later save all the data of the page when I click a button.
For now I just trying to display the display the parent/child viewmodels in a pre tag :
<div>
<pre data-bind="text: childViewModel()"></pre>
</div>
With the help of the containing viewmodel :
function childViewModel() {
var model = ko.dataFor($('databound-exam-control').get(0).firstChild);
return ko.toJSON(model, null, 2);
};
I get the following error during the call to ko.dataFor, probably because the the page is not pletely rendered :
Cannot write a value to a ko.puted unless you specify a 'write' option. If you wish to read the current value, don't pass any parameters.
Is that it ? Or am I pletelty missing the point here ?
Any help appreciated.
Share Improve this question edited Mar 10, 2016 at 15:43 asked Mar 10, 2016 at 15:21 anonanon 1-
I would say that it might be better to set up your main viewmodel to track those child models as you create them. As a contrived example, you can check out: jsfiddle/timh06/91n3m25t Also instead of
ko.dataFor
you can check out theko.contextFor
, which should give access to the binding context of the element. – Timh Commented Mar 10, 2016 at 18:32
3 Answers
Reset to default 9The easier way to municate between the parent viewmodel and the child ponent is by using parameters.
- create a new observable property in the parent view model, like
childViewModel = ko.observable()
- pass it as parameter to the child ponent
<databound-exam-control params= "{modelForParent: childViewModel}">
Note that the parameter inside the child viewmodel will be known asmodelForParent
, and in the parent view model will be known aschildViewModel
- in the viewmodel constructor for the ponent, in your
databound-exam-control-viewmodel.js
script, you receive the parameters as the only argument for your constructor. So, if your constructor looks like this:function SomeComponentViewModel(params)
you can access the parameter asparams.modelForParent
- use the parameter to pass whichever information you need from the child ponente to the parent ponent, for example:
params.modelForParent(createdChildViewModel)
.
Now, the parent ponent can acces the child view model using the childViewModel
observable.
Using observables is just one possibility. You can do it in other ways. For example pass a callback as parameter, and execute that callback in the viewmodel constructor to return whatever you want to the parent. I sometimes use this pattern:
- create a register api callback in the parent, like this:
registerApi = function(api) { config.childApi = api }
- pass this as parameter to the child ponent
- in the child view model constructor invoke the callback passing the child api
In this way, I can acces the api exposed by the child ponent like this: config.childApi.aMethosExposedByTheChild()
Important note: you must take into account that, as the child ponent loads asynchronously, the information exposed by the child is not immediately available.
Unless you need to use it immediately this isn't a problem. For example, in your case, it looks like you'll get information from the child viewmodel after the ponent has been loaded and the user has interacted with it, so that's not a problem.
If you need to access it as soon as possible you can use polling --- or better, expose a deferred (an example implementation: $.Deferred) to the child so that it can resolve it to let the parent veiw model know it's already available. This also happens when the child viewmodel depends on loading external resources by AJAX calls (for example to load a drop down list, or some other information existing on the server).
Another option, which I don't like, is that the parent viewmodel includes the whole child view model and passes it as parameter, so the parent viewmodel has full control on the child view model. Obviously this solution doesn't allow the ponent to be responsible for its own viewmodel, so there is a tight coupling between parent viewmodel and child ponent, which is not desirable.
I had a problem where my parent ponent needed to have access to a child's view model for sorting. My parent layout code looks something like this
<div class="parent-ponent">
<!-- ... -->
<div data-bind="foreach: { data: itemsSorted() as: 'item' }">
<child-ponent params="item: item"></child-ponent>
</div>
</div>
The problem is itemsSorted()
relies on observables in the child ponent's view model, e.g.
var ParentViewModel = function(items) {
var self = this;
self.items = ko.observable(items);
self.itemsSorted = ko.pureComputed(function() {
return self.items().sort(function(a, b) {
// ERROR here: The timestamp() observable isn't present yet,
// since ChildViewModel hasn't created the observable
return a.timestamp() - b.timestamp();
});
});
};
var ChildViewModel = function(item) {
self = this;
self.timestamp = ko.observable(item.timestamp);
};
Because itemsSorted()
doesn't contain view models until the layout is already plete, it's unable to use child observables.
Solution
My solution was to create the child's view model within the parent, and then pass that down through a parameter:
var ParentViewModel = function(items) {
var self = this;
self.items = ko.observableArray(items.map(function(item) {
// Initialize view model in parent to allow for sorting
return new ChildViewModel(item);
}));
self.itemsSorted = ko.pureComputed(function() {
return self.items().sort(function(a, b) {
// This works, because ChildViewModel's observables are already instantiated
return a.timestamp() - b.timestamp();
});
});
};
var ChildViewModel = function(item) {
self = this;
self.timestamp = ko.observable(item.timestamp);
};
Next, in the HTML, pass the view model to the ponent
<div class="parent-ponent">
<!-- ... -->
<div data-bind="foreach: { data: itemsSorted(), as: 'viewModel' }">
<child-ponent params="viewModel: viewModel"></child-ponent>
</div>
</div>
Finally, in the ponent registration, modify the child-ponent
's view model to use the parent's instantiated version:
ko.ponents.register('child-ponent', {
template: /* ... */,
viewModel: function(params) {
// viewModel is already created by parent, pass into ponent
return params.viewModel;
},
});
Now ParentViewModel
can create and use ChildViewModel
's attributes for sorting without needing to wait for an initial layout. The code in ChildViewModel
isn't aware of the difference and doesn't need to be changed. This does couple the child ponent to the parent which isn't good encapsulation of the ponent, but it allows for immediate layout and use of child observables by the parent.
This is the way I control my viewmodel ponent instance from a higher level.
//ViewModel for Component...
function ComponentViewModel(params) {
const self = this;
self.Message = ko.observable(params.Message);
};
//Register Component...
ko.ponents.register("my-ponent", {
template: { element: "my-ponent-template" },
viewModel: function (params) {
return params.ViewModel; //Return the instance created on root (or wherever you instace your ponent)...
}
});
//Principal ViewModel...
function RootViewModel() {
const self = this;
self.ComponentViewModel = ko.observable(new ComponentViewModel({
Message: "Hello from ponent!" //Params for ponent go here...
}));
};
ko.applyBindings(new RootViewModel() );
<script src="https://cdnjs.cloudflare./ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<my-ponent params="ViewModel: ComponentViewModel"> </my-ponent>
<template id="my-ponent-template">
<p data-bind="text: Message"> </p>
</template>