Can't grok the process of updating UI on model change - javascript

I have a collection of models. When a model changes it triggers a change event on the collection. I watch for the collection change event and then I update the UI.
How should I go about updating the UI? Don't I need to know what models are new, so I can append, and what already exist, so I can update?
One of the reason I feel I need this granularity is because there's an animation transition, so I need to relate every model to it's previous state. Does backbone help with this or should I just build this on my own?

to know which models are new, listen to the collection's "add" event. then you can render the individual item.
MyView = Backbone.View.extend({
initialize: function(){
_.bindAll(this, "renderItem");
this.collection.bind("add", this.renderItem);
},
renderItem: function(item){
// render the new item here
},
render: function(){
this.collection.each(this.renderItem);
}
});
in this example, rendering the collection works the same as rendering an individual item - it calls the same renderItem method that the "add" event calls.
to handle the scenario where you have a sorted list... you'll have to do some logic in your renderItem method to figure out the location of the item. something like this maybe (untested code... just an idea):
MyView = Backbone.View.extend({
initialize: function(){
_.bindAll(this, "renderItem");
this.collection.bind("add", this.renderItem);
},
renderItem: function(item){
var itemView = new ItemView({model: item});
itemView.render();
return itemView;
},
render: function(){
this.collection.each(function(item){
var itemView = renderItem(item);
var itemIndex = item.get("index");
var previousItem = this.$(".myList")[itemIndex];
$(itemView.el).insertAfter($(previousItem));
});
}
});
this code assumes you have an index attribute on your models to know the order that the models are supposed to be in.
also note that there's a high likelihood that this code won't execute as-is, since i haven't tested it out. but it should give you an idea of the code you'll want to write.

You don't need to go through the collection if the model changes. Simple render a sub view for each model and in which you can handle the change event.
Imagine you have the following structure:
Collection: Tasks
Model: Task
And two views:
Tasks view (main view)
Task view (sub view)
In the #render method of the Tasks view you render e.g. a <ul> element which will hold your tasks (sub views). Then you iterate through all your collection's models and create a Task view for each, passing the model and appending it's el to your main views <ul>.
Within your Task view you bind the #render method to the models change event and each Task view will take care of itself. — Am I making sense?

Related

Communicating Between Backbone Marionette views

