KnockoutJS : How do I remove an item from a child array? - javascript

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.

Related

Multiple KnockoutJs components on the same page running individually

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.

How to force knockoutjs to update UI (reevaluate bindings)

(I know there are other questions here asking the same thing; I've tried them and they don't apply here)
I have a collection being displayed by a Knockout JS foreach. For each item, the visible binding is set by call a method, based on something external to the item itself. When the externality changes, I need the UI to be redrawn.
A striped down version can be seen in this Fiddle: http://jsfiddle.net/JamesCurran/2us8m/2/
It starts with a list of four folder names, and displays the ones starting with 'S'.
<ul data-bind="foreach: folders">
<li data-bind="text: $data,
visible:$root.ShowFolder($data)"></li>
</ul>
<button data-bind="click:ToA">A Folders</button>
Clicking the button should display the ones starting with 'A' instead.
self.folders = ko.observableArray(['Active', 'Archive', 'Sent', 'Spam']);
self.letter = 'S';
// Behaviours
self.ShowFolder = function (folder)
{
return folder[0] === self.letter;
}
self.ToA = function ()
{
self.letter = 'A';
}
UPDATE:
After Loic showed me how easily this example could be fixed, I reviewed the differences between this example and my actual code. I'm using an empty object as a dictionary to toggle if an item is selected self.Selected()[item.Id] = !self.Selected()[item.Id];
The object being changed is already an observable. I assumed that Knockout didn't realize that the list is dependent on the external observable, but it does. What Knockout was missing was that the observable was in fact changing. So, the solution was simply:
self.Selected()[item.Id] = !self.Selected()[item.Id];
self.Selected.notifySubscribers();
Here's what I came up with:
What you have to understand is that Knockout is only "answering" to data changes in observables. If an observable changes, it will trigger every object that uses it. By making your self.letter an observable. You can simply change it's value and uses it somewhere like self.letter() and it will automagically redraw when needed.
http://jsfiddle.net/2us8m/3/
function WebmailViewModel() {
// Data
var self = this;
self.folders = ko.observableArray(['Active', 'Archive', 'Sent', 'Spam']);
self.letter = ko.observable('S');
// Behaviours
self.ShowFolder = function (folder)
{
return folder[0] === self.letter();
}
self.ToA = function ()
{
self.letter('A');
}
};
ko.applyBindings(new WebmailViewModel());
In case you have complex bindings, like storing an object inside an observable. If you want to modify that object you have multiple possible choices.
self.Selected()[item.Id] = !self.Selected()[item.Id];
You could change it to this by making everything "observables" but if my memory is right, it can become complicated.
self.Selected()[item.Id](!self.Selected()[item.Id]());
I remember I had one similar issue where I had dependency problem where I had to update a country, region, city. I ended up storing it as list inside an observable to prevent update on individual element change. I had something like this.
var path = PathToCity();
path[0] = 'all';
path[1] = 'all';
PathtoCity(path);
By doing this, the change would be atomic and there will be only one update. I haven't played a lot with knockout for a while. I'm not sure but I do believe that the last time I worked with knockout, it was able to "optimize" and prevent to redraw the whole thing. But be careful because if it is not able to guess that you didn't change many thing, it could redraw the whole observable tree (which could end up pretty bad in term of performance)
In your example, we could use the same behaviour with my modified example:
http://jsfiddle.net/2us8m/4/

KO ObservableArray update breaks when binding with .sort

I'm sorting my ObservableArray in the markup thusly, but for some reason, when I push a new object to the pages ObservableArray, this does not update.
//store.get('pages') returns an ObservableArray
<!-- ko foreach:store.get('pages').sort(function (l , r ) { return l.pageNumber() < r.pageNumber() ? -1 : 1}) -->
<markup/>
<!--/ko-->
However, when I remove the sort call, it catches the array change just fine.
Like so,
//works fine, updates when item pushed to observableArray
<!-- ko foreach:store.get('pages') -->
<markup/>
<!--/ko-->
Any idea for a simple workaround?
Edits:
Tried using valueHasMutated(), doesn't force an update either.
Workaround:
I ended up moving the sort call into a subscription on the observableArray, and it seems to be working fine now, not sure why though.
store.get returns an observableArray but store.get(...).sort(...) returns an array. You're effectively making a one way binding like this:
<div data-bind="foreach: ['foo', 'bar']"></div>
Also, although you bind a returned value of a function call, to me it has a code smell that you're coupling your business logic with your view logic. You have something like this:
// View
<div data-bind="foreach: $root.get()"></div>
// Javascript
function ViewModel () {
var self = this;
self.get = function () {
return ko.observableArray();
};
}
And it works, but from your view, it's unclear what you're doing. I think a better solution would be:
// View
<div data-bind="foreach: stores"></div>
// Javascript
function ViewModel () {
var self = this;
self.stores = ko.observableArray();
self.get = function () {
var arr = ["foo", "bar"];
stores(arr.sort(...));// When you do this, KO updates the foreach binding this is bound to
return stores;// not that you need to, you can access it from the viewModel.
};
}
Glad you found a workaround but take a minute and plan out your HTML binding structure. It's the first place I start when I'm creating a new view and it drives how I structure my ViewModel.

knockout How to change the model binded to elements

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>

Updating Divs when observable collection changes

Is there any plugin or jquery utility that will update my list of divs bound to an observable collection when items get added/removed to that collection? We have 1000s of items and so are looking for the most optimal way to add/remove divs when the bound list changes.
We would be interested in hearing about this in KO or jquery.tmpl.
This is maybe not the answer that your looking for, but it could be one way to do it.
I would wrap the array within an object that has an Add and Remove method(or other function that change the array)
var Collection = (function () {
var collectionArray = new Array();
//"private" methods
function updatePageDivs()
{
//Logic to update your Divs
}
//"public" methods
return{
Add: function(element){
collectionArray[collectionArray.length] = element;
updatePageDivs();
},
Remove: function(element){
//other logic to remove elements and to trigger the updatePage
}
}
});
You can now call
Collection.Add()
and
Collection.Remove()
to change your javascript collection. Both will update your pagedivs.

Categories