rerender Backbone views without losing references to dom - backbone.js

I have the following problem with backbone and I'd like to know what strategy is the more appropriated
I have a select control, implemented as a Backbone view, that initially loads with a single option saying "loading options". So I load an array with only one element and I render the view.
The options will be loaded from a collection, so I fire a fetch collection.
Then I initialize a component that is in charge of displaying in line errors for every field. So I save a reference of the dom element of the combo.
When the fetch operation is finally ready, I rerender the control with all the options loaded from the collection.
To render the view I user something like this:
render: function() {
this.$el.html(this.template(this.model.attributes));
return this;
}
pretty standard backbone stuff
the problem is that after rendering the view for the second time the reference of the dom is no longer valid,
perhaps this case is a bit strange, but I can think of lots of cases in which I have to re-render a view without losing their doms references (a combo that depends on another combo, for example)
So I wonder what is the best approach to re-render a view without losing all the references to the dom elements inside the view...

The purpose of Backbone.View is to encapsulate the access to a certain DOM subtree to a single, well-defined class. It's a poor Backbone practice to pass around references to DOM elements, those should be considered internal implementation details of the view.
Instead you should have your views communicate directly, or indirectly via a mediator.
Direct communication might look something like:
var ViewA = Backbone.View.extend({
getSelectedValue: function() {
return this.$(".combo").val()
}
});
var ViewB = Backbone.View.extend({
initialize: function(options) {
this.viewA = options.viewA;
},
doSomething: function() {
var val = this.viewA.getSelectedValue();
}
});
var a = new ViewA();
var b = new ViewB({viewA:a});
And indirect, using the root Backbone object as a mediator:
var ViewA = Backbone.View.extend({
events: {
"change .combo" : "selectedValueChanged"
},
selectedValueChanged: function() {
//publish
Backbone.trigger('ViewA:changed', this.$('.combo').val());
}
});
var ViewB = Backbone.View.extend({
initialize: function(options) {
//subscribe
this.listenTo(Backbone, 'ViewA:changed', this.doSomething);
},
doSomething: function(val) {
//handle
}
});
var a = new ViewA();
var b = new ViewB();
The above is very generic, of course, but the point I'm trying to illustrate here is that you shouldn't have to worry whether the DOM elements are swapped, because no other view should be aware of the element's existence. If you define interfaces between views (either via method calls or mediated message passing), your application will be more maintainable and less brittle.

Related

Backbone pre rendering of collection models

