How to handle nested views with Backbone.js? - javascript

In my app i have a few tagList, each one contains a few tags grouped by index_name.
I'm lost on how should i handle that with Backbone views.
I have a TagListView that extends Backbone.View, i guess i'll handle all events with this view.
My main question i : should i create a TagView with a render function that would be created & rendered for each tag in the TagListView render function ?
Here is what i do so far in my view : (is this ok for initialization ?!)
<ul id="strategy-tags">
<!-- initial data -->
<script type="text/javascript">
AppData.strategyTagList = $.parseJSON('<?php echo json_encode($strategyTagList); ?>');
strategyTagListView = new App.TagListView({el : "#strategy-tags", data: AppData.strategyTagList});
</script>
</ul>
Inside my core.js :
App.TagListView = Backbone.View.extend({
// dom event specific to this item.
events: {
},
initialize: function(options) {
this.el = options.el;
},
render: function() {
// let's construct the tag list from input data
_.each(options.data, function(index) {
// render index? <-- how ?
_.each(index, function(tag) {
// render tag? <-- how ?
console.log(tag);
});
});
return this;
}
});
Thanks a lot.

I would say yes, the benefit of the individual 'item' view being able to re-render individually is that if you make changes to the model behind such an item, only that item will need to be re-rendered by the browser. Which is best for performance.

It seems to be a question of granularity here, and one I've asked myself on occasion. My advice would be not to over do the views. It is akin to creating objects for everything in java - sometimes a simple string will suffice. If you find a case of increased granularity in the future you can always come back and change it.

Related

How to clear Backbone zombie views

I am trying to make my very first search app.
After the app is built, every DOM is rendering as I expect and events work as well. When I dig deeper into it, I find a strange behavior, and after I did some search, I found it is because of zombie view events delegate issue.
Here is some part of my code:
var searchList = Backbone.View.extend({
events:{
'click #submit':function() {
this.render()
}
},
render() {
this.showList = new ShowList({el:$('.ADoM')});
}
});
When #submit is clicked, a new instance of ShowList will be created and '.ADoM' DOM will be rendering.
showList.js
var showList = Backbone.View.extend({
events: {
"click .testing": function(e) {
console.log(e.currentTarget);
},
},
initialize() {
this.$el.html(SearchListTemplate());
}
});
The '.testing' button event is bound with it.
So as what 'zombie' does, after multiple clicks on submit, then clicking the '.testing' button, console.log() will output multiple time.
I have followed the article here and tried to understand and fix my issue, and also tried to add this.remove() in showList.js as someone mentioned, but unfortunately it may because I was not able to place them in the proper place in my code, the issue is still unsolved.
That has nothing to do with ES6, this is basic JavaScript and DOM manipulation.
Do not share the same element in the page
You're creating new ShowList instances which are bound to the same element in the page. In Backbone, that's bad practice.
Each Backbone View instance has its own root element on which events are bound. When multiple views share that same element, events are triggered on each instance, and you can't call remove on the view since it will remove the DOM element from the page completely.
You should dump the child view root element within the element you wish to reuse.
this.$('.ADoM').html(this.showList.render().el);
Reusing the view
The render function should be idempotent.
var searchList = Backbone.View.extend({
events: {
// you can use a string to an existing view method
'click #submit': 'render'
},
initialize() {
// instanciate the view once
this.showList = new ShowList();
},
// This implementation always has the same result
render() {
this.$('.ADoM').html(this.showList.render().el);
// Backbone concention is to return 'this' in the render function.
return this;
}
});
Your other view could be simplified as well to reflect the changes from the parent view.
var showList = Backbone.View.extend({
events: {
"click .testing": 'onTestingClick',
},
// Don't render in the initialize, do it in the render function
render() {
this.$el.html(SearchListTemplate());
},
onTestingClick(e) {
console.log(e.currentTarget);
}
});
This is a super basic example on reusing a view instead of always creating a new one.
A little cleanup is necessary
When done with a view, you should call remove on it:
Removes a view and its el from the DOM, and calls stopListening to
remove any bound events that the view has listenTo'd.
For this to work, when registering callbacks on model or collection events, use listenTo over on or bind to avoid other memory leaks (or zombie views).
A good pattern for view having multiple child views is to keep a reference of each child views and call remove on each of them when the parent gets removed.
See how to avoid memory leaks when rendering list views. When dealing with a lot of views (big list or tree of views), there are ways to efficiently render with Backbone which involves DocumentFragment batching and deferring.

Why people formally do this.$el.html() in the very first line of backbone view rendering?

