Knockout JS ObservableArray with many-to-many relationships - javascript

I am creating a guest list app using Knockout.js, and so far things are going swimmingly. However I have a best-practices question. My app has several different types of objects: guests and tags among them. Guests can have multiple tags, and tags can have multiple guests. At different points in the app, I need to display both arrays individually. For example, I have a "Guests" view where one can see all the guests along with their associated tags, and I also have a "Tags" view where one can see all tags and their associated guests. At present, the code for me to add a tag to a guest looks something like this:
var tag = function(opts) {
this.guests = ko.observableArray()
// Other tag code here...
}
var guest = function(opts) {
this.tags = ko.observableArray()
// Other guest code here...
var self = this
this.addTag = function(tag) {
self.tags.push(tag)
tag.guests.push(self)
}
}
I know there must be a better way of doing this kind of many-to-many relationship in Knockout other than updating each observableArray independently. This also leads to a kind of recursive app structure where a guest has a tags property/array which contains a tag, which has a guest property/array which contains a guest, which has a tags property... you get the picture.
Right now, the ViewModel structure is like so:
- Parent Object
- Guests ObservableArray
- Guest Object
- Tag Object as property of Guest
- Tags ObservableArray
- Tag Object
- Guest Object as property of Tag
So I guess my question is twofold: 1) Is there a better way to structure my ViewModel to avoid recursive arrays? and 2) how can I better use Knockout.js to update my ViewModel in a DRY manner, rather than updating both the tags array AND the guests array individually? Thanks!

There are probably other ways to do this, but this method has pretty minimal duplication, without sacrificing proper modeling. A server should have no trouble generating the data in this format.
Here it is in a (crudely styled) fiddle. Note, clicking a tag or guest will cause the selections below it to update (the first is selected by default).
Basically, store the relationships by id, and use computed array's to represent associations. Here is a basic viewmodel:
var ViewModel = function(guests, tags) {
var self = this;
self.guests = ko.observableArray(
ko.utils.arrayMap(guests, function(i){ return new Guest(i); }
));
self.tags= ko.observableArray(
ko.utils.arrayMap(tags, function(i){ return new Tag(i); }
));
self.selectedGuest = ko.observable(self.guests()[0]);
self.selectedTag = ko.observable(self.tags()[0]);
self.guestTags = ko.computed(function() {
return ko.utils.arrayFilter(self.tags(), function(t) {
return self.selectedGuest().tags().indexOf(t.id()) > -1;
});
});
self.tagGuests = ko.computed(function() {
return ko.utils.arrayFilter(self.guests (), function(g) {
return self.selectedTag().guests().indexOf(g.id()) > -1;
});
});
};
UPDATE
So I have made a new fiddle to demonstrate a different kind of mapping, but this code could easily co-exist with the above viewmodel; its only seperate for demonstration. Instead of working off selection, it offers a general lookup, so that any code can consume it. Below is the HTML from Tags (guests is symmetrical), and the guestMap function that was added to the viewmodel.
You will note that the names are inputs now, so you can change the names and watch all the bindings stay up to date. Let me know what you think:
<div>Tags
<ul data-bind="foreach: tags">
<li>
<input data-bind="value: name, valueUpdate: 'afterkeydown'" />
</br><span>Tags</span>
<ul data-bind="foreach: guests">
<li><span data-bind="text: $parents[1].guestMap($data).name()"></span></li>
</ul>
</li>
</ul>
</div>
self.guestMap = function(id) {
return ko.utils.arrayFirst(self.guests(), function(g) {
return id == g.id();
});
};

I had the same kind of problem. Displaying many to many related stuff. I had to do 'grid' style display and have an update mechanism in it.
I ended up replicating the structure I had in the backend DB. Two tables of items with join table in between. Pulled the data out from those in three arrays and kept updating the 'join' array. The test data and fiddle of my testings with it below.
var items = [{name: "T1",id: 1}, {name: "T2",id: 2}, {name: "T3",id: 3}, {name: "T4",id: 4}];
var cats = [{catname: 'C1', catid: 1}, {catname: 'C2',catid: 2}, {catname: 'C3',catid: 3}, {catname: 'C4',catid: 4}];
var catsItems = [{catid:1,itemid:2},{catid:2,itemid:1},{catid:4,itemid:2},{catid:3,itemid:4},{catid:3,itemid:1},{catid:1,itemid:3},{catid:2,itemid:4}];
Join table fiddle
I'm not really sure how efficient this method is with lots of data but did the stuff I needed it to do.

