Created by Tim Branyen @tbranyen with contributions
Provides a logical structure for assembling layouts with Backbone Views. Designed to be adaptive and configurable for painless integration.
Depends on Underscore, Backbone and jQuery. You can swap out the jQuery dependency completely with a custom configuration.
- Initial Screencast
- Example Application: GitHub Viewer & Source
- Integrating with Backbone Boilerplate with Handlebars
Development is fully commented source, Production is minified and stripped of all comments except for license/credits.
Include in your application after jQuery, Underscore, and Backbone have been included.
<scriptsrc="/js/jquery.js"></script><scriptsrc="/js/underscore.js"></script><scriptsrc="/js/backbone.js"></script><scriptsrc="/js/backbone.layoutmanager.js"></script>This example renders a View into a template which is injected into a layout.
These example templates are defined using a common pattern which leverages how
browsers ignore <script></script> contents when using a custom type
attribute.
Note: This is how LayoutManager expects templates to be defined by default (using script tags).
<script id="main-layout" type="layout">
<section class="content"></section>
<!-- Login template below will be injected here -->
<aside class="secondary"></aside>
</script>
<script id="login-template" type="template">
<form class="login">
<p><label for="user">Username</label><input type="text" name="user"></p>
<p><label for="pass">Password</label><input type="text" name="pass"></p>
<p><input class="loginBtn" type="submit" value="Login"></p>
</form>
</script>
Each View can associate a template via the template property. This name by
default is a jQuery selector, but if you have a custom configuration this could
potentially be a filename or JST function name.
Note: If you do not specify a template LayoutManager will assume the View's render method knows what it's doing and won't attempt to fetch/render anything for you.
varLoginView=Backbone.View.extend({template: "#login-template",// The render function will be called internally by LayoutManager.render: function(manage){// Have LayoutManager manage this View and call render.returnmanage(this).render();}});If you are using the default render method above, you can simply omit it and
LayoutManager will add it for you.
varLoginView=Backbone.View.extend({template: "#login-template"});Each LayoutManager can associate a template via the template property. This
name by default is a jQuery selector, but if you have a custom configuration
this could potentially be a filename or JST function name.
Note: Nesting LayoutManagers is not supported. If you want sub layouts, simply read on about nesting Views.
This code typically resides in a route callback.
varmain=newBackbone.LayoutManager({template: "#main-layout",// In the secondary column, put a new Login View.views: {".secondary": newLoginView()}});// Attach the Layout to the <body></body>.$("body").html(main.el);// Render the Layout.main.render();Views may also be alternatively defined outside the LayoutManager:
Use the following function to change out views at a later time. Remember to
call the View's render method after swapping out to have it displayed. The
setView return value is the view, so chaining a render is super simple.
main.setView(".header",newHeaderView());main.setView(".footer",newFooterView());// Chain a render methodmain.setView(".header",newHeaderView2()).render();Note: setView and setViews methods are available on all views. This
allows for nested Views, explained below.
Note: The first argument selector can be omitted completely if you would
like the nested View to exist directly on the element. This works well when
your parent View is something like a UL and your nested View is an LI.
Note: The third argument is optional, but when set to true it will
automatically append the View into the container instead of replacing the
content.
This is identical to how views are being assigned in the Layout example. It can be used in the following way:
varmain=newBackbone.LayoutManager({template: "#some-layout"});// Set the views outside of the layoutmain.setViews({".partial": newPartialView()});You may have a situation where a View is defined that encapsulates other nested Views. In these cases you should use nested views inside your LayoutManager View assignments.
Check out this example to see how easy this is:
varmain=newBackbone.LayoutManager({template: "#some-layout",views: {".partial": newPartialView({views: {".inner": newInnerView()}})}});Note: You can nest Views infinitely.
Instead of re-rendering the entire layout after data in a single View changes,
you can simply call render() on the View and it will automatically update
the DOM. You cannot bind to the initial render reference, like so:
Assume that you have a model that when changed, causes a redraw.
varMyView=Backbone.View.extend({initialize: function(){this.model.on("change",this.render,this);}});You must use this syntax instead:
varMyView=Backbone.View.extend({initialize: function(){this.model.on("change",function(){this.render();},this);}});Note: The reason for this, is that LayoutManager will automatically wrap your render function internally and provide you with a much more convenient function to re-render.
There are many times in which you will end up with a list of nested views
that result from either iterating a Backbone.Collection or Array
and will need to dynamically add these nested views into a main view.
LayoutManager solves this by exposing a method to change the insert mode
from replacing the innerHTML to appendChild instead. Whenever you
use the insertView method you will put the nested view into this special
mode.
Sub views are always inserted in order, regardless if the fetch method has
been overwritten to be asynchronous.
An example will illustrate the pattern easier:
This item template doesn't need to do much since it will be automatically
wrapped in an <li></li> by the View.
<script id="#item" type="template">
<%= name %>
</script>
// You may find it easier to have Backbone render the LI/TD/etc element// instead of including this in your template. This is purely convention// use what works for you.varItemView=Backbone.View.extend({template: "#item",// In this case we'll say the item is an <LI>tagName: "li"});The list View simply needs to provide an outlet for the above <li> to be
appended into.
// You will need to override the `render` function with custom functionality. varListView=Backbone.View.extend({tagName: "ul"render: function(manage){// Iterate over the passed collection and create a view for each item.this.collection.each(function(model){// Pass the sample data to the new SomeItem View.this.insertView(newItemView({serialize: {name: "Just testing!"}}));},this);// You still must return this view to render, works identical to// existing functionality.returnmanage(this).render();}});The insertView function as seen above is simply a shortcut to the setView
function, but automatically adds true to the append argument.
If you decide to add the selector partial to insertView, LayoutManager
will insert into the specified element.
For instance if you had a <UL> in your View and you wanted to insert into
that:
varListView=Backbone.View.extend({render: function(manage){// Append a new ItemView into the nested <UL>this.insertView("ul",newItemView());returnmanage(this).render();}});If your View is a <UL> then you can simply do the following:
varListView=Backbone.View.extend({// Ensure this View is a UL and not a DIVtagName: "ul",render: function(manage){// Append a new ItemView to the View.elthis.insertView(newItemView());returnmanage(this).render();}});If you wish to change append to prepend you can easily change how the
View is inserted by setting a new append function.
varListView=Backbone.View.extend({render: function(manage){// Append a new ItemView into the nested <UL>.this.insertView("ul",newItemView({// Prepend the element instead of append.append: function(root,child){$(root).prepend(child);}}));returnmanage(this).render();}});Note insertView can be used outside of render and is especially useful
for when you have an add event off a Collection.
The insertViews function is identical to insertView except that you can
bulk append like with setViews. You can one or more items by using an array.
varListView=Backbone.View.extend({render: function(manage){// Append a new ItemView into the nested <UL>.this.insertViews({// Append a single item."div": newItemView(),// Append multiple items."ul": [newItemView(),newItemView()]});returnmanage(this).render();}});The getView function allows you to find a specific View by some filter
function.
// Find an remove a View if it has the exact Model.main.getView(function(view){returnview.model===someModel;}).remove();This function works very similar to the getView function, except that it will
always return an Array of matching View's. If you omit the filter function,
it will return all subViews flattened.
// Find all sub views flattened in an Array.main.getViews()// Find all sub views that have a model.main.getViews(function(view){returnview.model;});The removeView function is available on all View's and should be called in
place of remove when applicable. It's mainly used internally, but if you
find that you need to scrub and remove a View and all its subView's you can use
this.
// Will remove the View and all subViews.main.removeView();// Will remove only append-mode (lists) items.main.removeView(true);Template engines bind data to a template. The term context refers to the data object passed.
LayoutManager will look for a serialize method or object automatically:
varLoginView=Backbone.View.extend({template: "#login-template",// Provide data to the templateserialize: function(){returnthis.model.toJSON();}});You can also pass the context object inside the render method:
varLoginView=Backbone.View.extend({template: "#login-template",render: function(manage){// Have LayoutManager manage this View and call render with data you// provide.returnmanage(this).render(this.model.toJSON());}});Once you've mastered the above features, you will want to learn more about how these methods actually work and how to integrate 3rd party plugins like jQuery into your Views.
The render function is overwritten on every LayoutManager and
Backbone.View instance. The overwritten render saves a reference to the
custom function you provide and will call this internally whenever you invoke
view.render().
varMyView=Backbone.View.extend({// This function gets wrapped by LayoutManager internally so you don't have// to pass any arguments to re-render.render: function(manage){returnmanage(this).render();}});Every render function accepts an optional callback function that will return
the View element once it has rendered itself and all of its children. The
render function returns a promise object that can be chained off of as
well.
// Using the callback methodnewMyView().render(function(el){// Use the DOMNode el here});// Using the promise resolve methodnewMyView().render().then(function(el){// Use the DOMNode el here});Every Backbone.View managed by LayoutManager can provide a custom cleanup
function that will run whenever the View is overwritten or removed.
varMyView=Backbone.View.extend({// This is a custom cleanup method that will remove the model reset eventcleanup: function(){this.model.unbind("change");},initialize: function(){this.model.on("change",function(){this.render();},this);}});Note: Be careful with unbinding, you don't want to inadvertently remove events from this model in other parts of your code. These are shared objects.
Attaching jQuery plugins should happen inside the render methods. You can
attach at either the Layout render or the View render. To attach in the
layout render:
$(".container").html(main.el);main.render(function(){// Elements are guarenteed to be in the DOMthis.$(".some-element").somePlugin();});To attach in the View render, you will need to override the render method
like so:
render: function(manage){returnmanage(this).render().then(function(){this.$(".some-element").somePlugin();});}This is a very cool example of the power in using deferreds. =)
Overriding LayoutManager options has been designed to work just like
Backbone.sync. You can override at a global level using
LayoutManager.configure or you can specify when instantiating a
LayoutManager instance.
Lets say you wanted to use Handlebars for templating in all your Views.
Backbone.LayoutManager.configure({// Override render to use Handlebarsrender: function(template,context){returnHandlebars.compile(template)(context);}});In this specific layout, define custom prefixed paths for template paths.
varmain=newBackbone.LayoutManager({template: "#main",// Custom paths for this layoutpaths: {template: "/assets/templates/"}});- Paths:
An empty object. Two valid property names:
templateandlayout.
paths: {}- Deferred: Uses jQuery deferreds for internal operation, this may be overridden to use a different Promises/A compliant deferred.
deferred: function(){return$.Deferred();}- Fetch:
Uses jQuery to find a selector and returns its
innerHTMLcontent as a string or template function (either works).
fetch: function(path){return_.template($(path).html());}- Partial: Uses jQuery to find the View's location and inserts the rendered element there. The append property determines if the View should append, defaults to replace via innerHTML.
partial: function(root,name,el,append){// If no selector is specified, assume the parent should be added to.var$root=name ? $(root).find(name) : $(root);// If no root found, return falseif(!$root.length){returnfalse;}// Use the append method if append argument is true.this[append ? "append" : "html"]($root,el);// If successfully added, return truereturntrue;}- HTML: Override this with a custom HTML method, passed a root element and an element to replace the innerHTML with.
html: function(root,el){$(root).html(el);}- Append: Very similar to HTML except this one will appendChild.
append: function(root,el){$(root).append(el);}- Detach: Remove an element from the DOM, but maintain events.
detach: function(el){$(el).detach();}- When: This function will trigger callbacks based on the success/failure of one or more deferred objects.
when: function(promises){return$.when.apply(null,promises);}- Render:
Renders a template with the
FunctionorStringprovided as the template variable.
render: function(template,context){returntemplate(context);}The fetch method is overridden to get the contents of layouts and templates.
If you can instantly get the contents (DOM/JST) you can return the
contents inside the function.
Backbone.LayoutManager.configure({fetch: function(name){return$("script#"+name).html();}});If you need to fetch the contents asynchronously, you will need to put the
method into "asynchronous mode". To do this, assign this.async()
to a variable and call that variable with the contents when you are done.
Backbone.LayoutManager.configure({fetch: function(name){vardone=this.async();$.get(name,function(contents){done(contents);});}});You may need to combine a mix of Engines and Transports to integrate.
Custom templating engines can be used by overriding render.
No configuration necessary for this engine.
Backbone.LayoutManager.configure({render: function(template,context){returnMustache.to_html(template,context);}});Backbone.LayoutManager.configure({fetch: function(path){returnHandlebars.compile($(path).html());},render: function(template,context){returntemplate(context);}});You can swap out how templates are loaded by overriding fetch.
No configuration necessary for this transport.
Backbone.LayoutManager.configure({fetch: function(path){vardone=this.async();$.get(path,function(contents){done(contents);});}});Whatever you decide to return as a template in fetch, can be used in the
render method.
Backbone.LayoutManager.configure({fetch: function(name){returnwindow.JST[name];},render: function(template,context){returntemplate(context);}});The traditional way of inserting a Layout into the DOM was by way of:
// Create the Layoutvarmain=newBackbone.LayoutManager(...);// Render the Layoutmain.render(function(el){// Attach Layout to the DOM$(".some-selector").html(el);});The new supported way of inserting into the DOM is:
// Create the Layout.varmain=newBackbone.LayoutManager(...);// Attach Layout to the DOM.$(".some-selector").html(main.el);// Render the Layout.main.render();- Patched massive memory leak and missing remove on setView
- Tons of new unit tests
- More API normalization
- Collection rendering bug fixes
- New View methods
- insertView & insertViews
- getView and getViews