Backbone.js: Should .render() and .remove() be able to reverse each other? - javascript

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()

Related

In Backbone, how do I have an after_render() on all views?

I am maintaining a javascript application and I would like there to be a jquery function invoked on pretty much every view. It would go something like this:
SomeView = Backbone.Marionette.ItemView.extend
initialize: ->
#on( 'render', #after_render )
after_render: ->
this.$el.fadeOut().fadeIn()
Clearly there is a better way to do this than have an after_render() in each view? What is the better way to do it? If you can give an answer that includes jasmine tests, I'll <3 you ;)
The event you are looking for is onDomRefresh. See here for the documentation:
https://github.com/marionettejs/backbone.marionette/blob/master/docs/marionette.view.md#view-domrefresh--ondomrefresh-event
Create your own base view class and put your afterRender code in it. When you create a view, inherit from this class.
var MyApp.ItemView = Backbone.Marionette.ItemView.extend({
afterRender: function() {
// This will be called after rendering every inheriting view.
}
});
var SpecificItemView = MyApp.ItemView.extend({
// this view will automatically inherit the afterRender code.
});
In general, it seems to be considered good practice to define your own base views for all 3 view types. It will enable you to easily add global functionality later.
There is a common pattern used across all Backbone frameworks, normally they have a render method which in turn calls beforeRender, renderTemplate and afterRender methods.
render:function(){
this.beforeRender();
this.renderTemplate();// method names are just indicative
this.afterRender();
return this;
}
In your Base view you can have these methods to be empty functions, and implement them wherever you want it. Not sure this answer applies to Marionette
Combining thibaut's and Robert Levy's answer, the correct solution would be:
var baseView = Backbone.Marionette.ItemView.extend({
onDomRefresh: function() {
// This will be triggered after the view has been rendered, has been shown in the DOM via a Marionette.Region, and has been re-rendered
// if you want to manipulate the dom element of the view, access it via this.$el or this.$('#some-child-selector')
}
});
var SpecificItemView = baseView.extend({
// this view will automatically inherit the onDomRefresh code.
});

What's the correct way to determine if a Backbone.View's this.el is attached to the document or floating?