Related

Backbone.js removing the model

I have been trying for hours now, without luck. If you could see on the image, I have mildly complex model. Image is taken from Chrome in debug.
I need to delete a model from collection, also I need to be able to change the URL where the backbone will shoot its ajax for delete. So in essence, this is my model structure:
attributes:
favorites {
bookmarkedArticles: [{id: 123123},{id: ...}],
bookedmarkedSearches: [{}],
dispatchesMailds: []
}
How can I delete model in bookmarkedArticles with id of 123123?
I have tried this:
var model = new metaModel(
{
favourites: {
bookmarkedArticles: {
id: "123123"
}
}
}
);
model.destroy();
also this
aamodel.headerData.collection.remove(model);
No success at all.
The information provided is not giving a lot of details, but I will try to answer considering two scenarios:
Option A:
You are trying to delete a model in the collection that has bookmarkedArticle.id="123123". if that is the case and considering the bookmarkedArticles it is just an Array of objects, I would suggest to filter the Collection using the underscore method filter and then delete the models returned by the filter.
var id = 123123;
var modelsToDelete = aamodel.headerData.collection.filter(function(model){
// find in the bookmarked articles
return _.find(model.get('bookmarkedArticles'), function(ba){
return (ba.id === id);
});
});
_.each(modelsToDelete, function(model){
model.destroy();
});
Option 2: If you want to remove the bookmarked article '123123' associated to your main model using just the 'destroy' method, firstable you have to convert 'bookmarkedArticles' to a Backbone.Collection as it is just an Array of Objects, there are some utilities for Backbone that allows you to do this easily:
https://github.com/blittle/backbone-nested-models
But by default this is not possible, then, If you want to remove the 'bookmarkedArticle' you can create the Backbone.Model and then use the method destroy. Example:
var BookmarkedArticle = Backbone.Model.extend({
url: function(){
return '/bookmarkArticle/' + this.id;
}
});
new BookmarkedArticle({"id": "123123","master": "5",...}).destroy();
Hope this information is useful and provide some guidance to solve your problem.

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.

Is it okay to delete attributes in my Backbone.Model's initialize method, and change them to properties of the model?

