I'm working on a backbone application that cascades views. There's one root view which creates its child views inside its initialize method and call child view rendering inside its own render. It may look like the following:
initialize: function(options) {
console.log('body');
this.template = tpl({});
this.headerView = new HeaderView(options);
this.chartView = new ChartView(options);
this.footerView = new FooterView(options);
},
render: function() {
console.log("body");
this.$el.html(this.template);
this.headerView.setElement(this.$el.find('.header')).render();
this.chartView.setElement(this.$el.find('.chart')).render();
this.footerView.setElement(this.$el.find('.footer')).render();
return this;
}
All child views go the same way - they render themselves inside their render method and call render on their children.
My question is: why does entire page display after the very last view has finished rendering? My page is pretty complicated and it takes 4 seconds to load and I've got a blank page in the meantime. I've got lots of console outputs where I clearly see, that some of the views have already rendered.
I don't understand why such big amounts of HTML are not displayed on the fly. And I don't want it to be like that.
It's because your root view is not yet attached to the dom.
You instantiate the root view, that will created an own el element that is not attached anywhere in the dom (this is legal). You then start rendering child views, that get appended to the root view... but still the root element is missing, so nothing gets displayed on screen. If these views are super complex, this process will take some time. When everything is done, you return from your root render function and the root el is finally attached to the dom, displaying everything.
To solve this, you can have the rendering of the subview happening after the root el has been attached to the dom (then you will see each piece incrementally). I am not saying that this approach is better, just that is a way to solve this performance issue.
One of the approaches could look something like:
RootView = Backbone.View.extend({
// your initialize is here
render: function() {
this.$el.html(this.template);
return this;
},
createHeaderView: function() {
this.$el.find('.header').append(this.headerView.render().el);
}
});
var root = new RootView()
$('body').append(root.render().el)
root.createHeaderView()
....
Here you notice that tha root el is attached to the dom before the subviews are rendered. This will make them appear incrementally.
Related
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.
giving a parent and a child view, I'd like 2 things:
the child view should render itself on instantiation
the child's render method should know the current parent's dom element
In practice, from the parent view, instead of this:
,add_bannerbox_view:function(model, collection, options){
var bannerbox = new BannerBoxView({ model: model });
this.bannerbox_views[model.cid] = bannerbox;
this.bannerbox_container.append(bannerbox.el);
bannerbox.render();
}
I'd like simply this;
,add_bannerbox_view:function(model, collection, options){
//here, BannerBoxView is supposed to render itself from initialize()
this.bannerbox_views[model.cid] = new BannerBoxView({ model: model, parent:this.el });
}
But I was wondering: is passing a parent's elem to the child a good practice? Or does it have some bad drawback?
Loose coupling is almost always preferable to tight coupling. The two best reasons I can think of are:
Reusable. Can be used by anywhere in your app without worrying about dependencies.
Testable. Can be tested independent of other components.
By requiring the child view to have a reference to the parent view, you are promoting tight coupling i.e. the child view becomes dependent on the parent view. This makes reusability extremely difficult, and if you're writing unit tests, you're going to have to instantiate or mock a parent class just so you can test the child. This is unnecessary and tedious.
If really what you're trying to do is have the child view automatically render, just extend the core Backbone.View and include a helper function that your parent views can call.
var MyView = Backbone.View.extend({
renderChild: function(view, options) {
var childView = new view(options);
this.views[options.model.cid] = childView;
this.$el.append(childView.el);
childView.render();
}
});
Then, you can define your parent views like so:
var ParentView = MyView.extend({
add_bannerbox_view: function() {
this.renderChild(BannerBoxView, {model: model});
}
});
The helper function we made will let you instantiate, append and render your child views with a single line of code.
I partially answer to myself. More than circular references (I'm passing only a dom element), drawbacks could arise for the self-appending functionality I'd like to use in child's render() method. The reason is possible memory leaks when having large number of views. There is a good explanation here:
http://ozkatz.github.io/avoiding-common-backbonejs-pitfalls.html
I should use var container = document.createDocumentFragment() in the parent view and then maybe pass container to the child view.
Also, following discussions above, and still not fully convinced of the various points (mine first :P) I'm using sort of bridge code. For now, I like doing this: I don't pass parent's dom element as a constructor argument. Instead, I pass it directly to the child's render(). The code is cleaned out to the bare bones:
//parent
var CustomBannersView = Backbone.View.extend({
initialize:function(){
this.groups_container = $('.groups-container');
this.group_views = {};
this.init();
this.set_events();
}
,init:function(){
//instantiate views without rendering for later use
this.collection.each(function(model){
this.group_views[model.cid] = new GroupView({ model:model, id:'group-' + model.cid });
},this);
}
,render:function(){
var temp_box = document.createDocumentFragment();
//render views without dom refresh. Passing the box.
_.each(this.group_views, function(groupview){ groupview.render(temp_box); });
//add container
this.groups_container.append(temp_box);
}
//dom events ----
,events:{
'click .create-gcontainer-button': function(){
this.collection.add(new Group());
}
}
,set_events:function(){
this.listenTo(this.collection,'add',function(model, collection, options){
//render a single subview, passing the main container
//no refresh problem here since it's a single view
this.group_views[model.cid] = new GroupView({ model: model, id:'group-' + model.cid }).render(this.groups_container);
});
}
});//end view
//child
var GroupView = Backbone.View.extend({
tagName: 'fieldset'
,className: 'group'
,initialize:function(){
this.template = Handlebars.compile($('#group-container').html());
}
,render:function(box){//box passed by parent
this.$el.html(this.template(this.model.toJSON()));
$(box).append(this.$el);
//now I can set things based on dom parent, if needed
return this;
}
});
This is an architectural question about Backbone JS:
AppView that contains a DOM element placeholder that is loaded with LeadsView.
LeadsView contains a DOM placeholder to present LeadView
My current route is #app/leads/1 which means that all 3 Views are loaded. AppView->LeadsView-> LeadView of lead #1.
Now suddenly the user hit the refresh button of the browser. The router would try to take it to #app/leads/:1 which routes to "app/leads/:lead_id" : "showLeadView", but AppView and LeadsView has not been rendered this time, hence the rendering of LeadView will fail.
It's looking for the DOM element to render itself into, but cannot find it.
How is that handled with Backbone?
TIA,
Nimrod.
You can programmatically initialize and render the other views when the "app/leads/:lead_id" : "showLeadView" route is hit.
The problem now becomes: How to find if a view was rendered or not ?
I solved this by adding a new property to my Models and Collections that would tell me if they were populated (either if they were fetched or if they have attributes set). An implementation for this can be found in the Thorax library repo.
And it looks like this:
isPopulated: function() {
/*jshint -W089 */
// We are populated if we have attributes set
var attributes = _.clone(this.attributes),
defaults = getValue(this, 'defaults') || {};
for (var default_key in defaults) {
if (attributes[default_key] != defaults[default_key]) {
return true;
}
delete attributes[default_key];
}
var keys = _.keys(attributes);
return keys.length > 1 || (keys.length === 1 && keys[0] !== this.idAttribute);
}
With this method i can verify if the collection needed to render the parent views has been populated. If it was't than call the methods that would be called if you navigated to app and then to leads (you practically access the routes programmatically).
It could look something like this:
if(!appModel.isPopulated())
this.showAppView();
if(!leadsCollection.isPopulated())
this.showLeadsView();
As you can see this means keeping references to appModel and leadsCollection in your router.
Update (clarification)
In the first line of my answer I was mentioning something like this:
AppView has a reference to a sub-view LeadsView that has a reference to a sub-view LeadView.
in your router you have an instance of AppView.
you pass appModel to AppView and call .render() on it when you hit app router.
you pass leadsCollection to AppView.getLeadsView() and call .render() on it when you hit leads route.
you pass the right lead model from leadsCollection to AppView.getLeadsView().getLeadView() and call .render() on it when you hit the route.
Or you can use something like Thorax
It already adds handling for subviews (takes care of event delegation, event destruction, DOM cleaning, etc.) and offers template handlers for it so you can point in your template where you want the sub-view to be rendered.
I hope now, it is clearer what I meant and as you can see it answers your question.
This is a new question but runs on from the last one, Dynamically load my 'IDs' into my Backbone Collection?
I now have all my models getting the data from my database. Now I want to be able to load this data into a view. But for some reason I can not get any views to work at all, this is my current view code,
var MyView= Backbone.View.extend({
el: '.page',
render: function() {
this.$el.html('CONTENT HERE FROM BACKBONE');
return this;
}
});
var testView = new MyView({});
$(document).ready(function(){
$('.page').append(testView);
})
Now '.page' is a div tag with that class set up on my page. But will not output my test text above, so what am I doing wrong?
The main aim is to have this view load the data form the model, which is all working fine, but right now I can not even get this simple test to work?
Have I forgotten to do with starting up the view functions with backbone?
All help most welcome.
Glenn.
you can initialize View inside document ready, View automatically call his render function when in initialize state, but in your case since document dose not load completely in initialize state, it fail to render
var testView;
$(document).ready(function(){
testView = new MyView
});
testView is an instance of MyView. You should not be calling append with testView. You need to invoke render method on testView instance. Since MyView el element is .page, content will be append to .page.
Use this:
$(document).ready(function(){
testView.render();
})
I have three different ways to initialize and render a view and its subviews, and each one of them has different problems. I'm curious to know if there is a better way that solves all of the problems:
Scenario One:
Initialize the children in the parent's initialize function. This way, not everything gets stuck in render so that there is less blocking on rendering.
initialize : function () {
//parent init stuff
this.child = new Child();
},
render : function () {
this.$el.html(this.template());
this.child.render().appendTo(this.$('.container-placeholder');
}
The problems:
The biggest problem is that calling render on the parent for a second time will remove all of the childs event bindings. (This is because of how jQuery's $.html() works.) This could be mitigated by calling this.child.delegateEvents().render().appendTo(this.$el); instead, but then the first, and the most often case, you're doing more work unnecessarily.
By appending the children, you force the render function to have knowledge of the parents DOM structure so that you get the ordering you want. Which means changing a template might require updating a view's render function.
Scenario Two:
Initialize the children in the parent's initialize() still, but instead of appending, use setElement().delegateEvents() to set the child to an element in the parents template.
initialize : function () {
//parent init stuff
this.child = new Child();
},
render : function () {
this.$el.html(this.template());
this.child.setElement(this.$('.placeholder-element')).delegateEvents().render();
}
Problems:
This makes the delegateEvents() necessary now, which is a slight negative over it only being necessary on subsequent calls in the first scenario.
Scenario Three:
Initialize the children in the parent's render() method instead.
initialize : function () {
//parent init stuff
},
render : function () {
this.$el.html(this.template());
this.child = new Child();
this.child.appendTo($.('.container-placeholder').render();
}
Problems:
This means that the render function now has to be tied down with all of the initialization logic as well.
If I edit the state of one of the child views, and then call render on the parent, a completely new child will be made and all of its current state will be lost. Which also seems like it could get dicey for memory leaks.
Really curious to get your guys' take on this. Which scenario would you use? or is there a fourth magical one that solves all of these problems?
Have you ever kept track of a rendered state for a View? Say a renderedBefore flag? Seems really janky.
This is a great question. Backbone is great because of the lack of assumptions it makes, but it does mean you have to (decide how to) implement things like this yourself. After looking through my own stuff, I find that I (kind of) use a mix of scenario 1 and scenario 2. I don't think a 4th magical scenario exists because, simply enough, everything you do in scenario 1 & 2 must be done.
I think it'd be easiest to explain how I like to handle it with an example. Say I have this simple page broken into the specified views:
Say the HTML is, after being rendered, something like this:
<div id="parent">
<div id="name">Person: Kevin Peel</div>
<div id="info">
First name: <span class="first_name">Kevin</span><br />
Last name: <span class="last_name">Peel</span><br />
</div>
<div>Phone Numbers:</div>
<div id="phone_numbers">
<div>#1: 123-456-7890</div>
<div>#2: 456-789-0123</div>
</div>
</div>
Hopefully it's pretty obvious how the HTML matches up with the diagram.
The ParentView holds 2 child views, InfoView and PhoneListView as well as a few extra divs, one of which, #name, needs to be set at some point. PhoneListView holds child views of its own, an array of PhoneView entries.
So on to your actual question. I handle initialization and rendering differently based on the view type. I break my views into two types, Parent views and Child views.
The difference between them is simple, Parent views hold child views while Child views do not. So in my example, ParentView and PhoneListView are Parent views, while InfoView and the PhoneView entries are Child views.
Like I mentioned before, the biggest difference between these two categories is when they're allowed to render. In a perfect world, I want Parent views to only ever render once. It is up to their child views to handle any re-rendering when the model(s) change. Child views, on the other hand, I allow to re-render anytime they need since they don't have any other views relying upon them.
In a little more detail, for Parent views I like my initialize functions to do a few things:
Initialize my own view
Render my own view
Create and initialize any child views.
Assign each child view an element within my view (e.g. the InfoView would be assigned #info).
Step 1 is pretty self explanatory.
Step 2, the rendering, is done so that any elements the child views rely on already exist before I try to assign them. By doing this, I know all child events will be correctly set, and I can re-render their blocks as many times as I want without worrying about having to re-delegate anything. I do not actually render any child views here, I allow them to do that within their own initialization.
Steps 3 and 4 are actually handled at the same time as I pass el in while creating the child view. I like to pass an element in here as I feel the parent should determine where in its own view the child is allowed to put its content.
For rendering, I try to keep it pretty simple for Parent views. I want the render function to do nothing more than render the parent view. No event delegation, no rendering of child views, nothing. Just a simple render.
Sometimes this doesn't always work though. For instance in my example above, the #name element will need to be updated any time the name within the model changes. However, this block is part of the ParentView template and not handled by a dedicated Child view, so I work around that. I will create some sort of subRender function that only replaces the content of the #name element, and not have to trash the whole #parent element. This may seem like a hack, but I've really found it works better than having to worry about re-rendering the whole DOM and reattaching elements and such. If I really wanted to make it clean, I'd create a new Child view (similar to the InfoView) that would handle the #name block.
Now for Child views, the initialization is pretty similar to Parent views, just without the creation of any further Child views. So:
Initialize my view
Setup binds listening for any changes to the model I care about
Render my view
Child view rendering is also very simple, just render and set the content of my el. Again, no messing with delegation or anything like that.
Here is some example code of what my ParentView may look like:
var ParentView = Backbone.View.extend({
el: "#parent",
initialize: function() {
// Step 1, (init) I want to know anytime the name changes
this.model.bind("change:first_name", this.subRender, this);
this.model.bind("change:last_name", this.subRender, this);
// Step 2, render my own view
this.render();
// Step 3/4, create the children and assign elements
this.infoView = new InfoView({el: "#info", model: this.model});
this.phoneListView = new PhoneListView({el: "#phone_numbers", model: this.model});
},
render: function() {
// Render my template
this.$el.html(this.template());
// Render the name
this.subRender();
},
subRender: function() {
// Set our name block and only our name block
$("#name").html("Person: " + this.model.first_name + " " + this.model.last_name);
}
});
You can see my implementation of subRender here. By having changes bound to subRender instead of render, I don't have to worry about blasting away and rebuilding the whole block.
Here's example code for the InfoView block:
var InfoView = Backbone.View.extend({
initialize: function() {
// I want to re-render on changes
this.model.bind("change", this.render, this);
// Render
this.render();
},
render: function() {
// Just render my template
this.$el.html(this.template());
}
});
The binds are the important part here. By binding to my model, I never have to worry about manually calling render myself. If the model changes, this block will re-render itself without affecting any other views.
The PhoneListView will be similar to the ParentView, you'll just need a little more logic in both your initialization and render functions to handle collections. How you handle the collection is really up to you, but you'll at least need to be listening to the collection events and deciding how you want to render (append/remove, or just re-render the whole block). I personally like to append new views and remove old ones, not re-render the whole view.
The PhoneView will be almost identical to the InfoView, only listening to the model changes it cares about.
Hopefully this has helped a little, please let me know if anything is confusing or not detailed enough.
I'm not sure if this directly answers your question, but I think it's relevant:
http://lostechies.com/derickbailey/2011/10/11/backbone-js-getting-the-model-for-a-clicked-element/
The context in which I set up this article is different, of course, but I think the two solutions I offer, along with the pros and cons of each, should get you moving in the right direction.
To me it does not seem like the worst idea in the world to differentiate between the intital setup and subsequent setups of your views via some sort of flag. To make this clean and easy the flag should be added to your very own View which should extend the Backbone (Base) View.
Same as Derick I am not completely sure if this directly answers your question but I think it might be at least worth mentioning in this context.
Also see: Use of an Eventbus in Backbone
Kevin Peel gives a great answer - here's my tl;dr version:
initialize : function () {
//parent init stuff
this.render(); //ANSWER: RENDER THE PARENT BEFORE INITIALIZING THE CHILD!!
this.child = new Child();
},
I'm trying to avoid coupling between views like these. There are two ways I usually do:
Use a router
Basically, you let your router function initialize parent and child view. So the view has no knowledge of each other, but the router handles it all.
Passing the same el to both views
this.parent = new Parent({el: $('.container-placeholder')});
this.child = new Child({el: $('.container-placeholder')});
Both have knowledge of the same DOM, and you can order them anyway you want.
What I do is giving each children an identity (which Backbone has already done that for you: cid)
When Container does the rendering, using the 'cid' and 'tagName' generate a placeholder for every child, so in template the children has no idea about where it will be put by the Container.
<tagName id='cid'></tagName>
than you can using
Container.render()
Child.render();
this.$('#'+cid).replaceWith(child.$el);
// the rapalceWith in jquery will detach the element
// from the dom first, so we need re-delegateEvents here
child.delegateEvents();
no specified placeholder is needed, and Container only generate the placeholder rather than the children's DOM structure. Cotainer and Children are still generating own DOM elements and only once.
Here is a light weight mixin for creating and rendering subviews, which I think addresses all the issues in this thread:
https://github.com/rotundasoftware/backbone.subviews
The approach taken by this plug is create and render subviews after the first time the parent view is rendered. Then, on subsequent renders of the parent view, $.detach the subview elements, re-render the parent, then insert the subview elements in the appropriate places and re-render them. This way subviews objects are reused on subsequent renders, and there is no need to re-delegate events.
Note that the case of a collection view (where each model in the collection is represented with one subview) is quite different and merits its own discussion / solution I think. Best general solution I am aware of to that case is the CollectionView in Marionette.
EDIT: For the collection view case, you may also want to check out this more UI focused implementation, if you need selection of models based on clicks and / or dragging and dropping for reordering.