Backbone.Diorama.NestingView allows you to nest views inside each other, for example a collection index view where each model in the collection gets a sub view. This approach has a number of advantages:
- You can build smaller views and templates with clear responsibilities. (SRP).
- Smaller views listen and respond to events on a smaller number of objects, meaning smaller parts of the DOM can be re-rendered when changes occur.
- Sub views create opportunities for code re-use (DRY)
- Smaller views are easier to test in isolation.
In this example we will make an index list of blog posts, where each blog post
has it's own view. Note that this code is very close to what is generated by
diorama generate nestingView
First, we create a new View extending from the Backbone.Diorama.NestingView class, which itself extends from Backbone.View:
class Backbone.Views.PostIndexView extends Backbone.Diorama.NestingView
template: Handlebars.templates['post_index.hbs']
initialize: (options) ->
# Our Backone.Collection of posts
@postCollection = options.postCollection
@render()
render: =>
# Render template, which will create subviews with addSubViewTo helper (see template below)
# Note that a reference to @ is passed in as 'thisView'
@$el.html(@template(thisView: @, posts: @postCollection.models))
# Attach the subviews to the elements created by addSubViewTo helper
@attachSubViews()
return @
onClose: ->
# When we close this view, we also want to close all its child views
@closeSubViews()
Next we need to create a template which uses the addSubViewTo
handlebars
helper to create the sub views and render placeholder elements (complete with
data-sub-view-key attributes)
### post_index_view template ###
<h1>My Blog posts</h1>
{{#each posts}}
<!-- Use addSubViewTo to create a PostRowView for each post -->
<!-- Note that the first argument is the nesting view we're attaching to -->
{{addSubViewTo ../thisView "PostRowView" post=this}}
{{/each}}
Finally, we need to create the child view 'PostRowView'. This is just a typical Backbone.View, which takes in a post model and calls render() in its initialize function.
class Backbone.Views.PostRowView extends Backbone.View
template: Handlebars.templates['post_row.hbs']
initialize: (options) ->
# Read in the post model sent from the addSubViewTo handlebars helper
@post = options.post
# Call render as soon as the view is created
@render()
render: =>
@$el.html(@template(post: @post.toJSON()))
...and the post row template, which shows the title of the post
<h2>{{post.title}}</h2>
Now we have a PostRow view for each of our blog posts, which can setup its own event bindings for each model.
In the code we've built so far, each time PostIndexView::render
is
called, all of the sub views are closed and replaced with new instances. While
sometimes this is appropriate, often you may want to reuse subviews between
render calls.
Diorama.NestingView
allows you to do this by specifying a cache key as a third
argument to addSubViewTo
. Cache keys mean you can re-render parent views
without destroying or re-rendering their children, making state easier to
handle and improving app performance.
Let's modify our PostIndex template to use a sub view cache key:
<!-- addSubViewTo without a cache key -->
{{addSubViewTo ../thisView "PostRowView" post=this}}
<!-- addSubViewTo modified with a cache key -->
{{addSubViewTo ../thisView "PostRowView" "post-row-{{post.cid}}" post=this}}
The cache key is a handlebars template, which uniquely identifies a sub view. The handlebars template will interpolate the variables in the next argument (which are passed to the sub view initialize function).
Therefore, in the above example, with a post model with cid 'c1', the view with
have a cache key 'post-row-c1'
. This key is used to store the view in postIndexView.subViews['post-row-c1']
.
If we call render again with post model 'c1' again, the view will reuse the sub
view in postIndexView.subViews['post-row-c1']
. If we remove post model 'c1'
and call render again, the view in 'post-row-c1'
will be closed and
deleted.
For example:
posts = new Backbone.Collections.PostCollection()
# Add a post with cid = c1
posts.push(new Backbone.Models.Post())
# Create and render a postIndexView
postIndexView = new Backbone.Views.PostIndexView(postCollection: posts)
# A PostRowView will have been created and stored here using the cache-key:
postIndexView.subViews['post-row-c1']
# Calling render() again will reuse the subView at 'post-row-c1'
postIndexView.render()
postIndexView.subViews['post-row-c1'] # Same view as above
# remove post c1, add post c2 and re-render
posts.pop()
posts.push(new Backbone.Models.Post())
postIndexView.render()
# The view for post row c1 will be closed and deleted
postIndexView.subViews['post-row-c1'] # undefined
# A new view has been created for post c2
postIndexView.subViews['post-row-c2'] # the new view
Call this helper to insert a sub view into a template at this point. Creates an instance of the given childViewName, adds it to nestingView.subViews and inserts a placeholder DOM element.
nestingView
- The parent nesting view object, this should always be the view whose template is being rendered. This variable is required because handlebars helpers are executed in the global context. If you used a generator to create your nesting view, this will be passed into the template asthisView
childViewName
- The class name of the child view you want to insert. This will expect the view to be namespaced in Backbone.Views, e.g. given 'PostRowView', it creates an instance of Backbone.View.PostRowViewcache-key-template
- (optional) A optional handlebars template you can use to uniquely indentify your sub view, which allows view reuse and the ability to render a parent view without re-rendering its children. See Utilising view caching and reuseoptionsHash
- (optional) A hash which will be passed into the initialize function of the view named by childViewName. If only 3 arguments are given, this is assumed to be the 3rd argument (slightly odd behavior forced by handlebars :-| ).
Examples:
<!-- Add a new Backbone.Views.PostRowView for the post model into the current NestingView -->
{{addSubViewTo thisView "PostRowView" model=post}}
<!-- Add a new Backbone.Views.TodoItem for the task model with a cache key template -->
{{addSubViewTo thisView "TodoItem" "todo-item-{{task.cid}}" task=task}}
For each view in @subViews
, it sets the DOM element of the view to the
placeholder rendered by the addSubViewTo
handlebars helper. Call this
after you've rendered the template for your NestingView (NestingViews generated
by diorama generate nestingView
do this by default).
Calls close
on each sub view. Call this inside your NestingView's onClose
method (NestingViews generated by diorama generate nestingView
do this by
default).
Calls the render function on each sub view. Sub-views generated by diorama generate nestingView
call render inside their initialize functions, meaning there is
no need to call this, unless you want to force the children to re-render for
some reason
The syntax of NestingView was simplified in the 0.2.0 release, existing apps will require some small changes.
Render functions were previously required to call closeSubViews
,
then renderSubViews
render: =>
@closeSubViews() # No longer required to be called
@$el.html(@template(thisView: @))
@renderSubViews() # Replaced with attachSubViews()
closeSubView
is no longer required (attachSubViews cleans up automatically), and attachSubViews
replaces the call to renderSubViews
. The preferred pattern is to have sub views call render themselves, for example in their initialize function, however, you can manually re-render (like diorama 0.1.1) by calling renderSubViews
after attachSubViews
.
render: =>
@$el.html(@template(thisView: @))
@attachSubViews() # Attaches sub views (but does not call render)
@renderSubViews() # (Optional) If you wish to manually render sub views, call this