viewmaster
v1.2.4
Published
Few tested opinions on how to handle deeply nested views in Backbone.js focusing on encapsulation and reusability.
Downloads
54
Readme
Backbone.Viewmaster
Few tested opinions on how to handle deeply nested views in Backbone.js focusing on modularity. This is implemented after writing several Backbone.js applications and by carefully picking the recurring patterns seen on them.
Backbone.Viewmaster is a single View extended from Backbone.View. Views extended from it can be infinitely nested with each others using the four nesting methods setView, appendView, prependView and insertView. There is no separate concept of layouts or list views. It's just a humble Backbone View Class with versatile nesting capabilities.
The main idea behind Backbone.Viewmaster is that views should be small and independent building blocks of application UI. Read the tutorial to see how it's encouraged.
Created by Esa-Matti Suuronen @EsaMatti.
Download
- backbone.viewmaster.js 11kb, with comments
- backbone.viewmaster.min.js 1kb, minified and gzipped
or use from npm with Browserify
npm install viewmaster
Dependencies
- Backbone.js 0.9.9 or later
API Docs
Tutorial
In this tutorial we build the classic TODO app and go through the most important features of Backbone.Viewmaster while discussing the ideas behind them.
Basics
Backbone.Viewmaster
is a View extended from Backbone.View
. You start by
extending your custom views from it. Lets define a layout for our app with some
container elements for our nested views.
<script type="template" id="layout">
<div class="header"></div>
<hr>
<h1>TODOs</h1>
<div class="addview-container"></div>
<div class="todo-container"></div>
<div>You have <%= count %> todos</div>
<hr>
<div class="footer"></div>
</script>
var TodoLayout = Backbone.Viewmaster.extend({
template: function(context){
return _.template($("#layout").html(), context);
},
context: function() {
return {
count: this.collection.size()
};
}
});
First thing you do for every Viewmaster view is set a template function for
it. It can be any function which takes a context object as the first argument
and returns neither a HTML string or a DOM object. The context object is by
default this.model.toJSON()
or an empty object if your view does not have a
model. You can customize this behavior by overriding the context method.
There is no need to define the render method. Viewmaster already defines it
and uses your template function to populate the this.el
element.
Nesting
Now we create a nested view for the addview-container
.
<script type="template" id="addview">
<input type="text">
<button>Add</button>
</script>
var AddTodoItem = Backbone.Viewmaster.extend({
template: function(context){
return _.template($("#addview").html(), context);
},
events: {
"click button": "addTodo"
},
addTodo: function(e){
e.preventDefault();
this.collection.add(new Backbone.Model({
text: this.$("input").text()
});
}
});
Nested views are also extended from Backbone.Viewmaster
. Any Viewmaster view
can be nested in any Viewmaster view and you can do as deep nesting as you
want.
Since we wanted this to be the view for the addview-container
and because
it's an element of TodoLayout
it is the responsibility of TodoLayout
to
nest it. We do that in its constructor using the setView method.
// TodoLayout
constructor: function(){
Backbone.Viewmaster.prototype.constructor.apply(this, arguments);
// Nest AddTodoItem inside TodoLayout
this.setView(".addview-container", new AddTodoItem({
collection: this.collection
}));
// Render layout on when a todo is removed or added to update
// the todo count
this.listenTo(this.collection, "add remove", this.render);
},
Here it's important to notice that setView and its friends, appendView, prependView, insertView and getViews should be considered in Java terms as protected methods. Which means you should use them only from within the view definition. This is because they all take a CSS selector as the first argument and because the CSS selectors are very implementation specific details of your view. If you need to set the view from the outside create a setter method for it to keep your views modular and maintainable.
Backbone 0.9.9 and later has a listenTo method on every event emitter object. This should be always used in views instead of the on method. Using it Backbone and Backbone.Viewmaster can automatically remove your view related event bindings when you discard your views.
Rendering
Now we are ready to create and render our nested view. We do that by calling the default render method.
var items = new Backbone.Collection();
var layout = new TodoLayout({
collection: items
});
layout.render();
$("body").append(layout.el);
The render method takes care of rendering itself and the initial rendering
of its child views. This means it will render child views only once unless
{ force: true }
is passed to the render method. This is because normally it
should be the responsibility of the child view to know when it should render
itself. The parent view only initialize child views.
Multiple views in single container
We add new TodoItem views to our layout using the appendView method whenever a todo model is added to the collection. refreshViews is used to make child view changes visible.
Every view container can contain multiple views. Just start adding more views to it if you need lists.
// TodoLayout
constructor: function(){
... snip ...
this.listenTo(this.collection, "add", this.addItem);
},
addItem: function(model){
// Create new view for the todo item
this.appendView("ul", new TodoItem({
model: model
}));
// Render the new item and put it to DOM tree
this.refreshViews();
}
The difference between render and refreshViews is that the latter one updates only child view changes made with setview, appendView, prependView and insertView and does not touch the parent itself. The former renders the parent and then calls refreshViews.
refreshViews will also take of of the initial rendering of child views if they have not been rendered before.
Removing views
Any view, parent or child, can be discarded with the remove method. It removes automatically all the Backbone and DOM event listeners. If the view is a parent to other views it will call remove on them also.
<script type="template" id="item">
<span class="item"><%= text %></span>
<button class="edit">edit</button>
<button class="done">x</button>
</script>
var TodoItem = Backbone.Viewmaster.extend({
constructor: function(){
Backbone.Viewmaster.prototype.constructor.apply(this, arguments);
// Rerender view after edit
this.listenTo(this.model, "change", this.render);
},
template: function(context){
return _.template($("#item").html(), context);
},
events: {
"click .done": "done",
"click .edit": "edit"
},
done: function(){
// When todo task is completed destroy the model and remove the view
this.model.destroy();
this.remove();
},
edit: function() {
var newContent = prompt("Edit todo", this.model.get("text"));
if (newContent !== null) this.model.set("text", newContent);
}
});
Views can be also removed by replacing them with setView. Viewmaster automatically figures out which views were left out and calls remove on them on the next refreshViews call.
If you need to use the view and its children again some time later use the detach method. It detaches the view from in its parent view, but leaves the event callbacks and children untouched.
Event bubbling and broadcasting
In order to keep views decoupled and resusable their implementation should not assume too much about their children and especially about their parents. Backbone.Viewmaster helps with this by implementing event bubbling and broadcasting.
Event bubbling works exactly like in the DOM. Events triggered with the bubble method are bubbled up to their parents too. Broadcasting is the opposite of this. Events triggered with the broadcast method are broadcasted down to the all child views too. These can be combined to make loose dependencies between sibling views too.
Lets add search capabilities to our TODO app. We create a simple Search view which bubbles 'search' events up to its parents.
var Search = Backbone.Viewmaster.extend({
template: function() {
return "<input type=text placeholder=Search >";
},
events: {
"keyup input": function(e) {
// Bubble 'search' event up to parent layout
this.bubble("search", $(e.target).val());
}
}
});
We'll abstract TodoItem listing to its own view and make it handle 'search' event broadcasts.
var TodoItemList = Backbone.Viewmaster.extend({
constructor: function(){
Backbone.Viewmaster.prototype.constructor.apply(this, arguments);
// On load display all items
this.setItems();
// Add new TodoItem view on new item model
this.listenTo(this.collection, "add", function(model) {
this.appendView("ul", new TodoItem({
model: model
}));
this.refreshViews();
});
// Filter out todos on 'search' event broadcasts
this.listenTo(this, "search", function(searchString) {
this.setItems(this.collection.filterSearch(searchString));
this.refreshViews();
});
},
template: function() {
return "<ul></ul>";
},
setItems: function(items) {
items = items || this.collection;
this.setView("ul", items.map(function(model) {
return new TodoItem({
model: model
});
}));
}
});
Now in the layout we just retrigger the bubbling 'search' event as a broadcast to the TodoItemList.
// TodoLayout
constructor: function(){
... snip ...
this.todoItemList = new TodoItemList({ collection this.collection ));
this.setView(".todo-container" this.todoItemList);
this.setView(".header" new Search());
// Listen on bubbled 'search' events from Search view
this.listenTo(this, "search", function(searchString) {
// Broadcast them to TodoItemList view
this.todoItemList.broadcast("search", searchString);
}
},
The cool thing about this is that the connection between the TodoItemList and the Search view is completely decoubled without introducing a global event object or manually passing some vent object to all your views. This makes unit testing simpler and if we wanted to add an another search box to a footer container we can just do that and that's it. No need to bind anything extra.
this.setView(".footer" new Search());
Another important feature of bubbling and broadcasting is that the views can be easily wrapped with other views and the event bindings would still work. You don't have to do anything in your wrapper view. The events will simply bubble and broadcast through it.
Conclusion
That's about it. Check out the full working todo app in the examples directory or play with the live app here.
FAQ
How do I use jQuery plugins?
In afterTemplate. It is executed right after the template is rendered but before child views are added making it the perfect place to add them without having to worry about side effects to the child views.
afterTemplate: function(){
this.$("element").jqueryPlugin();
}
Changelog
1.2.4
- Fix module.exports
1.2.3
- README fix for npmjs.org
1.2.2
- Backbone.Viewmaster is now prefed way to access the View. Backbone.ViewMaster is still available for backwards compatibility.
- Backbone namespace is not polluted when Viewmaster is loaded as node module.
1.2.1
- Define node module files
1.2.0
- Add afterTemplate
- Views can be added as constructor functions now too. They will get
model
andcollection
attributes from their parent views automatically. - Removed experimental
elements
object which was never documented. UseafterTemplate
from now on.
1.1.2
- Release to npm for Browserify usage
1.1.1
- Make sure view arrays don't mutate during iteration
1.1.0
- Support Backbone.js 0.9.9
- Remove
bindTo
,unbindFrom
andunbindAll
in favour of new Backbone methods listenTo and stopListening introduced in 0.9.9 - Add broadcast method
- Replace implicit event bubbling on trigger with simpler explicit bubble method
- Rename
renderViews
to refreshViews - Avoid detaches on refreshViews call unless it is absolutely necessary
- Documentation updates
1.0.0
- First release
License
The MIT License. See LICENSE.