I am trying to use templating to render a view initially, but to update the view on subsequent calls when it is part of the document.
My app's page looks a little something like
<body>
<!-- ... -->
<div id="view_placeholder"/>
<!-- ... -->
</body>
And in pseudo-code I want to do something like so
Backbone.View.extend({
// ...
render: function() {
if (this.el *IS NOT A CHILD OF document*) {
// render the contents from the template
} else {
// update the content visibility based on the model
}
},
// ...
});
The reason for this is that the template contains quite a lot of nodes and regenerating it for every change is not practicable.
I have explored some of the data-binding libraries, e.g. rivets.js but they are a poor fit to the template:model relation.
One thing I noticed is that this.el.parentNode==null before I add it to the document, but I am not sure that this is a definitive test, and in any case if I wrap this view within another, then that test becomes less useful (or maybe I am being overly cautious as once within another view's el I have rendered my sub-template anyway.
Another option I can see is to use a field to track the rendered status, e.g.
Backbone.View.extend({
//
templateRendered:false,
// ...
render: function() {
if (!this.templateRendered) {
// render the contents from the template
this.templateRendered = true;
} else {
// update the content visibility based on the model
}
},
// ...
});
but that feels hacky to me.
So my question is:
What is the best way to track the fact that I have rendered the template fully and therefore only need to tweak the rendered template rather than re-render it (and re-insert all the sub-views)?
I think the idiomatic backbone approach is to only call a full render() on your view when you want a full render, and use model change event bindings to call sub-render functions that render smaller portions of the view.
var AddressView = Backbone.View.extend({
initialize: function (options) {
Backbone.view.prototype.initialize.call(this, options);
_.bindAll(this)
options.model.on('change:name', this.renderName);
options.model.on('change:street', this.renderStreet);
options.model.on('change:zipCode', this.renderZipCode);
},
renderName: function (model) {
this.$el.find("#name").text(model.get("name"));
},
renderZipCode: function (model) {
this.$el.find("#zipcode").text(model.get("zipCode"));
},
renderStreet: function (model) {
this.$el.find("#stree").text(model.get("street"));
},
render: function () {
//Populate this.el with initial template, subviews, etc
//assume this.template is a template function that can render the main HTML
this.$el.html(this.template(model));
this.renderName(this.model);
this.renderZipCode(this.model);
this.renderStreet(this.model);
return this;
}
});
Code as above is undoubtedly tedious. I would reconsider knockback.js or rivets.js, personally, but I believe the pattern above is the canonical vanilla backbone.js approach.
I would avoid rendering a view until its element is about to be inserted. In any case, you can find out by checking the parent of the view's element, i.e.
this.$el.parent()
should be empty if the this.$el is not part of your document.

attaching backbone.js views to existing elements vs. inserting el into the DOM

I am implementing my first actual non-tutorial Backbone app, and have 2-ish questions about an aspect of using using backbone.js that isn't settling very well with me, which relates to injecting a view's rendered el into the DOM vs. using an existing element for el. I suspect I will provide you all with a few "teachable moments" here, and appreciate the help.
Most Backbone View examples I see in the web specify tagName, id and/or className, when creating a View and thereby create an el that is unattached from the DOM. They typically look something like:
App.MyView = Backbone.View.extend({
tagName: 'li',
initialize: function () {
...
},
render: function () {
$(this.el).html(<render an html template>);
return this;
}
});
But the tutorials don't always get around to explaining how they recommend getting the rendered el into the DOM. I've seen it a few different ways. So, my first question is: where is the appropriate place to call a view's render method and insert its el into the DOM? (not neccessarily one and the same place). I've seen it done in a router, in the view's initialize or render functions, or just in a root level document ready function. ( $(function () ) . I can imagine that any of these work, but is there a right way to do it?
Second, I am starting with some HTML markup/wireframe, and converting html portions to js templates corresponding to backbone views. Rather than let the view render an unattached element and providing an anchor point in the html to stick it in, I feel like its more natural, when there is only going to be one element for a view and it won't be going away, to use an existing, emptied wrapper element (often a div or span) as the el itself. That way I don't have to worry about finding the place in the document to insert my unattached el, which would potentially end up looking like this (note the extra layering):
<div id="insert_the_el_in_here"> <!-- this is all that's in the original HTML doc -->
<div id="the_el"> <!-- i used to be a backbone generated, unattached el but have been rendered and inserted -->
<!-- we're finally getting to some useful stuff in here -->
</div>
</div>
So part of my second question is, for a basically static view, is there anything wrong with using an existing element from the page's HTML directly as my view's el? This way I know its already in the DOM, in the right place, and that calling render will immediately render the view on the page. I would acheive this by passing the already exixting element to my view's constsructor as 'el'. That way, it seems to me, i don't have to worry about sticking it into the DOM (making question 1 sort of moot), and calling render will immediately update the DOM. E.g.
<form>
<div someOtherStuff>
</div>
<span id="myView">
</span>
</form>
<script type="text/template" id = "myViewContents"> . . . </script>
<script type="application/javascript">
window.MyView = Backbone.View.extend( {
initialize: function () {
this.template = _.template($('#myViewContents').html());
this.render();
},
render: function () {
$(this.el).html(this.template());
return this;
}
});
$(function () {
window.myView = new MyView({ el: $('#myView').get(0) });
});
</script>
Is this an OK way to do it for static views on the page? i.e., there is only one of these views, and it will not go away in any circumstance. Or is there a better way? I realize that there may be different ways to do things (i.e., in a router, in a parent view, on page load, etc.) based on how I am using a view, but right now I am looking at the initial page load use case.
Thanks
There's absolutely nothing wrong with the idea of attaching a view to an existing DOM node.
You can even just put the el as a property on your view.
window.MyView = Backbone.View.extend( {
el: '#myView',
initialize: function () {
this.template = _.template($('#myViewContents').html());
this.render();
},
render: function () {
this.$el.html(this.template()); // this.$el is a jQuery wrapped el var
return this;
}
});
$(function () {
window.myView = new MyView();
});
What I recommend is, do what works... The beauty of Backbone is that it is flexible and will meet your needs.
As far as common patterns are concerned, generally I find myself having a main view to keep track of over views, then maybe a list view and individual item views.
Another common pattern as far as initialization is concerned is to have some sort of App object to manage stuff...
var App = (function ($, Backbone, global) {
var init = function () {
global.myView = new myView();
};
return {
init: init
};
}(jQuery, Backbone, window));
$(function () {
App.init();
});
Like I said before though, there's really no WRONG way of doing things, just do what works.
:)
Feel free to hit me up on twitter #jcreamer898 if you need any more help, also check out #derickbailey, he's kind of a BB guru.
Have fun!
You can also send an HTML DOM Element object into the view as the 'el' property of the options.
window.MyView = Backbone.View.extend( {
initialize: function () {
this.template = _.template($('#myViewContents').html());
this.render();
},
render: function () {
this.$el.html(this.template()); // this.$el is a jQuery wrapped el var
return this;
}
});
$(function () {
window.myView = new MyView({
el: document.getElementById('myView')
});
});
Use delegate events method:
initialize: function() {
this.delegateEvents();
}
To understand why: http://backbonejs.org/docs/backbone.html#section-138 near "Set callbacks, where"
Looks also like these days you can youse setElement.

Backbone JS: can one view trigger updates in other views?