There's a Backbone.js view render function below:
var bookListView = Backbone.View.extend({
model: BooksCollection,
render: function() {
this.$el.html(); // lets render this view
var self = this;
for (var i = 0; i < this.model.length; ++i) {
// lets create a book view to render
var m_bookView = new bookView({
model: this.model.at(i)
});
// lets add this book view to this list view
this.$el.append(m_bookView.$el);
m_bookView.render(); // lets render the book
}
return this;
},
});
I just came to know that people usually add this.$el.html(); at the begining of the render function for rendering the view.
However, I have no idea why this code is used on the first line. It just works totally same, even though I remove the first line.
That line you usually see is this.$el.html('');. Note the extra '' which makes it a setter, rather than getter. It is used to clear the existing content in the view similar to this.$el.empty(). If it is not there, when you re-render the view to display changed data, the old data will stay in it, since it is a collection view to which items are just appended.
In the context of this particular example, let's say you have 2 books now and if the data changes to 3 books, you re-render this view - you'll have 5 books in it without the suspicious line.
As it is this.$el.html(); doesn't do anything in the code you shared, but this.$el.html(''); fixes a potential bug. I think you missed ''

Unable to add components(buttons) dyanamically in Enyo

I am using a window view in Enyo, which basically fetches data from database, and based on the no. items fetched, multiple buttons are created dyanamically. On click of any of the button, an another call to database is made to fetch other set of items. The fetched items need to be added dyanamically to a <ul> item as buttons. Which is done by the code -
testPOSView : function(inSender, inEvent) {
var data = inEvent.data;
console.log(data.tables);
enyo.forEach(data.tables, function(table) {
console.log(table);
this.$.sectiontablebar.createComponent({
kind : 'OB.OBPOSPointOfSale.UI.TablesButton',
button : {
kind : 'OB.UI.Section',
content: table.tableName,
id: table.tableId
}
});
}, this);
}
But when i click on the button, i am getting the results from DB, but they are not added to the sectiontablebar component.
The complete code of the file is available # https://gist.github.com/sangramanand/ad665db9cd438001254a
Any help would be appreciated. Thanks!!!
I'm not certain this is the way to do it, but when I create subcomponents dynamically, I add this.render() to the end of the function. This renders the component, thus showing the dynamically added content.
If I were to rewrite your code I would do it like this:
testPOSView : function(inSender, inEvent) {
var data = inEvent.data;
enyo.forEach(data.tables, function(table) {
// create the sub-component in "this"
this.createComponent({
// and assign the container
container: this.$.sectiontablebar,
kind : 'OB.OBPOSPointOfSale.UI.TablesButton',
button : {
kind : 'OB.UI.Section',
content: table.tableName,
id: table.tableId
}
});
}, this);
this.render();
}
More than likely you are losing the pointer to this when testPOSView is running after an async call to get the db results.
In your anythingTap function (see gist, readers) you might try binding the function you're sending to OB.DS.Process:
this.bindSafely(function(data) {me.doTestPOSView();}))
By the way, you can probably eliminate all that me = this tomfoolery if you bind your functions properly.

BackboneJS Rendering Problems

