I have noticed there is a different, the way DOM been updated by Marionette comparing with Backbone. I have created two simple fiddles. One using Backbone and other one based on marionette. Both examples has a helper method call processDom and that method simply throw an error after iterating 50 times.
However in backbone example elements been appended to DOM till die() method fires. But in marionette based example DOM has not been updated at all. It would be great if someone can explain how this works. I wonder marionette internally using a virtual dom kind of a technique.
Render method in marionette example
render: function () {
var viewHtml = this.template({
'contentPlacement': 'Sydney'
});
this.$el.html(viewHtml);
this.processDom(this.$el);
this.postRender();
}
Render method in backbone example
render: function () {
var template = Handlebars.compile($('#sample-template').html());
var viewHtml = template({
'contentPlacement': 'Sydney'
});
this.$el.html(viewHtml);
this.processDom(this.$el);
this.postRender();
},
Links to fiddle examples
http://jsfiddle.net/samitha/v5L7c2t5/7/
http://jsfiddle.net/samitha/pc2cvmLs/7/
In general for post processing purposes you could use onRender of Marionette.ItemView. It's not a good practice to rewrite Marionette.ItemView.render.
Rendering of views inside the regions for Marionette is handled a bit different as in Backbone case.
When you rendering Backbone.View - your element will attach itself to the DOM's $('#search-container'), and in render method it will operate with already attached element so you can see the changes.
When you rendering Marionette.ItemView with Marionette.Region.show method, Marionette.Region already attached to the DOM and it need to render appropriate view (in your case ItemView) and only after that step it will attach it to the DOM and will set the ItemView as currentView.
You can see from source of Marionette.Region.show that it attaches view after render is called. In your case render will throw error and it will never be attached to the DOM.
To understand it deeper lets look at the 2 methods which are responsible for attaching/creating views in BackboneView. Marionette uses the same method for Marionette.ItemView.
_ensureElement: function() {
if (!this.el) { // case when el property not specified in view
var attrs = _.extend({}, _.result(this, 'attributes'));
if (this.id) attrs.id = _.result(this, 'id');
if (this.className) attrs['class'] = _.result(this, 'className');
// creating new jQuery element(tagNam is div by default) with attributes specified
var $el = Backbone.$('<' + _.result(this, 'tagName') + '>').attr(attrs);
this.setElement($el, false);
} else { // case when el property exists
this.setElement(_.result(this, 'el'), false);
}
}
And
setElement: function(element, delegate) {
if (this.$el) this.undelegateEvents();
// here is all magic
// if element is instance of Backbone.$, which means that el property not specified
// or it's already jquery element like in your example( el: $('#search_container')) with Backbone.
// this.$el will be just a jQuery element or already attached to the DOM jQuery element
// in other case it will try to attach it to the DOM.
this.$el = element instanceof Backbone.$ ? element : Backbone.$(element);
this.el = this.$el[0];
if (delegate !== false) this.delegateEvents();
return this;
},
As you can see from comments all magic is in a few lines of code, and that's why I love it.
Related
I've been trying to learn Backbone, and I'm developing an app now. But I have a problem with a view's events: App.views.ChannelView should have a click event, but it is not firing.
Here's the code:
http://pastebin.com/GgvVHvtj
Everything get rendered fine, but events won't fire. Setting the el property in the view will work, but I can't use it, and I've seen on Backbone's todo tutorial that it is possible.
How do I make events fire without a defined el property?
You must define the el element to be an existing element in your DOM. If you do not define it, fine, it will default to a div, but when you render the view, the html generated must be appended/prepended whatever, you get the point, to an existing DOM element.
Events are scoped to the view, so something's wrong with your scope. From the code you provided I can't reproduce the problem, so if you might, please provide a live example on jsfiddle/jsbin etc in order to fully understand the issue.
Demo ( in order to demonstrate the view render )
var App = {
collections: {},
models: {},
views: {},
};
App.models.Channel = Backbone.Model.extend({
defaults: {
name: '#jucaSaoBoizinhos'
}
});
App.views.ChannelView = Backbone.View.extend({
el:$('#PlaceHolder'),
events: {
"click .channel": "myhandler"
},
render: function() {
this.$el.html('<div class="channel"><button>' + this.model.get('name') + '</button></div>');
return this;
},
myhandler: function(e) {
alert(e);
console.log(this.model.get('name'));
},
});
var chView = new App.views.ChannelView({model: new App.models.Channel()});
//console.log(chView.render().el) //prints div#PlaceHolder
//without the el specified in the view it would print a div container
//but i would have to render the view into an existing DOM element
//like this
//$('#PlaceHolder').html(chView.render().el)
chView.render()
Can you try doing a
events: {
"all": "log"
}
log: function(e) {
console.log e;
}
That should log out every event that's getting fired. I find it super helpful when troubleshooting.
backbone view events can work without dom element specified. If you can't use any element at the view createion (initialization) moment, then you can use it's 'setElement' method, to attach your view to specified dom element. Here is description.
Be the way your view render method will not work also without specified 'el'.
I'm developing a webapplication with Resthub, so there is a backbone.js stack at the front-side. I need to call a method, everytime a new view (also all sorts of subviews) is rendered, to add some Twitter-Bootstrap specific stuff (help-popovers, kind of quick help, which get their options from a global json file, so the help-texts are easier to maintain).
As far as I know there isn't a backbone-built-in event which is fired every time a view is rendered.
So my question is: What is the easiest way to extend all views, so that they fire an event when the render method is (implicitly or explicitly) called. I want to extend all my views cause I don't want to trigger this event manually in all views I have, because it's error-prone and all developers has to remember that they've to trigger that event.
If you want to do something(fire an event or anything else) for all cases when the render method is called, the most straight forward way might be to update the render method in your copy of Backbone's source code (assuming you want the behavior across the project).
By default the render method just returns 'this'
render: function() {
return this;
},
If there is something you always want to do before render, you can add it within the render method
render: function() {
//add your extra code/call
return this;
},
Alternatively you can also override the prototype of Backbone.View function and update/create your own version(s) something like
_.extend(Backbone.View.prototype, Backbone.Events, {
render: function() {
console.log('This is a test');
return this;
}
});
var testView = Backbone.View.extend({
});
var testview = new testView();
testview.render(); //displays This is a test
//any view rendered will now have the console log
Taking this a step further, you can add your own version of render, calling it say 'myrender' and/or add your own event(s) say 'myevent' which can then be called before/after you call render/myrender
_.extend(Backbone.View.prototype, Backbone.Events, {
render: function() {
//console.log('This is a test');
this.mynewevent();
return this;
},
myrender: function() {
console.log('Pre-render work');
this.render();
},
mynewevent: function() {
console.log('New Event work');
}
});
var testView = Backbone.View.extend({
});
var testview = new testView();
//testview.render();
testview.myrender();
Underscore's extend is being used here and since Backbone has a dependency on Underscore, if you are using Backbone, Underscore should be available for you as well.
We are using Backbone views to enhance our app, and the architecture of our application dictates that we attach our views to existing DOM elements.
We recently encountered a situation in which it is necessary to detach a Backbone view from a DOM element without removing the DOM element itself. Thus, we cannot use View.remove() since it will in turn invoke this.$el.remove();
I had originally created the following method for detaching the view, but I am concerned that I may be causing a memory leak:
detach: function() {
this.stopListening();
this.unbind();
this.setElement(null);
}
This effectively replaces the View's element with empty jQuery object, and it unbinds all Backbone events. However, it occurred to me that jQuery might be storing a reference to the new empty object. I am not entirely clear on jQuery's internals so I can't comment on the effectiveness of this method.
I then modified the method as follows:
detach: function() {
this.stopListening();
this.unbind();
this.undelegateEvents();
this.el = null;
this.$el = null;
}
I think this better achieves the desired result as it removes all references to the DOM and to jQuery. I am not 100% confident in this approach, so I was wondering how others have handled this scenario.
Does this method seem sound? Is there a better way to achieve this?
Extend the View and override remove so that it no longer calls this.$el.remove();
Example
var EnhanceView = Backbone.View.extend({
remove: function() {
this.stopListening();
return this;
}
});
should be as simple as that.
Because Backbone.js is pretty flexible, I am wondering about the best approach for certain things. Here I'm wondering if I'm supposed to build my application's views so that '.render()' and '.remove()' correctly reverse each other.
At first, the way that seems cleanest is to pass the view a ID or jQuery element to attach to. If things are done this way though, calling '.render()' will not correctly replace the view in the DOM, since the main element is never put back in the DOM:
App.ChromeView = Backbone.View.extend({
render: function() {
// Instantiate some "sub" views to handle the responsibilities of
// their respective elements.
this.sidebar = new App.SidebarView({ el: this.$(".sidebar") });
this.menu = new App.NavigationView({ el: this.$("nav") });
}
});
$(function() {
App.chrome = new App.ChromeView({ el: $("#chrome") });
});
It seems preferable to me to set it up so that .remove() and .render() are exact opposites:
App.ChromeView = Backbone.View.extend({
render: function() {
this.$el.appendTo('body');
this.sidebar = new App.SidebarView({ el: this.$(".sidebar") });
this.menu = new App.NavigationView({ el: this.$("nav") });
}
});
$(function() {
App.chrome = new App.ChromeView();
});
What does the Backbone.js community say? Should .remove() and .render() be opposite sides of the same coin?
I prefer that render does NOT attach the view's element to the dom. I think this promotes loose coupling, high cohesion, view re-use, and facilitates unit testing. I leave attaching the rendered element to a container up to either the router or a main "layout" type container view.
The nice thing about remove is that it works without the view having knowledge of the parent element, and thus is still loosely coupled and reusable. I definitely don't like to put random DOM selectors from my layout HTML (#main or whatever) into my views. Definitely bad coupling there.
I will note that in certain annoying situations, some things like the chosen jQuery plugin require some code to run AFTER the element has been attached to the DOM. For these cases I usually implement a postAttach() callback in the view and try to keep the amount of code there as small as possible.
Yes, the in-house View.remove() is very agressive.
For the propose of re-create the View again using an external el I am used to rewrite it like this:
remove: function(){
this.$el.empty();
return this;
}
But I don't think the framework should implement magic behavior to avoid this external DOM elements deletion.
This framework behavior is aggressive, ok, but it is very cheap to customize it when needed as we see above.
What about this? If we just have .initialize and .render take a parentSelector property, we can do this and end up with a usage that is:
Loosely coupled
Reversable .remove()/.render()
Single method
instantiation & rendering for the calling method
eg:
// Bootstrap file
curl(['views/app']).then(
function(App){
app = new App('body');
});
// view/app.js
define([
'text!views/app.tmpl.html'
, 'link!views/app.css'
]
, function (template) {
var App
// Set up the Application View
App = Backbone.View.extend({
// Standard Backbone Methods
initialize: function (parentSel) {
console.log('App view initialized')
if (parentSel) { this.render(parentSel) }
}
, render: function (parentSel) {
if (parentSel) { this._sel = parentSel } // change the selector if render is called with one
if (!this._sel) { return this } // exit if no selector is set
this.$el.appendTo(this._sel)
this.$el.html(
this.compiledTemplate({ 'content':'test content' })
);
return this
}
// Custom Properties
, compiledTemplate: _.template(template)
})
return App
});
// External usage
// I can call .remove and .render all day long now:
app.remove()
app.render()
app.remove()
In my view I don't declare this.el because I create it dinamically, but in this way the events don't fire.
This is the code:
View 1:
App.Views_1 = Backbone.View.extend({
el: '#content',
initialize: function() {
_.bindAll(this, 'render', 'renderSingle');
},
render: function() {
this.model.each(this.renderSingle);
},
renderSingle: function(model) {
this.tmpView = new App.Views_2({model: model});
$(this.el).append( this.tmpView.render().el );
}
});
View 2:
App.Views_2 = Backbone.View.extend({
initialize: function() {
_.bindAll(this, 'render');
},
render: function() {
this.el = $('#template').tmpl(this.model.attributes); // jQuery template
return this;
},
events: {
'click .button' : 'test'
},
test: function() {
alert('Fire');
}
});
});
When I click on ".button" nothing happens.
Thanks;
At the end of your render() method, you can tell backbone to rebind events using delegateEvents(). If you don't pass in any arguments, it will use your events hash.
render: function() {
this.el = $('#template').tmpl(this.model.attributes); // jQuery template
this.delegateEvents();
return this;
}
As of Backbone.js v0.9.0 (Jan. 30, 2012), there is the setElement method to switching a views element and manages the event delegation.
render: function() {
this.setElement($('#template').tmpl(this.model.attributes));
return this;
}
Backbone.View setElement: http://backbonejs.org/#View-setElement
setElementview.setElement(element)
If you'd like to apply a Backbone view to a different DOM element, use
setElement, which will also create the cached $el reference and move
the view's delegated events from the old element to the new one.
Dynamically creating your views in this fashion has it's pros and cons, though:
Pros:
All of your application's HTML markup would be generated in templates, because the Views root elements are all replaced by the markup returned from the rendering. This is actually kind of nice... no more looking for HTML inside of JS.
Nice separation of concerns. Templates generate 100% of HTML markup. Views only display states of that markup and respond to various events.
Having render be responsible for the creation of the entire view (including it's root element) is in line with the way that ReactJS renders components, so this could be a beneficial step in the process of migrating from Backbone.Views to ReactJS components.
Cons: - these are mostly negligible
This wouldn't be a painless transition to make on an existing code base. Views would need to be updated and all templates would need to have the View's root elements included in the markup.
Templates used by multiple views could get a little hairy - Would the root element be identical in all use cases?
Prior to render being called, the view's root element is useless. Any modifications to it will be thrown away.
This would include parent views setting classes/data on child view elements prior to rendering. It is also bad practice to do this, but it happens -- those modifications will be lost once render overrides the element.
Unless you override the Backbone.View constructor, every view will unnecessarily delegate it's events and set attributes to a root element that is replaced during rendering.
If one of your templates resolves to a list of elements rather than a single parent element containing children, you're going have a bad time. setElement will grab the first element from that list and throw away the rest.
Here's an example of that problem, though: http://jsfiddle.net/CoryDanielson/Lj3r85ew/
This problem could be mitigated via a build task that parses the templates and ensures they resolve to a single element, or by overriding setElement and ensuring that the incoming element.length === 1.