I want to perform an action, clearing parent element, after a collection has fetched his models but prior to the models rendering.
I've stumbled upon before and after render methods yet they are model specific, which will cause my parent element to clear before every model rendering.
I'm able of course to perform the action pre-fetching yet I want it to occur when fetch is done and before models are rendered.
I tried using reset and change events listening on the collection yet both resulted unwanted end result.
Reset event seamed to go in that direction yet the passed argument was the entire collection and not a single model from the collection, therefore using the add event callback wasn't possible due to difference in argument type (collection and not a model as required)
Any ideas how to invoke a callback when fetch a collection fetch is successful yet models are yet to be rendered?
The model contains the returned attributes while collection contains url for fetching and parse method to return argument wrapped object.
Below is the code I use to render the collection view, which is basically rendering each model's view within the collection.
Collection View
---------------
var FoosView = Backbone.View.extend({
el: '#plans',
events: {
//'click tr': 'rowClick'
},
initialize: function() {
this.listenTo(this.collection, 'add', this.renderNew);
_.bindAll(this, "render");
this.render();
},
renderNew: function(FooModel) {
var item = new FooView({model: FooModel});
this.$el.prepend(item.render().$el);
}
...
});
The model view
--------
var FooView = Backbone.View.extend({
tagName: 'li',
initialize: function(options) {
this.options = options || {};
this.tpl = _.template(fooTpl);
},
render: function() {
var data = this.model.toJSON();
this.$el.html(this.tpl(data));
return this;
}
});
Thanks in advance.
OK, I think I understand your question and here is a proposed solution. You are now listening to the reset event on your collection and calling this.renderAll. this.renderAll will take the list of models from the collection and render them to the page, but only AFTER the list element has been emptied. Hope this helps :).
var FoosView = Backbone.View.extend({
el: '#plans',
collection: yourCollection, // A reference to the collection.
initialize: function() {
this.listenTo(this.collection, 'add', this.renderNew);
this.listenTo(this.collection, 'reset', this.renderAll);
},
renderAll: function() {
// Empty your list.
this.$el.empty();
var _views = []; // Create a list for you subviews
// Create your subviews with the models in this.collection.
this.collection.each(function(model) {
_views.push(new FooView({model: model});
});
// Now create a document fragment so as to not reflow the page for every subview.
var container = document.createDocumentFragment();
// Append your subviews to the container.
_.each(_views, function(subview) {
container.appendChild(subview.render().el);
});
// Append the container to your list.
this.$el.append(container);
},
// renderNew will only run on the collections 'add' event.
renderNew: function(FooModel) {
var item = new FooView({model: FooModel});
this.$el.prepend(item.render().$el);
}
});
I am forced to assume a few things about you html, but I think the above code should be enough to get you up and running. Let me know if it works.
I'm not totally sure about what you are asking but have you tried:
MyCollection.fetch({
success: function(models,response) {
//do stuff here
}
});
Also you may be interested taking a look at http://backbonejs.org/#Model-parse
Hope it helps!
Edit: there is no direct link between fetching and rendering my bet is that you binded rendering to model change.
USE===============>>>> http://backbonejs.org/#Model-parse

backbone js view event binding only to views elements

Hi I'm learning backbone and I am having trouble with binding events to views. My problem is that I have a view constructor that when called, binds all views to a button press event that is only part of one view. I would like the button press event to be bound to only the 1 view that contains the button.
http://jsbin.com/tunazatu/6/edit?js,console,output
click on all of the view buttons
then click back to view 1
click the red button (all view's models console.log their names)
So I've looked at the code from this post mutliple event firing which shows that you can have multiple views that have the same el thru tagName but map events only to their html elements. This is also what is done in the localtodos example from Jérôme Gravel-Niquet
I have also tried not declaring el /tunazatu/7/edit?js,console,output but then it seems like no event gets bound.
var AppView = Backbone.View.extend({
tagName:"div", //tagName defined
getName:function(){
console.log(this.model.get('name'));
},
initialize:function(options){
this.listenTo(this.model, 'change', this.render);
var temp_mapper = {appView1:'#route1',appView2:'#route2',appView3:'#route3'};
var m_name = this.model.get('name');
this.template = _.template($(temp_mapper[m_name]).html()); //choose the correct template
},
render:function(){
var temp = this.template(this.model.toJSON()); //populate the template with model data
var newElement = this.$el.html(temp); //put it in the view's tagName
$('#content').html(newElement);
},
events:{
"click button":"log"
},
log:function(){
this.getName();
}
});
Your problem is that your AppView really looks like this:
var AppView = Backbone.View.extend({
el: "#content",
//...
Every time you create a new AppView, you bind another event delegator to #content but you never remove those delegations. If you create three AppViews, you end up with three views listening to click button inside #content.
I would recommend two things:
Avoid trying to re-use views, create and destroy them (via View#remove) as needed. Views should be lightweight enough that putting them together and tearing them down should be cheap.
Don't bind multiple views to the same el. Instead, let each view create its own el and then let the caller put that el inside some container.
If you do both of those things then your problem will go away. Your AppView would look more like this:
var AppView = Backbone.View.extend({
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this; // Common practise, you'll see why shortly.
},
// As you already have things...
});
Then your router methods would look more like this:
view1: function() {
if(this.appView)
this.appView.remove();
this.appView = this.createView('appView1');
$('#content').html(this.appView.render().el);
// that `return this` is handy ----------^^
},
If you must stick with your current approach then you'll have to call undelegateEvents on the current AppView before you render another one and delegateEvents on the new AppView after you render it.
But really, don't be afraid to destroy views that you don't need right at this moment: destroy any view that you don't need on the page right now and create new instances when you need them. There are cases where you don't want to destroy your views but you can usually avoid it.

events not firing after re-render in backbone.js

I am facing a problem while trying to click submit after re-render.
This is my view:
ShareHolderInfoView = Backbone.View.extend( {
template : 'shareholderinfo',
initialize: function() {
this.model = new ShareHolderInfoModel();
},
render : function() {
$.get("shareholderinfo.html", function(template) {
var html = $(template);
that.$el.html(html);
});
//context.loadViews.call(this);
return this;
},
events:{
"change input":"inputChanged",
"change select":"selectionChanged",
"click input[type=submit]":"showModel"
},
inputChanged:function(event){
var field = $(event.currentTarget);
var data ={};
data[field.attr('id')] = field.val();
this.model.set(data);
},
showModel:function(){
console.log(this.model.attributes);
alert(JSON.stringify(this.model.toJSON()));
}
});
This is my Router
var shareholderInfo, accountOwnerInfo;
App.Router = Backbone.Router.extend({
routes:{
'share':'share',
'joint':'joint'
},
share:function(){
$("#subSection").empty();
if(!shareholderInfo){
shareholderInfo = new ShareHolderInfoView();
$("#subSection").append(shareholderInfo.render().el);
} else{
$("#subSection").append(shareholderInfo.$el);
}
},
joint:function(random){
$("#subSection").empty();
if(!accountOwnerInfo){
accountOwnerInfo = new AccountOwnerInfoView();
$("#subSection").append(accountOwnerInfo.render().el);
} else{
$("#subSection").append(accountOwnerInfo.$el);
}
}
});
This is my HTML a div with id='subSection'.
if I check in console, I can able to see the events bound to that view.
Object {change input: "inputChanged", change select: "selectionChanged", click input[type=submit]: "showModel"}
But its not calling that showModel function afer i click submit. Please help.
Your fundamental problem is that you're improperly reusing views.
From the fine manual:
.empty()
Description: Remove all child nodes of the set of matched elements from the DOM.
[...]
To avoid memory leaks, jQuery removes other constructs such as data and event handlers from the child elements before removing the elements themselves.
So when you say:
$("#subSection").empty();
you're not just clearing out the contents of #subSection, you're also removing all event handlers attached to anything inside #subSection. In particular, you'll remove any event handlers bound to accountOwnerInfo.el or shareholderInfo.el (depending on which one is already inside #subSection).
Reusing views is usually more trouble than it is worth, your views should be lightweight enough that you can destroy and recreate them as needed. The proper way to destroy a view is to call remove on it. You could rewrite your router to look more like this:
App.Router = Backbone.Router.extend({
routes: {
'share':'share',
'joint':'joint'
},
share: function() {
this._setView(ShareHolderInfoView);
},
joint: function(random){
this._setView(AccountOwnerInfoView);
},
_setView: function(view) {
if(this.currentView)
this.currentView.remove();
this.currentView = new view();
$('#subSection').append(this.currentView.render().el);
}
});
If your views need any extra cleanup then you can override remove on them to clean up the extras and then chain to Backbone.View.prototype.remove.call(this) to call the default remove.
If for some reason you need to keep your views around, you could call delegateEvents on them:
delegateEvents delegateEvents([events])
Uses jQuery's on function to provide declarative callbacks for DOM events within a view. If an events hash is not passed directly, uses this.events as the source.
and you'd say things like:
$("#subSection").append(shareholderInfo.$el);
shareholderInfo.delegateEvents();
instead of just:
$("#subSection").append(shareholderInfo.$el);
I'd strongly recommend that you treat your views and cheap ephemeral objects: destroy them to remove them from the page, create new ones when they need to go on the page.

Multiple backbone views referencing one collection

I am trying to create my first backbone app and am having some difficulty getting my head around how I am meant to be using views.
What I am trying to do is have a search input that each time its submitted it fetches a collection from the server. I want to have one view control the search input area and listen to events that happen there (a button click in my example) and another view with sub views for displaying the search results. with each new search just prepending the results into the search area.
the individual results will have other methods on them (such as looking up date or time that they where entered etc).
I have a model and collection defined like this:
SearchResult = Backbone.model.extend({
defaults: {
title: null,
text: null
}
});
SearchResults = Backbone.Collection.extend({
model: SearchResult,
initialize: function(query){
this.query = query;
this.fetch();
},
url: function() {
return '/search/' + this.query()
}
});
In my views I have one view that represents the search input are:
var SearchView = Backbone.View.extend({
el: $('#search'),
events: {
'click button': 'doSearch'
},
doSearch: function() {
console.log('starting new search');
var resultSet = new SearchResults($('input[type=text]', this.el).val());
var resultSetView = new ResultView(resultSet);
}
});
var searchView = new SearchView();
var ResultSetView = Backbone.View.extend({
el: $('#search'),
initialize: function(resultSet) {
this.collection = resultSet;
this.render();
},
render: function() {
_(this.collection.models).each(function(result) {
var resultView = new ResultView({model:result});
}, this);
}
});
var ResultView = Backbone.view.extend({
tagName: 'div',
model: SearchResult,
initialize: function() {
this.render();
},
render: function(){
$(this.el).append(this.model.get(title) + '<br>' + this.model.get('text'));
}
});
and my html looks roughly like this:
<body>
<div id="search">
<input type="text">
<button>submit</button>
</div>
<div id="results">
</div>
</body>
In my code it gets as far as console.log('starting new search'); but no ajax calls are made to the server from the initialize method of the ResultSetView collection.
Am I designing this right or is there a better way to do this. I think because the two views bind to different dom elements I should not be instantiating one view from within another. Any advice is appreciated and if I need to state this clearer please let me know and I will do my best to rephrase the question.
Some problems (possibly not the only ones):
Your SearchView isn't bound to the collection reset event; as written it's going to attempt to render immediately, while the collection is still empty.
SearchView instantiates the single view ResultView when presumably it should instantiate the composite view ResultSetView.
You're passing a parameter to the SearchResults collection's constructor, but that's not the correct way to use it. See the documentation on this point.
You haven't told your ResultSetView to listen to any events on the collection. "fetch" is asynchronous. When completed successfully, it will send a "reset" event. Your view needs to listen for that event and then do whatever it needs to do (like render) on that event.
After fixing all the typos in your example code I have a working jsFiddle.
You see like after clicking in the button an AJAX call is done. Of course the response is an error but this is not the point.
So my conclusion is that your problem is in another part of your code.
Among some syntax issues, the most probable problem to me that I see in your code is a race condition. In your views, you're making an assumption that the fetch has already retrieved the data and you're executing your views render methods. For really fast operations, that might be valid, but it gives you no way of truly knowing that the data exists. The way to deal with this is as others have suggested: You need to listen for the collection's reset event; however, you also have to control "when" the fetch occurs, and so it's best to do the fetch only when you need it - calling fetch within the search view. I did a bit of restructuring of your collection and search view:
var SearchResults = Backbone.Collection.extend({
model: SearchResult,
execSearch : function(query) {
this.url = '/search/' + query;
this.fetch();
}
});
var SearchView = Backbone.View.extend({
el: $('#search'),
initialize : function() {
this.collection = new SearchResults();
//listen for the reset
this.collection.on('reset',this.displayResults,this);
},
events: {
'click button': 'doSearch'
},
/**
* Do search executes the search
*/
doSearch: function() {
console.log('starting new search');
//Set the search params and do the fetch.
//Since we're listening to the 'reset' event,
//displayResults will execute.
this.collection.execSearch($('input[type=text]', this.el).val());
},
/**
* displayResults sets up the views. Since we know that the
* data has been fetched, just pass the collection, and parse it
*/
displayResults : function() {
new ResultSetView({
collection : this.collection
});
}
});
Notice that I only created the collection once. That's all you need since you're using the same collection class to execute your searches. Subsequent searches only need to change the url. This is better memory management and a bit cleaner than instantiating a new collection for each search.
I didn't work further on your display views. However, you might consider sticking to the convention of passing hashes to Backbone objects. For instance, in your original code, you passed 'resultSet' as a formal parameter. However, the convention is to pass the collection to a view in the form: new View({collection: resultSet}); I realize that that's a bit nitpicky, but following the conventions improves the readability of your code. Also, you ensure that you're passing things in the way that the Backbone objects expect.

Can't get iscroll4 to play with backbone view

I am trying to use iScroll4 inside a backbone.js application. I have several dynamically loaded lists, and I want to initialize iScroll after the appropriate view has loaded.
I'm trying to call 'new iScroll' when the list view finishes loading, but cannot for the life of me figure out how to do this.
Has anyone gotten these two to work together? Is there an example out there of a backbone view initializing a scroller once its element has loaded?
you are correct, you have to load the view first,
or defenately refresh iscroll afterwards
in our applications, we usually use the render method to render the view
and have a postRender method that handles initialization of these extra plugins like iscroll
of course you need some manual work to get it done but this is the gist of it:
var myView = Backbone.View.extend({
// more functions go here, like initialize and stuff... but I left them out because only render & postRender are important for this topic
// lets say we have a render method like this:
render: function() {
var data = this.collection.toJSON();
this.$el.html(Handlebars.templates['spotlightCarousel.tmpl'](data));
return this;
},
// we added the postRender ourself:
postRender: function() {
var noOfSlides = this.collection.size();
$('#carouselscroller').width(noOfSlides * 320);
this.scroller = new IScroll('carouselwrapper', {
snap: true,
momentum: false,
hScrollbar: false
});
}
});
now the calling of these methods
we did this outside our view as we like some view manager to handle this
but it boils down to this
var col = new myCollection();
var view = new myView({ collection: col });
$('#wrapper').html(view.render().$el); // this chaining is only possible due to the render function returning the whole view again.
// here we always test if the view has a postRender function... if so, we call it
if (view.postRender) {
view.postRender();
}

Resources