For the last six months I've been working with Backbone. The first two months were messing around, learning and figuring out how I want to structure my code around it. The next 4 months were pounding away a production-fit application. Don't get me wrong, Backbone has saved me from the thousands-lines mess of client side code that were the standard before, but it enabled me to do more grandiose things in less time, opening up a complete new stack of problems. For all the questions I raise here there are simple solutions that feels like hacks or just feel wrong. I promise a 300 points bounty for an awesome solution. Here goes:
Loading - For our use case (an admin panel) pessimistic syncing is bad. For some things I need to validate things on the server before accepting them. We started out before the 'sync' event was merged into Backbone,
and we used this little code for mimicking the loading event:
window.old_sync = Backbone.sync
# Add a loading event to backbone.sync
Backbone.sync = (method, model, options) ->
old_sync(method, model, options)
model.trigger("loading")
Great. It works as expected but it doesn't feel correct. We bind this event to all the relevant views and display a loading icon until we receive a success or error event from that model. Is there a better, saner, way to do this?
Now for the hard ones:
Too many things render themselves too much - Let's say our application have tabs. Every tab controls a collection. On the left side you get the collection. You click a model to start editing it at the center. You change its name and press tab to get to the next form item. Now, your app is a "real time something something" that notices the difference, runs validations, and automatically sync the change to the server, no save button required! Great, but the H2 at the start of the form is the same name as in the input - you need to update it. Oh, and you need to update the name on the list to the side. OH, and the list sorts itself by names!
Here's another example: You want to create a new item in the collection. You press the "new" button and you start filling out the form. Do you immediately add the item to the collection? But what happens if you decided to discard it? Or if you save the entire collection on another tab? And, there's a file upload - You need to save and sync the model before you can start uploading the file (so you can attach the file to the model). So everything starts rendering in tremors: You save the model and the list and the form renders themselves again - it's synced now, so you get a new delete button, and it shows in the list - but now the file upload finished uploading, so everything starts rendering again.
Add subviews to the mix and everything starts looking like a Fellini movie.
It's subviews all the way down - Here's a good article about this stuff. I could not, for the love of everything that is holy, find a correct way to attach jQuery plugins or DOM events to any view that has subviews. Hell ensues promptly. Tooltips hear a render coming a long and start freaking around, subviews become zombie-like or do not respond. This is the main pain points as here actual bugs stand, but I still don't have an all encompassing solution.
Flickering - Rendering is fast. In fact, it is so fast that my screen looks like it had a seizure. Sometimes it's images that has to load again (with another server call!), so the html minimizes and then maximizes again abruptly - a css width+height for that element will fix that. sometimes we can solve this with a fadeIn and a fadeOut - which are a pain in the ass to write, since sometimes we're reusing a view and sometimes creating it anew.
TL;DR - I'm having problems with views and subviews in Backbone - It renders too many times, it flickers when it renders, subviews detach my DOM events and eat my brains.
Thank you!
More details: BackboneJS with the Ruby on Rails Gem. Templates using UnderscoreJS templates.
Partial rendering of views
In order to minimize the full rendering of your DOM hierarchy, you can set up special nodes in your DOM that will reflect updates on a given property.
Let's use this simple Underscore template, a list of names:
<ul>
<% _(children).each(function(model) { %>
<li>
<span class='model-<%= model.cid %>-name'><%= model.name %></span> :
<span class='model-<%= model.cid %>-name'><%= model.name %></span>
</li>
<% }); %>
</ul>
Notice the class model-<%= model.cid %>-name, this will be our point of injection.
We can then define a base view (or modify Backbone.View) to fill these nodes with the appropriate values when they are updated:
var V = Backbone.View.extend({
initialize: function () {
// bind all changes to the models in the collection
this.collection.on('change', this.autoupdate, this);
},
// grab the changes and fill any zone set to receive the values
autoupdate: function (model) {
var _this = this,
changes = model.changedAttributes(),
attrs = _.keys(changes);
_.each(attrs, function (attr) {
_this.$('.model-' + model.cid + '-' + attr).html(model.get(attr));
});
},
// render the complete template
// should only happen when there really is a dramatic change to the view
render: function () {
var data, html;
// build the data to render the template
// this.collection.toJSON() with the cid added, in fact
data = this.collection.map(function (model) {
return _.extend(model.toJSON(), {cid: model.cid});
});
html = template({children: data});
this.$el.html(html);
return this;
}
});
The code would vary a bit to accommodate a model instead of a collection.
A Fiddle to play with http://jsfiddle.net/nikoshr/cfcDX/
Limiting the DOM manipulations
Delegating the rendering to the subviews can be costly, their HTML fragments have to be inserted into the DOM of the parent.
Have a look at this jsperf test comparing different methods of rendering
The gist of it is that generating the complete HTML structure and then applying views is much faster than building views and subviews and then cascading the rendering. For example,
<script id="tpl-table" type="text/template">
<table>
<thead>
<tr>
<th>Row</th>
<th>Name</th>
</tr>
</thead>
<tbody>
<% _(children).each(function(model) { %>
<tr id='<%= model.cid %>'>
<td><%= model.row %></td>
<td><%= model.name %></td>
</tr>
<% }); %>
</tbody>
</table>
</script>
var ItemView = Backbone.View.extend({
});
var ListView = Backbone.View.extend({
render: function () {
var data, html, $table, template = this.options.template;
data = this.collection.map(function (model) {
return _.extend(model.toJSON(), {
cid: model.cid
});
});
html = this.options.template({
children: data
});
$table = $(html);
this.collection.each(function (model) {
var subview = new ItemView({
el: $table.find("#" + model.cid),
model: model
});
});
this.$el.empty();
this.$el.append($table);
return this;
}
});
var view = new ListView({
template: _.template($('#tpl-table').html()),
collection: new Backbone.Collection(data)
});
http://jsfiddle.net/nikoshr/UeefE/
Note that the jsperf shows that the template can be be split into subtemplates without too much penalty, which would allow you to provide a partial rendering for the rows.
On a related note, don't work on nodes attached to the DOM, this will cause unnecessary reflows. Either create a new DOM or detach the node before manipulating it.
Squashing zombies
Derick Bailey wrote an excellent article on the subject of eradicating zombie views
Basically, you have to remember that when you discard a view, you must unbind all listeners and perform any additional cleanup like destroying the jQuery plugin instances. What I use is a combination of methods similar to what Derick uses in Backbone.Marionette:
var BaseView = Backbone.View.extend({
initialize: function () {
// list of subviews
this.views = [];
},
// handle the subviews
// override to destroy jQuery plugin instances
unstage: function () {
if (!this.views) {
return;
}
var i, l = this.views.length;
for (i = 0; i < l; i = i + 1) {
this.views[i].destroy();
}
this.views = [];
},
// override to setup jQuery plugin instances
stage: function () {
},
// destroy the view
destroy: function () {
this.unstage();
this.remove();
this.off();
if (this.collection) {
this.collection.off(null, null, this);
}
if (this.model) {
this.model.off(null, null, this);
}
}
});
Updating my previous example to give the rows a draggable behavior would look like this:
var ItemView = BaseView.extend({
stage: function () {
this.$el.draggable({
revert: "invalid",
helper: "clone"
});
},
unstage: function () {
this.$el.draggable('destroy');
BaseView.prototype.unstage.call(this);
}
});
var ListView = BaseView.extend({
render: function () {
//same as before
this.unstage();
this.collection.each(function (model) {
var subview = new ItemView({
el: $table.find("#" + model.cid),
model: model
});
subview.stage();
this.views.push(subview);
}, this);
this.stage();
this.$el.empty();
this.$el.append($table);
return this;
}
});
http://jsfiddle.net/nikoshr/yL7g6/
Destroying the root view will traverse the hierarchy of views and perform the necessary cleanups.
NB: sorry about the JS code, I'm not familiar enough with Coffeescript to provide accurate snippets.
Ok, in order.. :)
Loading...
In case you want to validate data which stored on server, good practice do it on server-side. If validation on server will be unsuccessful, server should send not 200 HTTP code, therefore save metod of Backbone.Model will trigger error.
Other side, for validation data backbone has unimplemented validate method. I guess that right choise to implement and use it. But keep in mind that validate is called before set and save, and if validate returns an error, set and save will not continue, and the model attributes will not be modified. Failed validations trigger an "error" event.
Another way, when we call silent set(with {silent: true} param), we should call isValid method manually to validate data.
Too many things render themselves too much..
You have to separate your Views under their logic. Good practice for collection is separate view for each model. In this case you could render each element independently. And even more - when you initalizing your container view for collection, you could bind any event from each model in the collection to appropriate view, and they will render automatically.
Great, but the H2 at the start of the form is the same name as in the
input - you need to update it. Oh, and you need to update the name on
the list to the side.
you could use JQuery on method to implement callback which send value to display. Example:
//Container view
init: function() {
this.collection = new Backbone.Collection({
url: 'http://mybestpage.com/collection'
});
this.collection.bind('change', this.render, this);
this.collection.fetch();
},
render: function() {
_.each(this.collection.models, function(model) {
var newView = new myItemView({
model: model,
name: 'view' + model.id
});
this.$('#my-collection').append(newView.render().$el);
view.on('viewEdit', this.displayValue);
}, this);
},
...
displayValue: function(value) {
//method 1
this.displayView.setText(value); //we can create little inner view before,
//for text displaying. Сonvenient at times.
this.displayView.render();
//method 2
$(this.el).find('#display').html(value);
}
//View from collection
myItemView = Backbone.View.extend({
events: {
'click #edit': 'edit'
},
init: function(options) {
this.name = options.name;
},
...
edit: function() {
this.trigger('viewEdit', this.name, this);
}
OH, and the list sorts itself by names!
You can use sort method for backbone collections. But (!) Calling sort triggers the collection's "reset" event. Pass {silent: true} to avoid this. How to
Here's another example: You want to create a new item in the
collection...
When we press a "New" button we need to create a new model, but only when .save() method will trigger success, we should push this model to collection. In another case we should display error message. Of course we have no reasons to add a new model to our collection until it has been validated and saved on server.
It's subviews all the way down... subviews become zombie-like or do not respond.
when you (or any model) calling render method, all elements inside it will be recreated. So in case when you have subviews, you should call subView.delegateEvents(subView.events); for all of subviews; Probably this method is little trick, but it works.
Flickering..
Using thumbnails for big and medium images will minimize flickering in lot of cases. Other way, you could separate rendering of view to images and other content.
Example:
var smartView = Backbone.View.extend({
initialize: function(){
this.model.on( "imageUpdate", this.imageUpdate, this );
this.model.on( "contentUpdate", this.contentUpdate, this );
},
render: function(){
this.$el.html(this.template(this.model.toJSON()));
},
imageUpdate: function(){
this.$el.find('#image').attr('src', this.model.get('imageUrl'));
},
contentUpdate: function(){
this.$el.find('#content').html(this.model.get('content'));
}
})
I hope this helps anyone. Sorry for grammar mistakes, if any :)
Loading...
I'm a huge fan of eager loading. All my server calls are JSON responses, so it isn't a huge deal to make them more often than not. I usually refresh a collection every time it's needed by a view.
My favorite way to eager load is by using Backbone-relational. If I organize my app in a hierarchical manner. Consider this:
Organization model
|--> Event model
|--> News model
|--> Comment model
So when a user is viewing an organization I can eager load that organization's events and news. And when a user is viewing a news article, I eager load that article's comments.
Backbone-relational provides a great interface for querying related records from the server.
Too many things render themselves too much...
Backbone-relational helps here too! Backbone-relational provides a global record store that proves to be very useful. This way, you can pass around IDs and retrieve the same model elsewhere. If you update it in one place, its available in another.
a_model_instance = Model.findOrCreate({id: 1})
Another tool here is Backbone.ModelBinder. Backbone.ModelBinder lets you build your templates and forget about attaching to view changes. So in your example of collecting information and showing it in the header, just tell Backbone.ModelBinder to watch BOTH of those elements, and on input change, your model will be updated and on model change you view will be updated, so now the header will be updated.
It's subviews all the way down... subviews become zombie-like or do not respond...
I really like Backbone.Marionette. It handles a lot of the cleanup for you and adds an onShow callback that can be useful when temporarily removing views from the DOM.
This also helps to facilitate attaching jQuery plugins. The onShow method is called after the view is rendered and added to the DOM so that jQuery plugin code can function properly.
It also provides some cool view templates like CollectionView that does a great job of managing a collection and its subviews.
Flickering
Unfortunately I don't have much experience with this, but you could try pre-loading the images as well. Render them in a hidden view and then bring them forward.

Ember JS: observed properties on destroyed objects

I'm working on a simple app which displays a list of issues of a particular repo on Github. Below is the code of IssueView which generates the html of an issue and insert to the DOM
App.IssueView = Ember.View.extend({
tagName: "li",
classNames: ["sugar", "issue_wrapper"],
templateName: "app/templates/issue",
init: function() {
App.LabelsController.addObserver("label", this, this.labelUpdated);
this._super();
},
click: function(event) {
var target = event.target;
if (target.className == "title") {
// Using bindingContext is a temporary solution to access data of this issue
App.IssuesController.set("issue", this.bindingContext);
App.IssuesController.set("state", "viewIssueDetails").notifyPropertyChange("state");
}
},
labelUpdated: function() {
this.labels = this.labels || this._collectLabels(),
label = App.LabelsController.get("label").name;
this.set("isVisible", this.labels.indexOf(label) != -1);
},
_collectLabels: function() {
var labels = [];
this.bindingContext.labels.forEach(function(label) { labels.push(label.name) });
return labels;
}
})
The way I generate it is
<script type="text/x-handlebars">
{{#view App.IssuesListView}}
{{#each App.IssuesController}}
{{view App.IssueView contentBinding="this"}}
{{/each}}
{{/view}}
</script>
The problem I had is with this line
App.LabelsController.addObserver("label", this, this.labelUpdated);
Everytime a new IssueView is generated and inserted into the DOM, I got an error "You cannot set observed properties on destroyed objects" when the 'label' property of LabelsController is updated. When I look into Firebug I saw that my IssueView's state is "destroy" instead of inDOM. I wonder why that happened and what can I do to get around it?
The #each helper in your template will ensure that IssueViews are created and destroyed as the collection of issues changes. You are manually adding the observer, which means you are responsible for removing the observer, too. I believe that using the observes(...) function prototype extension will handle that for you. (See http://ember-docs.herokuapp.com/symbols/Ember.Observable.html under "Observing Property Changes").
If you want to pursue the manual route, consider moving the addObserver to didInsertElement and adding a corresponding removeObserver in willDestroyElement.
One side note: if I'm understanding what you are trying to do with this code correctly, I would consider binding to an ArrayController that handles presenting the correct set of issues based on the selected label instead of the approach you are taking.

Categories

Resources