In my simple project I have 2 views - a line item view (Brand) and App. I have attached function that allows selecting multiple items:
var BrandView = Backbone.View.extend({
...some code...
toggle_select: function() {
this.model.selected = !this.model.selected;
if(this.model.selected) $(this.el).addClass('selected');
else $(this.el).removeClass('selected');
return this;
}
});
var AppView = Backbone.View.extend({
...some code...
delete_selected: function() {
_.each(Brands.selected(), function(model){
model.delete_selected();
});
return false;
},
});
Thing is, I want to know how many items are selected. In this setup selecting is NOT affecting the model and thus not firing any events. And from MVC concept I understand that views should not be directly talking to other views. So how can AppView know that something is being selected in BrandViews?
And more specifically, I AppView to know how many items were selected, so if more than 1 is selected, I show a menu for multiple selection.
You might want to have a read of this discussion of Backbone pub/sub events:
http://lostechies.com/derickbailey/2011/07/19/references-routing-and-the-event-aggregator-coordinating-views-in-backbone-js/
I like to add it in as a global event mechanism:
Backbone.pubSub = _.extend({}, Backbone.Events);
Then in one view you can trigger an event:
Backbone.pubSub.trigger('my-event', payload);
And in another you can listen:
Backbone.pubSub.on('my-event', this.onMyEvent, this);
I use what Addy Osmani calls the mediator pattern http://addyosmani.com/largescalejavascript/#mediatorpattern. The whole article is well worth a read.
Basically it is an event manager that allows you to subscribe to and publish events. So your AppView would subscript to an event, i.e. 'selected'. Then the BrandView would publish the 'selected' event.
The reason I like this is it allows you to send events between views, without the views being directly bound together.
For Example
var mediator = new Mediator(); //LOOK AT THE LINK FOR IMPLEMENTATION
var BrandView = Backbone.View.extend({
toggle_select: function() {
...
mediator.publish('selected', any, data, you, want);
return this;
}
});
var AppView = Backbone.View.extend({
initialize: function() {
mediator.subscribe('selected', this.delete_selected)
},
delete_selected: function(any, data, you, want) {
... do something ...
},
});
This way your app view doesn't care if it is a BrandView or FooView that publishes the 'selected' event, only that the event occured. As a result, I find it a maintainable way to manage events between parts of you application, not just views.
If you read further about the 'Facade', you can create a nice permissions structure. This would allow you to say only an 'AppView' can subscribe to my 'selected' event. I find this helpful as it makes it very clear where the events are being used.
Ignoring the problems with this that you already mention in your post, you can bind and trigger events to/from the global Backbone.Event object, which will allow anything to talk to anything else. Definitely not the best solution, and if you have views chatting with one another then you should consider refactoring that. But there ya go! Hope this helps.
Here is my case with a similar need: Backbone listenTo seemed like a solution to redirect to login page for timed out or not authenticated requests.
I added event handler to my router and made it listen to the global event such as:
Backbone.Router.extend({
onNotAuthenticated:function(errMsg){
var redirectView = new LoginView();
redirectView.displayMessage(errMsg);
this.loadView(redirectView);
},
initialize:function(){
this.listenTo(Backbone,'auth:not-authenticated',this.onNotAuthenticated);
},
.....
});
and in my jquery ajax error handler:
$(document).ajaxError(
function(event, jqxhr, settings, thrownError){
.......
if(httpErrorHeaderValue==="some-value"){
Backbone.trigger("auth:not-authenticated",errMsg);
}
});
You can use Backbone object as the event bus.
This approach is somewhat cleaner but still relies on Global Backbone object though
var view1 = Backbone.View.extend({
_onEvent : function(){
Backbone.trigger('customEvent');
}
});
var view2 = Backbone.View.extend({
initialize : function(){
Backbone.on('customEvent', this._onCustomEvent, this);
},
_onCustomEvent : function(){
// react to document edit.
}
});
Use the same model objects. AppView could be initialized with a collection, and BrandView initialized with one model from that collection. When attributes of a branch object change, any other code that has a reference to that model can read it.
So lets so you have some brands that you fetch via a collection:
var brands = new Brands([]);
brands.fetch();
Now you make an AppView, and an array of BrandView's for each model.
var appView = new AppView({brands: brands});
var brandViews = brands.map(function(brand) {
return new BrandView({brand: brand});
});
The appView and the brandViews now both have access to the same model objects, so when you change one:
brands.get(0).selected = true;
Then it changes when accessed by the views that reference it as well.
console.log(appView.brands.get(0).selected); // true
console.log(brandViews[0].brand.selected) // true
Same as John has suggested above, the Mediator Pattern works really good in this scenario, as Addy Osmani summing this issue up again in Backbone fundamentals.
Wound up using the Backbone.Mediator plugin which is simple and great, and makes my AMD View modules working together seamlessly =)

Backbone refresh view events

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.

Categories