I'm a junior javascript developer, got an internship in a company that uses Backbone and Marionette.
My first task is to create searching, filtering and sorting functionality in a collection based in some inputs, the thing is i got 2 differents views: one itemView renders the input fields(search field, sorting selection,etc ) and a collectionView renders the collection.
I've bee analizing backbone event aggregator, listenTo method, etc to find a way to make the collectionView listen to submit, click events in the itemView so it can render itself accordingly. For example when the user enters "frog" in the search field, the collectionView displays models containing that criteria, if the user clicks the last modified sorting option, the collectionView renders itself that way.
Any suggestion is really welcome.
Thanks in advance.
Basically Marionette does everything for you, you just need to initialize collection view properly.
You can specify which child view events your collection view should listen to (anyway it listens to some default events by default)
Here is example of search functionality and event handling of child views:
HTML
<script id='itemViewTemplate' type = "text/template">
<div class='itemView'><%= title %></div>
</script>
<script id='collectionViewTemplate' type = "text/template">
<div class="collectionView"></div>
</script>
<input value='' id='search' placeholder='search'>
Javascript
// our data to show and filter
var data = [
{title: "title 1"},
{title: "title 2"},
{title: "title 3"}
];
// item model
var FooBar = Backbone.Model.extend({
defaults: {
title: ""
}
});
// collection of items
var FooBarCollection = Backbone.Collection.extend({
model: FooBar
});
// item view
var FooView = Marionette.ItemView.extend({
template: "#itemViewTemplate",
events: {
"click": "_onClick"
},
_onClick: function() {
this.trigger('click', this); // here we trigger event which will be listened to in collection view
}
});
// collection view
var MyCollectionView = Marionette.CollectionView.extend({
childView: FooView,
template: "#collectionViewTemplate",
childEvents: {
'click': '_onItemClick' // listen to any child events (in this case click, but you can specify any other)
},
_onItemClick: function(item) {
$('.message').text("item clicked: " + item.model.get("title"));
console.log(arguments); // event handler
}
});
// init our collection
var myCollection = new FooBarCollection(data);
// another collection which will be filtered
var filterCollection = new FooBarCollection(data);
// init collection view
var myCollectionView = new MyCollectionView({
collection: myCollection
});
// render collection view
$("body").append(myCollectionView.render().$el);
// search
$('#search').change(function() {
var value = $(this).val(); // get search string
var models = filterCollection.where({
title: value
}); // find models by search string
value ? myCollection.reset(models) : myCollection.reset(filterCollection.models);
// reset collection with models that fits search string.
// since marionette collection view listens to all changes of collection
// it will re-render itself
// filter collection holds all of our models, and myCollection holds subset of models, you can think of more efficient way of filtering
});
// just to show click event info
$('body').append("<div class='messageContainer'>Click on item to see its title:<div class='message'></div></div>");
Marionette collection view listens to all myCollection events, for exaple
If you'll write
myCollection.add({title: 'title 4'});
It will automatically render new itemView in collection view.
Same for remove, reset and other defaut Backbone.Collection methods (which trigger events, and marionette listens to them);
Here is jsfiddle: http://jsfiddle.net/hg48uk7s/11/
And here is docs on marionette:
http://marionettejs.com/docs/v2.4.3/marionette.collectionview.html#collectionviews-childevents
I suggest to start reading docs on marionnet from beginnig, because CollectionView inherits a lot from ItemView and ItemView inherits a lot from View and so on, so you could know all the features Collection.View has.
UPDATE
Maybe I misunderstood a question a bit, you need a communication between collectionView and some other view (itemView in this case another view, not the view collectionView uses to render its children, that's what I thought). In this case, here is an updated fiddle:
http://jsfiddle.net/hg48uk7s/17/
You need third entity to handle communication between collectionView and searchView for example. Usually it some controller, which listens to searchView events, then calls some handler, giving control to collectionView, which uses search value to filter itself.

Best method for rendering collection in Backbone

I'm using Backbone and I have the following model and collection
App.Models.Person = Backbone.Model.extend({
});
App.Collections.People = Backbone.Collection.extend({
model: App.Models.Person,
url: 'api/people',
});
However what I'm struggling on is the best way to render this collection. Here's how I've done it so far which works but doesn't seem to be the most elegant solution
App.Views.PeopleView = Backbone.View.extend({
el: $(".people"),
initialize: function () {
this.collection = new App.Collections.People();
//This seems like a bad way to do it?
var that = this;
this.collection.fetch({success: function(){
that.render();
}});
},
render: function () {
var that = this;
_.each(this.collection.models, function (item) {
that.renderPerson(item);
}, this);
},
I'm fairly new to Backbone but have to assign this to a different variable to I use it inside of the success function just seems like a bad way of doing things? Any help on best practices would be appreciated.
Backbone allows you to register for events that you can react to. When the collection is synchronized with the server, it will always fire the sync event. You can choose to listen for that event and call any given method. For instance ...
initialize: function () {
this.collection = new App.Collections.People();
this.listenTo(this.collection, "sync", this.render);
// Fetch the initial state of the collection
this.collection.fetch();
}
... will set up your collection so that it would always call this.render() whenever sync occurs.
The docs on Backbone Events are succinct but pretty good. Keep in mind a few things:
The method you use to register event listeners (i.e. listenTo or on) changes how you provide the context of the called function. listenTo, for instance, will use the current context automatically; on will not. This piece of the docs explains it pretty well.
If you need to remove a view, you will need to disconnect event listeners. The easiest way to do that is to use listenTo to connect them in the first place; then when destroying the view you can just call view.stopListening().
For rendering, there are a lots of suggestions for how to do it. Generally having a view to render each individual model is one way. You can also use Backbone.Collection#each to iterate over the models and control the scope of the iterating function. For instance:
render: function() {
this.collection.each(function(model) {
var view = new App.Collections.PersonView({ model: model });
view.render();
this.$el.append(view.$el);
}, this);
}
Note the second argument to .each specifies the scope of the iterator. (Again, have a look at the docs on controlling scope. If you'd rather have a framework help out with the rendering, check out Marionette's CollectionView and ItemView.
If your view is supposed to just render the collection you can send the collection to temnplate and iterate through in template, otherwise you can create another subView for that purpose or send the individual models of the collection to another subview and append to the container, hope it was helpful.

Backbone: How to save an attribute change of a model right from its collection?

This is not so specific but common question about my Backbone architecture understanding.
So I need to edit Product attributes right from the list.
I created a Backbone Model for Product and Backbone Collection for the list of Products.
In the list you can choose any model and system should slide down Edit Interface for this model.
So I launch the view where I want to render the list and show/edit the specific model from the collection:
App.Views.ProductList = Backbone.View.extend({
el: '#content',
initialize: function() {
this.render();
},
events: {
'click #show': 'show', // to show model interface in the list
'click #save': 'save' // to save model changes in the list
},
show: function(e) {
var id = $(e.currentTarget).data('id');
$('#product' + id).slideToggle();
},
save: function(e) {
var id = $(e.currentTarget).data('id');
var form = $('#product' + id + ' form');
var model = this.collection.at(id);
model.set(form.serializeJSON());
model.save({
url: '/product'
});
},
render: function() {
var template = _.template($('#productList').html());
this.$el.empty().append(
template({
products: this.collection.toJSON()
})
);
return this;
}
});
So, guys, Im sure that in the way of updating a model from its collection Im going with very very wrong pattern. I think collections are able to listen to changes of collection models.
The way I choose model.save() uses collections url not models url and I cant override collections url. Please, explain how you would solve this problem!
One thing I always like to do to avoid problems like these is to create a "list view" and an "item view", and render multiple item views on the list view. This allows me to have two separate events hashes: One at the item view level, and one at the list view level. Doing this also makes it obvious where Backbone event handling should go (item view if we are focused on one model, list view if we are focused on the collection).
Remember also that Backbone collections proxy all model events to their own event triggers, so it becomes possible to do collection.on("change:someModelAttr"). All of those callbacks will get the model that was affected, too.
The problem described in the end was cos of in the model I define url not urlRoot.

A Backbone.js view that is a simple select list

I've built a Backbone-powered library that allows a user to add/remove items, much like the Todos example.
Every time an item is add or removed - or the entire collection is refreshed - I need two other select elements that are on other areas of the page to re-populate with the latest items as options. How would this be implemented, do I simply re-populate the select element in the render function of the view which holds a reference to the collection?
I'm tempted to create a view just for the select options but this seems like overkill, especially when considering the view doesn't need to re-act to any events. The select options are used by other views to populate form data.
You're correct: create a unique view for each select option. It's not overkill at all; that's what Views are for. They listen for events from their models, in this case the item list, and redraw themselves upon receiving an event. They have container designations, so once you've established those in the parameters for the View subclass, you never need to think about them again. You can style them independently.
That's the whole point of the Views being the way they are.
More importantly, you could also abstract out "view of a list of things," and then each of your specific views can inherit from that view, and add two features: the filter ("latest"), and the renderer. You have to write the renderer anyway; you may as well exploit a little syntatic sugar to make it clear what you're rendering where. It's better than writing comments.
Not to distract from Elf Sternberg's already excellent answer, but to add a little context:
Don't loop over collections in your templates
If you want to do this, you might as well just use HTML partials and
AJAX. Instead, use a Backbone View that renders its own views (the
granularity is what minimizes server syncs and page refreshes). This
is recursive, you can repeat this pattern until there is no more
associated data to loop over.
— Jonathan Otto in A Conceptual Understanding of Backbone.js For The Everyman
Of course, there are a few gotchas when you want to render subviews.
I did a code search to try and find some examples of how to do this. Turns out that the TodoMVC example is a good model for doing this. From the Strider-CD source (MIT License):
var UserView = Backbone.View.extend({
//... is a class. not sure how to put that here
tagName: "option",
// Cache the template function for a single item.
template: _.template($('#user-item-template').html()),
// The DOM events specific to an item.
// maybe could put links here? but then user couldn't see on mouse-over
// The UserView listens for changes to its model, re-rendering. Since there's
// a one-to-one correspondence between a **User** and a **UserView** in this
// app, we set a direct reference on the model for convenience.
initialize: function() {
_.bindAll(this, 'render');
this.model.bind('change', this.render);
this.model.bind('destroy', this.remove);
},
// Re-render the contents of the User item.
render: function() {
$(this.el).html(this.template(this.model.toJSON()));
return this;
},
// Remove the item, destroy the model.
clear: function() {
this.model.clear();
}
});
// The Application
// ---------------
// Our overall **AppView** is the top-level piece of UI.
var UsersView = Backbone.View.extend({
// Instead of generating a new element, bind to the existing skeleton of
// the App already present in the HTML.
el: $("#user-form"),
// no events here either at this time
// At initialization we kick things off by
// loading list of Users from the db
initialize: function() {
_.bindAll(this, 'addAll', 'addOne','render');
Users.bind('add', this.addOne);
Users.bind('reset', this.addAll);
Users.bind('all', this.render);
console.log("fetching Users");
Users.fetch();
},
// Re-rendering the App just means refreshing the statistics -- the rest
// of the app doesn't change.
render: function() {
console.log("rendering User AppView");
// might want to put some total stats for the Users somewhere on the page
},
// Add a single todo item to the list by creating a view for it, and
// appending its element to the `<ul>`.
addOne: function(User) {
console.log("adding one User: " + User.get("id") + "/" + User.get("email"));
var view = new UserView({model: User});
this.$("#user-list").append(view.render().el);
},
// Add all items in the **Users** collection at once.
addAll: function() {
console.log("adding all Users");
console.log(Users.length + " Users");
Users.each(this.addOne);
}
});
// Finally, we kick things off by creating the **App**.
console.log("starting userapp now");
var UsersApp = new UsersView();
});
Additional examples of a select list view with option sub-views can be found in:
Zipkin source
reviewboard source

backbone.js - collections and views

I understand how to get a collection together, or an individual model. And I can usually get a model's data to display. But I'm not clear at all how to take a collection and get the list of models within that collection to display.
Am I supposed to iterate over the collection and render each model individually?
Is that supposed to be part of the collection's render function?
Or does the collection just have it's own view and somehow I populate the entire collection data into a view?
Just speaking generally, what is the normal method to display a list of models?
From my experience, it's the best to keep in your collection view references to each model's view.
This snippet from the project I'm currently working on should help you get the idea better:
var MyElementsViewClass = Backbone.View.extend({
tagName: 'table',
events: {
// only whole collection events (like table sorting)
// each child view has it's own events
},
initialize: function() {
this._MyElementViews = {}; // view chache for further reuse
_(this).bindAll('add');
this.collection.bind('add', this.add);
},
render: function() {
// some collection rendering related stuff
// like appending <table> or <ul> elements
return this;
},
add: function(m) {
var MyElementView = new MyElementViewClass({
model: m
});
// cache the view
this._MyElementViews[m.get('id')] = MyElementView;
// single model rendering
// like appending <tr> or <li> elements
MyElementView.render();
}
});
Taking this approach allows you to update views more efficiently (re-rendering one row in the table instead of the whole table).
I think there are two ways to do it.
Use a view per item, and manipulate the DOM yourself. This is what the Todos example does. It's a nice way to do things because the event handling for a single model item is clearer. You also can do one template per item. On the downside, you don't have a single template for the collection view as a whole.
Use a view for the whole collection. The main advantage here is that you can do more manipulation in your templates. The downside is that you don't have a template per item, so if you've got a heterogeneous collection, you need to switch in the collection view template code -- bletcherous.
For the second strategy, I've done code that works something like this:
var Goose = Backbone.Model.extend({ });
var Gaggle = Backbone.Collection.extend({
model: Goose;
};
var GaggleView = Backbone.View.extend({
el: $('#gaggle'),
template: _.template($('#gaggle-template').html()),
render: function() {
$(this.el).html(this.template(this.model.toJSON()));
}
};
var g = new Gaggle({id: 69);
g.fetch({success: function(g, response) {
gv = new GaggleView({model: g});
gv.render();
}});
The template code would look something like this:
<script type="text/template" id="gaggle-template">
<ul id="gaggle-list">
<% _.each(gaggle, function(goose) { %>
<li><%- goose.name %></li>
<% }); %>
</ul>
</script>
Note that I use the _ functions (useful!) in the template. I also use the "obj" element, which is captured in the template function. It's probably cheating a bit; passing in {gaggle: [...]} might be nicer, and less dependent on the implementation.
I think when it comes down to it the answer is "There are two ways to do it, and neither one is that great."
The idea of backbone is that view rendering is event driven.
Views attach to Model data change events so that when any data in the model changes the view updates itself for you.
What you're meant to do with collections is manipulate a collection of models at the same time.
I would recommend reading the annotated example.
I've also found this a confusing part of the Backbone framework.
The example Todos code is an example here. It uses 4 classes:
Todo (extends Backbone.Model). This represents a single item to be todone.
TodoList (extends Backbone.Collection). Its "model" property is the Todo class.
TodoView (extends Backbone.View). Its tagName is "li". It uses a template to render a single Todo.
AppView (extends Backbone.View). Its element is the "#todoapp" . Instead of having a "model" property, it uses a global TodoList named "Todos" (it's not clear why...). It binds to the important change events on Todos, and when there's a change, it either adds a single TodoView, or loops through all the Todo instances, adding one TodoView at a time. It doesn't have a single template for rendering; it lets each TodoView render itself, and it has a separate template for rendering the stats area.
It's not really the world's best example for first review. In particular, it doesn't use the Router class to route URLs, nor does it map the model classes to REST resources.
But it seems like the "best practice" might be to keep a view for each member of the collection, and manipulate the DOM elements created by those views directly.

Categories