In a video tutorial on the Knockout.js homepage, the presenter sets up a simple example in which the ViewModel contains an observableArray property containing instances of a custom object (a "friend" object, in this case):
function friend(name) {
return {
name: ko.observable(name),
remove: function () {
viewModel.friends.remove(this);
}
};
}
var viewModel = {
friends: ko.observableArray()
}
ko.applyBindings(viewModel);
This is then rendered using a KO template:
<script id="friendsTemplate" type="text/html">
<li>
<input data-bind="value: name" />
<button data-bind="click: remove">Remove</button>
</li>
</script>
It seems odd to me that the logic for removing a friend object (the Model?) from the ViewModel's friends collection is implemented in the friend object itself. Doesn't this create an undesirable coupling between the friend object and the ViewModel instance? Is there a more consistent design approach that would allow the removeFriend functionality to be defined in the ViewModel rather than in the friend object?
A more common pattern would be to put a removeFriend function one level higher and the bind to it using:
<button data-bind="click: $parent.removeFriend">Remove</button>
When Knockout calls the handler from the click or event binding it will pass the current data item as the first argument. It will also set the context (this) equal to the current data as well. Depending on your structure, you may have to bind the function to ensure that it is called with the appropriate context or use a strategy like var self = this;.
Since, viewModel is an object literal, in this case removeFriend could look like:
friends: ko.observableArray(),
removeFriend: function(friend) {
viewModel.friends.remove(friend);
}
Related
I am new to Knockout and struggling with having multiple instances of a component on a single page.
Background. Component gets an array of objects from a data service dynamically. Depending on user action, the component might need to re-render the array (new objects provided), therefore the array is an observable one.
Problem. There have to be multiple instances of the component on the page, however they have to work independently. Even though initially they receive the same array of objects.
I've created a simple example that would illustrate the problem.
HTML.
Emulate data service call:
<button data-bind="click: getData">PULL</button><br>
Component 1:
<item-picker params="items: itemsArray"></item-picker>
Component 2:
<item-picker params="items: itemsArray"></item-picker>
JavaScript.
ko.components.register("item-picker", {
viewModel: function(params) {
var self = this;
self.items = params.items;
self.addPerson = function() {
self.items.push({
name: "New at " + new Date()
});
};
},
template: "<button data-bind='click: addPerson'>Add</button><div data-bind='foreach: items'><p data-bind='text: name'></p></div>"
});
// top level viewmodel
var vm = function() {
var self = this;
self.itemsArray = ko.observableArray([]);
self.getData = function(){
self.itemsArray([]);
self.itemsArray.push({name: 'Bert'}, {name: 'Charles'}, {name: 'Denise'});
};
};
ko.applyBindings(new vm());
The example is also available on JSFiddle: https://jsfiddle.net/ignas/14jgksj0/6/
EDIT
I should have mentioned that data service call is async, and data may be received later than the page load, and also user is able to force data re-retrieval (using different parameters for example). I've updated code snippets and the example on JSFiddle accordingly.
Question. In the JSFiddle example, how can I make components work independently?
I.e. Adding a new name to "Component 1" should not add a name to "Component 2"
Create a new observable array wrapper and inner array in your constructor:
self.items = ko.observableArray(params.items().slice(0));
https://jsfiddle.net/dx6b6nvy/
There's two layers in your current structure:
An observableArray instance inside vm.itemsArray
An underlying, regular, array
Currently, all your components are modifying (2) via (1). To make sure each component holds its own, independent, selection, you'll have to create new arrays.
To create a new observableArray, you call ko.observableArray again. To create a copy of the original list, (note that the object references are still the same!), I've used slice.
edit:
The approach above decouples the component from the original array. If you want to keep the relation but not modify the source, here's what you can do:
In your component:
self.myItems = ko.observableArray([]);
self.items = ko.pureComputed(function() {
return params.items().concat(self.myItems());
});
You push to myItems, which will trigger an update of the computed. Any changes to the source observable array also trigger updates. You can even add a sort, if you like.
So with the introduction of components and custom elements it seems a lot easier to encapsulate your logic and markup, however I am a bit unsure how to make use of components within foreach sections when you need to pass in a viewmodel.
So my current scenario is that I have a view model which looks something like:
function SomePartialViewModel()
{
this.Name = ko.observable();
this.Score = ko.observable();
this.SomeDate = ko.observable();
}
function SomeViewModel()
{
this.DataFromWebServiceCall = ko.observableArray();
this.GetDataFromServer = function(){
// get some data from service and populate DataFromWebSeriviceCall with instances of SomePartialViewModel
}
}
Now in the above we have a POJO to contain partial data, and we have a main view model for the view which will contact a web service or something, and populate its array with instances of the partial. Then this would currently be used like so:
<div id="partial-data" data-bind="template: { name: 'partial-view', foreach: DataFromWebServiceCall }"></div>
<div class="partial-view">
<label data-bind="text: Name"></label>
<label data-bind="text: Score"></label>
<label data-bind="text: SomeDate"></label>
</div>
Now assume the .partial-view is in a script tag with correct template name etc and is a correct template, then the #partial-data is in the main view and wants to display all the instances on the page. Now currently it all works, but I would like to move to a more component based model, and currently we can see that the template relies upon the SomePartialViewModel data, so we have our template and our viewmodel for that component, however the problem is around getting the viewmodel into the component, as currently you register the component at setup time, then you use params to populate chunks of it. However in this case I want to pass in the viewmodel to the component at binding time...
So I was wondering how I can go about doing this, as I imagine I could register the component with a template but no viewmodel, but is there the notion of a data style binding where I can set the $data property and move to a foreach from a template binding on the view?
Hopefully the problem I am trying to solve can be seen and any info would be great.
There are loads of ways to pass values and/or viewmodels to components using the params.
If you use the createViewModel method, you can just pass in the viewmodel via the params and use the partial viewmodel as the component viewmodel:
ko.components.register("partial-view", {
viewModel: {
createViewModel: function (params) {
return params.value;
}
},
template: "<div>Partial View for <b data-bind='text: Name'></b></div>"
});
You can see a working example in this fiddle: http://jsfiddle.net/Quango/fn1ymf9w/
You can define viewModels under viewModels :)
just like defining an observable you can define another viewModel and using "with" binding you can create a component based model you desire.
First you create your components and sub-Components and sub-sub-sub-Components etc viewModels seperately.
var SomePartialViewModel = function()
{
this.Name = ko.observable();
this.Score = ko.observable();
this.SomeDate = ko.observable();
}
var SomeViewModel = function()
{
this.DataFromWebServiceCall = ko.observableArray();
this.GetDataFromServer = function(){
// get some data from service and populate DataFromWebSeriviceCall with instances of SomePartialViewModel
}
this.SPM = new SomePartialViewModel(); // partial-1
}
And then you create a MainViewModel and bind all the main elements here.
var MainViewModel = function() {
var self = this;
self.SVM = new SomeViewModel();
self.SPM = new SomePartialViewModel(); // partial-2
}
ko.applyBindings(new MainViewModel());
then in your html you can create your components obeying the context you created on knockout entities
...
<body>
<div data-bind="with: SVM">
....
<div data-bind="with: SPM">
<!-- partial-1 data -->
</div>
...
</div>
<div data-bind="with: SPM">
<!-- partial-2 data -->
</div>
</body>
...
You may want to create seperate files for your component models and using a modular script loader like Require js you can bind all together to a complete component based knockout web application
I'm really just starting into knockout and so far it is pretty impressive. With that, there is also an issue I am running into.
I'm trying to take a list of binded objects and when on is clicked, I want it take that model and bind it to another view for further editing, no issues there. The problem I run into is that I get an error saying I cannot bind multiple times to the same element. I have read the ko.cleanNode([domElement]) can be used to circumvent it, but that is really meant to be used internally. I have done some searching and people mention you should use with but I don't think that applies to what I want to do.
Is there a way to apply the bindings but then just update the underlying model context?
Simplified jsfiddle example. My js code starts on like 119, above it the ko mapping plugin.
First, use the external resources to add external resources to your fiddle rather than pasting them into your fiddle.
Second, on your problem, the easiest way to do something like this is to have a SelectedItem property in your view model and bind the child view to it. Something like this:
var MyViewModel = function() {
var self = this;
self.MyItems = ko.observableArray();
self.SelectedItem = ko.observable();
}
var MyChildViewModel = function() {
var self = this;
self.SomeProperty = ko.observable();
}
Now populate MyItems with instances of MyChildViewModel and set SelectedItem to whichever item in MyItems you want to have as your child and bind the child view to SelectedItem:
<div id="theChildView" data-bind="with: SelectedItem">
<span data-bind="text: SomeProperty"></span>
</div>
Issue:
I'm still learning knockoutJS, please guide me if my approach is wrong.
Here is my fiddle: http://jsfiddle.net/amitava82/wMH8J/25/
While onclick of edit, I receive the json model which is represented in the view and I want to remove certain items (child array) or actions (parent array) from the model (I removed add UI to add more Actions from the fiddle for simplicity) and then finally pass the model back to server.
Now, deleting from root level is easy. I'm stuck with deleting individual item which is ActionParamaters in ActionItems array.
Question:
How do I remove an item from a child array?
You can pass the clicked actionItem and the containing action array to deleteActionItem function as follows:
<!-- /ko -->
remove item
In your model you need to make every actionItem array observable using ko.mapping plugin (see edit function)
var viewModel = function() {
var self = this;
self.data = ko.observable();
self.edit = function() {
self.data ( ko.mapping.fromJS(editData) );
}
self.log = function() {
console.log(self.data())
}
self.deleteAction = function(data) {
//delete root node
self.data().remove(data)
}
self.deleteActionItem = function(data,actionItem) {
//delete items
data.ActionItems.remove(actionItem);
}
}
Then you will be able to remove the item from array in the deleteActionItem function and since the array is observable now, the result will reflect to binded dom element.
Sam, your fiddle data was too complicated. When asking questions, you will improve your chance of getting help if you distill your fiddle down to the relevant elements. I have cooked up a simple fiddle that illustrates nested arrays, and removal.
Here is the HTML, note that the remove function is inside the context of the array, so it calls a function on $parent instead of $root. This lets us target the context directly above, instead of the root.
<ul data-bind="foreach: editData">
<li>
<span data-bind="text: name"></span>
<button data-bind="click: $parent.removeParent">Remove Parent</button>
...
<!-- This line is on the child context -->
<button data-bind="click: $parent.removeChild">Remove Child</button>
</ul>
Here is the parent model. Note the removal function here is for removing children. When the removeChild function is called, it is from the child context asking for $parent, which will call this remove.
var Parent = function(name, children) {
var self = this;
self.name = ko.observable(name);
self.children = ko.observableArray(children);
self.removeChild = function(child) {
self.children.remove(child);
};
};
Your fiddle also makes no use of models, which are an important aspect of MVVM development. You should consider going through the tutorials on the knockout site to get a better understanding of how to structure knockout applications. It will help you deal with problems like this much easier.
I am just referring the tutorials from knockout.js:
http://learn.knockoutjs.com/#/?tutorial=webmail
In the UI the markup is:
<!-- Folders -->
<ul class="folders" data-bind="foreach: folders">
<li data-bind="text: $data,
css: { selected: $data == $root.chosenFolderId() },
click: $root.goToFolder"></li>
</ul>
and it's ViewModel is:
function WebmailViewModel() {
// Data
var self = this;
self.folders = ['Inbox', 'Archive', 'Sent', 'Spam'];
self.chosenFolderId = ko.observable();
// Behaviours
self.goToFolder = function(folder) { self.chosenFolderId(folder); };
};
ko.applyBindings(new WebmailViewModel());
Can anybody tell me what is is $root and why is it required? If I remove it, it doesn't work.
$root refers to the top model in KnockoutJS hierarchy (the one you use in .applyBindings). In your case WebmailViewModel object is the $root.
It is required, because when you use foreach then inside the loop the context changes. Everything you want to fire here is associated to an element within a loop. Thus you need $root to use functions/fields defined outside of that context (in your case chosenFolderId is a method of WebmailViewModel object).
You'll need to check out the binding contexts page.
$root
This is the main view model object in the root context, i.e., the
topmost parent context. It is equivalent to $parents[$parents.length
- 1].
The $root context always refers to the top-level ViewModel, regardless of loops or other changes in scope. This allow us to access top-level methods for manipulating the ViewModel.
In your example $data represent to folder array values like 'Inbox', 'Archive','Sent' and 'spam'. But $root represent root functions of ViewModel like chosenFolderId and goToFolder .
See http://www.dotnet-tricks.com/Tutorial/knockout/bSKG240313-Understanding-Knockout-Binding-Context-Variable.html
I think this link examples will help you more.