skalb.com

Extending and Refactoring Views in Backbone.js

In a previous post, I built an example single page app using Backbone. One thing that bothered me was how similar the views are, yet didn’t share any code. I think part of this was that I originally scaffolded the entire app and worked backwards.

For example, here’s Project vs Feature Index Templates:

1
2
3
4
5
6
7
8
<h1>Listing projects</h1>

<table id="projects-table">
  <tr>
    <th>Name</th>
    <th></th>
  </tr>
</table>

vs.

1
2
3
4
5
6
7
8
<h1>Listing features</h1>

<table id="features-table">
  <tr>
    <th>Name</th>
    <th></th>
  </tr>
</table>

This is easily refactored to:

1
2
3
4
5
6
7
8
<h1>Listing <%= type %></h1>

<table id="items-table">
  <tr>
    <th>Name</th>
    <th></th>
  </tr>
</table>

Similarly, the New View changed from:

1
2
3
4
5
6
7
8
9
10
11
12
13
<h1>New project</h1>

<form id="new-project" name="project">
  <div class="field">
    <label for="name"> name:</label>
    <input type="text" name="name" id="name" value="<%= name %>" >
  </div>

  <div class="actions">
    <input type="submit" value="Create Project" />
  </div>

</form>

to

1
2
3
4
5
6
7
8
9
10
11
12
13
<h1>New <%= type %></h1>

<form id="new-item">
  <div class="field">
    <label for="name"> name:</label>
    <input type="text" name="name" id="name" value="<%= name %>" >
  </div>

  <div class="actions">
    <input type="submit" value="Create <%= type %>" />
  </div>

</form>

Great, that was easy and now I just reduced my total Templates. To share functionality between the Views I needed to create a base View class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Trackbone.Views ||= {}

class Trackbone.Views.IndexView extends Backbone.View
  template: JST["backbone/templates/index"]

  initialize: () ->
    @options.items.bind('reset', @addAll)
    @options.items.bind('sync', @addAll)

  addAll: () =>
    # This shouldn't be needed, but for some reason
    # lists are rendered twice
    @$("tbody").html('')

    @options.items.each(@addOne)

  addOne: (item) =>
    item.collection = @options.items
    @$("tbody").append(@getView({model: item}).render().el)

  render: =>
    $(@el).html(@template(type: @options.type))
    @addAll()

    return this

First thing to note is that this View has an initialize method. But that method will never be called automatically because we’re going to extend this View into a new class and create an instance of the subclass instead. Also note that we’re calling a function getView() that isn’t defined in this class.

1
2
3
4
5
6
7
8
9
10
11
#= require ../index_view

Trackbone.Views.Projects ||= {}

class Trackbone.Views.Projects.IndexView extends Trackbone.Views.IndexView
  initialize: () ->
    super
    @options.type = "Projects"

  getView: (options) =>
    new Trackbone.Views.Projects.ProjectView(options)

We can call the into the parent class by using super in this view’s initialize. This is just a CoffeeScript shortcut to apply the same arguments to the parent’s constructor. I’ve also explicitly required the parent View class since Rails does not guarantee which order JavaScript files will be loaded in the browser. The getView function here creates the correct ItemView based off the Project model.

The New item View shown below is generic enough that it did not need to be extended for each model.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Trackbone.Views.Projects ||= {}

class Trackbone.Views.NewView extends Backbone.View
  template: JST["backbone/templates/new"]

  events:
    "submit #new-item": "save"

  save: (e) ->
    e.preventDefault()
    e.stopPropagation()

    name = @.$("#new-item #name").val()
    if name
      $("#new-item #name").val('')
      @collection.create(name: name)

  render: ->
    $(@el).html(@template(type: @options.type))

    return this

The Item View is a bit trickier because it contained the select handling for when an item was clicked. To be able to reuse the handling, I had to make the load methods consistently named loadChildren as shown below.

1
2
3
4
5
6
7
8
9
10
11
12
class Trackbone.Models.Project extends Backbone.Model
  paramRoot: 'project'

  defaults:
    name: null

  loadChildren: ->
    @children = new Trackbone.Collections.FeaturesCollection([], {project_url: @url()});

class Trackbone.Collections.ProjectsCollection extends Backbone.Collection
  model: Trackbone.Models.Project
  url: '/projects'

One thing to point out in the Item View base class is that I believe I could have used the fat arrow to retain the correct context.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Trackbone.Views.Projects ||= {}

class Trackbone.Views.ItemView extends Backbone.View
  template: JST["backbone/templates/item"]

  events:
    "click .select" : "select"
    "click .destroy" : "destroy"

  tagName: "tr"
  className: "item"

  select: () ->
    if @model.loadChildren
      window.toggleSelected(@el)
      @model.loadChildren()
      do (@model, @renderChildren) ->
        @model.children.fetch(
          success: @renderChildren(@model.children)
        )
        @model.children.fetch()

  destroy: () ->
    @model.destroy()
    this.remove()

    return false

  render: ->
    $(@el).html(@template(@model.toJSON() ))
    return this

Now the actual Project view only had to define how to render it’s children.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#= require ../item_view

Trackbone.Views.Projects ||= {}

class Trackbone.Views.Projects.ProjectView extends Trackbone.Views.ItemView
  template: JST["backbone/templates/item"]

  renderChildren: (children) ->
    featuresView = new Trackbone.Views.Features.IndexView(items: children)
    $("#list-features").html(featuresView.render().el)

    # We should probably only render this once instead of each load
    newFeaturesView = new Trackbone.Views.NewView(collection: children, type: "Features")
    $("#new-features").html(newFeaturesView.render().el)

    $("#list-bugs").html('')
    $("#new-bugs").html('')

Again, here’s the backbone, coffeescript, rails

« How to (easily) handle model relationships in Rails and Backbone.js Handling permalinks in Backbone.js with Routers »

Comments