I have the following object relations between my three models (I am not using Backbone-relational... this is just describing the underlying structure of my data) :
Person has many Cars
Car has many Appraisals.
I have a single method to retrieve a Person, which brings along all the Cars and the Appraisals of those Cars. It looks like this:
{id: 1,
name: John Doe,
cars: [
{id: 3, make: 'Porsche',
appraisals: [
{id: 27, amount: '45000', date: '01/01/2011'}
]
},
{id: 4, make: 'Buick', appraisals: []}
]
}
When I create a new Person I pass in this entire mess. In my Person's initialize function I do this:
...
initialize: function() {
//Cars => Collection of Car
this.cars = new Cars();
_.each(this.get('cars'), function(car) {
this.cars.add(new Car(car));
});
this.unset('cars');
}
...
And in my Car initialize function I do something similar:
...
initialize: function() {
//Appraisals => Collection of Appraisal
this.appraisals = new Appraisals();
_.each(this.get('appraisals'), function(appraisal) {
this.appraisals.add(new Appraisal(appraisal));
});
this.unset('appraisals');
}
...
I also have to override the toJSON function for Person and Car models.
Is there anything wrong with this? I've seen it suggested elsewhere to make nested collections properties rather than attributes, and my experience confirms that is easier, but I'm wondering if I am breaking some rules or doing something dumb here.
I don't have the answer for 'storing nested collections as properties or as attributes' question, but I think you can simplify your code a bit initializing nested collections like this:
Person:
...
initialize: function() {
this.cars = new Cars(this.get('cars'));
this.unset('cars');
}
...
Car:
...
initialize: function() {
this.appraisals = new Appraisals(this.get('appraisals'));
this.unset('appraisals');
}
...
I answered a similar question here: backbone.js - getting extra data along with the request
In the answer that I provided, it was more about a collection owning a model association — a has one, basically.
I think a Person should have a CarsList containing Car models. A Car should have an AppraisalsList containing Appraisal models. You would probably override the parse and toJSON functions of Person and Car as needed.
I would definitely avoid using attributes for associations. The unset functions in the above examples are a bad smell to me.
If I may give my 2 cents worth of input(s):
If you were to draw an OOD class diagram of the classes and model the associations in any object-oriented language of your choice (other than javascript) how would you do it?
You see backbone.js helps put 'structure' to your javascript that could become an tangled spaghetti code. So if you Person has many Cars and a Car has many Appraisals you have two options: Compositions vs. Associations
Composition: What you are doing above: A person object is responsible for creating the cars and car objects for creating appraisals. The 'lifetime' of each object is dependent on the parent. Now that may/may not be how it 'should' be modeled, but that's the choice you've made.
Now, let's see simple associations. You create the person, cars, and appraisals independently (probably appraisal cannot exist without the car, but let's assume otherwise for now).
Now these objects are created but you need to "wire up" these associations - you can do that externally in a separate "initializer" class/container so to speak and just use setter/getters to connect them.
Conclusion: Use what best models your domain and don't let it be governed by your data store (i.e., the JSON object in this case). Backbone's sheer beauty comes from this ability of imparting classic OO structure to your code and thinking in that way when coding. So choose a good mix of OO relations (compositions, aggregations or simple associations) and select the 'best model' for your problem and implement accordingly.
Combining with #kulesa's suggestion, you'll "clean up" your code and achieve exactly what you want without worrying about breaking any principles/practices while organizing your code effectively.
Hope this helps!
I don’t personally think it makes sense to use properties to store some of a model’s data. What experiences did you have that made properties feel easier?
Backbone, internally, appears to use properties only for metadata (e.g. the by-id and by-cid maps of the models in a collection) and quick access to attributes (e.g. the id property, which is updated whenever the id attribute changes). Using properties also stops you from using Backbone’s event system or .get()/.set(), and forces you to override .toJSON().
Sticking with attributes, I believe that you could get the same result by overriding .set() — It gets called when a new instance of a model is created, before .initialize(), and it will also be called if something else tries to set any attribtues. I once did something similar like this:
var Person = Backbone.Model.extend({
set: function(attributes, options){
var outAttributes = {};
_.each(attributes, function(value, name){
switch(name){
case 'cars':
outAttributes.cars = new Cars(value);
break;
default:
outAttributes[name] = value;
}
}, this);
Backbone.Model.prototype.set.call(this, outAttributes, options);
}
});
…you could modify it to, say, update an existing Cars instance instead of creating a new one.
.set() is also where Backbone updates the value of the id property, so if you choose to use properties instead of attributes, it might still be the best to suck in the values (instead of .initialize()).
I had a similar situation and I solved it this way:
parse: function(data) {
if (data.Success) {
var policies = Rens.get('Policies').model, // model with nested collextion
claims = Rens.get('Claims').model; // model with nested collextion
// reseting collections
claims.claims.reset();
policies.policies.reset();
$(data.Result.Policies).each(function(i, policy) {
var claimsList = policy.ClaimsList,
policyWithClaims = _.clone(policy);
claims.claims.add(claimsList);
_.extend(policyWithClaims, {
ClaimsList: claims.getPolicyClaims.bind({
context: claims,
policyNumber: policy.PolicyNumber
}),
CarYearString: Rens.formatDate(policy.CarYear).date.HH,
PolicyEndDateString: Rens.formatDate(policy.PolicyEndDate).fullDate
});
policies.policies.add(policyWithClaims);
});
}
}
After this i have collection with policies and each policy has attribute with method linked to claims collection.
claims.getPolicyClaims returns all claims for current policy

Categories