Updating Divs when observable collection changes - javascript

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.

Related

How to easily determine what's added/removed in a $scope.$watch?

I am tasked with wrapping a JQuery-style UI component within an Angular directive. I have completed the task but it looks rather ugly. The underlying component's API only has add and remove methods for controlling a list of items. The only thing I can think to do is watch the two-way bound variable passed into my directive with scope.$watchCollection and then iterate over the old and new arguments to determine what has been added or removed, then invoke the add/remove API calls on the underlying component. This works, but I'm curious if there is a better way to accomplish this. The code is hideous and confusing.
Here is a demo minimal recreation of the component in it's natural habitat:
http://codepen.io/Chevex/pen/ofGeB?editors=101
You can see the component is instantiated on an element that has the items in the list within the markup. The component then returns an API to add/remove individual items. Please note that I have no control over the underlying component in our actual application and that the real component does far more than my demo component (which could easily be re-written with an ng-repeat).
Here is a demo minimal recreation of my directive wrapper:
http://codepen.io/Chevex/pen/LDgCF?editors=101
This works great and simulates a two-way bound variable with a jquery-style component that only has add/remove methods for individual items. I want to know if there is a better way to determine new/removed items within $watchCollection.
// Watch items for changes.
scope.$watchCollection('items', function (newItemList, oldItemList) {
// Iterate over the newItemList and determine what items are new.
if (newItemList) {
newItemList.forEach(function (newItem) {
var addItem = true;
if (oldItemList) {
oldItemList.forEach(function (oldItem) {
if (newItem === oldItem) {
addItem = false;
}
});
}
if (addItem) {
listApi.addItem(newItem);
}
});
}
// Iterate over the oldItemList and determine what was removed.
if (oldItemList) {
oldItemList.forEach(function (oldItem, oldItemIndex) {
var removeItem = true;
if (newItemList) {
newItemList.forEach(function (newItem) {
if (oldItem === newItem) {
removeItem = false;
}
});
}
if (removeItem) {
listApi.removeItem(oldItemIndex);
}
});
}
});
All of that just to determine what should be added or removed using the component API whenever the collection changes. Is this the best way? I ask because I've been finding myself writing similar logic repeatedly and wondered if there may be a more proper and/or simpler way to do it.
You could use something like this:
Array.prototype.diff = function(arr) {
return this.filter(function(el) { return arr.indexOf(el) < 0; });
};
var addedItems = newItemList.diff(oldItemList);
var removedItems = oldItemList.diff(newItemList);

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/

Using Marionette to group items in a collection view

I'm building an application using backbone and marionette.js. I'm planning on using a collection view to present some items and then allow them to be filtered, sorted and grouped.
I was wondering if there are any good design ideas for actually appending the html in a grouped fashion. I have a few ideas but I was wondering if someone might have input on which would be better design.
My first idea is to change the appendHtml method on the collection view, and if grouping is enabled, I can have the appendHtml function either find or create the child group's bin and place the child view in it.
appendHtml: function(collectionView, itemView, index){
var $container = this.getItemViewContainer(collectionView);
// get group from model
var groupName = itemView.model.get("group");
// try to find group in child container
var groupContainer = $container.find("." + groupName);
if(groupContainer.length === 0){
// create group container
var groupContainer = $('<div class="' + groupName + '">')
$container.append(groupContainer);
}
// Append the childview to the group
groupContainer.append(itemView);
}
My second idea is to break apart the collection into groups first and then maybe render multiple views... This one seems like it might be more work, but might also be a bit better as far as the code structure is concerned.
Any suggestions or thought eliciting comments would be great!
Thanks
Maybe not exactly what you're looking for, but here's a somewhat related question:
Backbone.Marionette, collection items in a grid (no table)
My solution to that issue -- one fetched collection that could be rendered as a list or a grid ("items grouped in rows") was to use _.groupBy() in a "wrapper" CompositeView and pass modified data down the chain to another CompositeView (row) and then down to an ItemView.
Views.Grid = Backbone.Marionette.CompositeView.extend({
template: "#grid-template",
itemView: Views.GridRow,
itemViewContainer: "section",
initialize: function() {
var grid = this.collection.groupBy(function(list, iterator) {
return Math.floor(iterator / 4); // 4 == number of columns
});
this.collection = new Backbone.Collection(_.toArray(grid));
}
});
Here's a demo:
http://jsfiddle.net/bryanbuchs/c72Vg/
I've done both of the things your suggesting, and they both work well. It largely comes down to which one you prefer and maybe which one fits your scenario better.
If you have data that is already in a grouped hierarchy, using one of the many hierarchical model / collection plugins or your own hierarchy code, then the idea of rendering a list of groups, with each group rendering a list of items is probably easier.
If you have data that is flat, but contain a field that you will group by, then the appendHtml changes will probably be easier.
hth
This is in addition to Derick's and bryanbuchs' answers. My method uses a main collection view and another collection view as its childView.
Collection views have a 'addChild' method, which is called whenever a model is added to the view's collection. The 'addChild' method is responsible for rendering the child's view and adding it to the HTML for the collection view at a given index. You can see the source code on github here. I'll paste it here for simplification:
addChild: function(child, ChildView, index) {
var childViewOptions = this.getOption('childViewOptions');
if (_.isFunction(childViewOptions)) {
childViewOptions = childViewOptions.call(this, child, index);
}
var view = this.buildChildView(child, ChildView, childViewOptions);
// increment indices of views after this one
this._updateIndices(view, true, index);
this._addChildView(view, index);
return view;
}
As you can see the 'addChild' method calls the 'buildChildView' method. This method actually builds the view.
// Build a `childView` for a model in the collection.
buildChildView: function(child, ChildViewClass, childViewOptions) {
var options = _.extend({model: child}, childViewOptions);
return new ChildViewClass(options);
}
So for your use case you can override the 'addChild' method and make a call to the original method if your grouping criteria is matched. And then in the overridden 'buildChildView' method you can pass the group (which is a subset of your collection) to its childView, which is another Marionette.CollectionView.
Example:
MyCollectionView.prototype.addChild = function(child, ChildView, index) {
if(mycriteria){
return ProductGroup.__super__.addChild.apply(this, arguments);
}
};
MyCollectionView.prototype.buildChildView = function(child, ChildViewClass,
childViewOptions) {
options = _.extend({
collection: "Pass your group collection which is a subset of your main collection"},
childViewOptions
);
return new ChildViewClass(options);
};

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

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.

How does a Backbone.js View know when its collection has changed?

I have a Backbone View that has a Collection as its Model. If the collection is passed in through its constructor it can add listeners to the collection in its initialize function, but how can it know when its collection is set after construction so that it can listen to events from the collection?
I want to be able to change its collection during its lifecycle and have it re-render based on the data in the new collection, but there seems to be no way of it knowing when its collection has changed? Are there any hooks available?
[Note: See my answer below with code based on stusmith's answer]
I don't think there is any automatic way to know - collection is just an ordinary property.
You could always provide a function setCollection instead, which unbinds events from the old collection (if any), assigns the collection, and rebinds to the new one.
For clarity, you would also call this function from initialize.
Here is my solution based on stusmith's answer:
initialize: function(){
if(this.collection){
this.addCollectionListeners();
}
},
setCollection:function(collection){
if(collection != this.collection){
if(this.collection){
this.removeCollectionListeners();
}
this.collection = collection;
this.addCollectionListeners();
}
},
removeCollectionListeners:function(){
//Remove listeners
},
addCollectionListeners:function(){
//Add listeners
},

Categories