Template rendering

Tim Branyen edited this page Apr 8, 2015 · 7 revisions

One of the best parts about LayoutManager is working with the super flexible sync/async friendly fetchTemplate and renderTemplate overridable methods.

Fetching

There are several ways of loading your templates. If you can, try to abstract as much into the fetchTemplate method to make your template properties lean and descriptive.

Script tag hack

Out of the box LayoutManager has defaults in place to easily fetch templates from script tags containing template contents.

If this is slightly confusing, check out the Overview section for more information on how these work.

<script type="template" id="my-template">
  Hi <%= name %>!
</script>

You can now use this template easily by doing:

Backbone.Layout.extend({
  template: "#my-template"
});

This is not recommended for production applications. Check out the recommended best practices below for more information on alternative methods.

How template is passed to fetchTemplate

When the template property is not null, undefined, or a function it will be passed to a configurable method fetchTemplate. You can read more about this in Properties and Methods.

If the template is not a string, it will be passed unchanged to fetchTemplate. If the template property is a string and you have a prefix property set, the template will be prefixed with that properties value.

Path based string template example:

Backbone.Layout.extend({
  // Remember the prefix needs to have a trailing `/`.
  prefix: "/templates/", template: "my-template",

  // This method would be called with the `prefix` + `template`.
  fetchTemplate: function(path) {
    // Path would be `/templates/my-template` here.
  }
});

Selector based string template example:

Backbone.Layout.extend({
  // Remember the prefix needs to have a trailing `/`.
  prefix: "script#", template: "my-template",

  // This method would be called with the `prefix` + `template`.
  fetchTemplate: function(path) {
    // Path would be `script#my-template` here.
  }
});

Pass in a template function

If you have access to the template function directly, perhaps through RequireJS, or a global template namespace, you can directly assign it to the template property.

// Assume `window.JST` is an object containing all your templates.
window.JST = {
  // Basic template demonstration.
  "some-template": _.template("<%= some template %>")
};

// Create a new Layout that uses the previously defined template function.
Backbone.Layout.extend({
  template: window.JST["some-template"]
});

Recommended fetch implementation

The default implementation is not an ideal way of loading templates and we highly recommend you explore other options, such as AJAX loading during development and compiling for production.

Here is an example:

Backbone.Layout.configure({
  // Set the prefix to where your templates live on the server, but keep in
  // mind that this prefix needs to match what your production paths will be.
  // Typically those are relative.  So we'll add the leading `/` in `fetch`.
  prefix: "templates/",

  // This method will check for prebuilt templates first and fall back to
  // loading in via AJAX.
  fetchTemplate: function(path) {
    // Check for a global JST object.  When you build your templates for
    // production, ensure they are all attached here.
    var JST = window.JST || {};

    // If the path exists in the object, use it instead of fetching remotely.
    if (JST[path]) {
      return JST[path];
    }

    // If it does not exist in the JST object, mark this function as
    // asynchronous.
    var done = this.async();

    // Fetch via jQuery's GET.  The third argument specifies the dataType.
    $.get(path, function(contents) {
      // Assuming you're using underscore templates, the compile step here is
      // `_.template`.
      done(_.template(contents));
    }, "text");
  }
});

Serializing

Once you have loaded your template function into the View using an above method, you'll want to be able to provide the correct data to render.

The serialize property is named the same as the example in the Backbone documentation to reduce confusion.

Function

Use a function for serialize when you have data changing dynamically such as models, collections, etc.

Backbone.Layout.extend({
  serialize: function() {
    return { user: this.model };
  }
});

This will provide this.model as user inside of your templates. If you are not using a template engine that supports inline JavaScript, you want to pass the raw attributes instead.

Backbone.Layout.extend({
  serialize: function() {
    return { user: _.clone(this.model.attributes) };
  }
});

When you provide a collection or anything iterable, it is considered a good practice to pass it as a wrapped (chained) underscore object.

Backbone.Layout.extend({
  serialize: function() {
    return {
      // Wrap the users collection.
      users: _.chain(this.collection)
    };
  }
});

The benefit here is that you no longer depend on an _ variable inside your templates and it becomes easier to read:

<% users.each(function(user) { %>

instead of:

<% _.each(users, function(user) { %>

Object

If your content never changes, or you want to assign the context manually at run-time, you can use an object instead of a function. This is very similar to how Backbone handles almost all dynamic values (think Model#url).

// Create a demo Layout.
var myLayout = new Backbone.Layout({
  template: _.template("<%= msg %>")
});

// Set the data.
myLayout.serialize = { msg: "Hello world!" };

Rendering

When to render

If you nest a View and call render on the parent, you will not need to call render on the nested View. The render logic will always iterate over nested Views and ensure they are rendered properly.

Sometimes it's confusing as to when you should trigger render. Take this common scenario:

Backbone.Layout.extend({
  addView: function(model, render) {
    // Insert a nested View into this View.
    var view = this.insertView(new ItemView({ model: model }));

    // Only trigger render if it not inserted inside `beforeRender`.
    if (render !== false) {
      view.render();
    }
  },

  beforeRender: function() {
    this.collection.each(function(model) {
      this.addView(model, false);
    }, this);
  },

  initialize: function() {
    this.listenTo(this.collection, "add", this.addView);
  }
});

There is special logic inside of addView that conditionally determines if you should render or not. This is semi-fragile and hopefully a more straightforward method will arise.

The return value

Whenever you call render you will receive back a Deferred object, which can be used to know when the render has completed.

myLayout.render().then(function() {
  /* Rendering has completed. */
});

If you want access to the View, the Deferred returns with a property view that you can access to continue chaining.

myLayout.render().view.$el.appendTo("body");

Cancelling a render from beforeRender

If you wish to prevent a render from happening, you can cancel the render by returning false or a rejected Promise from within the beforeRender method.

Example:

var CancelledView = Backbone.Layout.extend({
  beforeRender: function() {
    return false;
  },

  afterRender: function() {
    window.alert("This will never alert!");
  }
});

Keeping an inserted View

Whenever you insert a View and render it, if it has already rendered it will be deleted. This prevents duplicates in lists. Since this is not always desired behavior, you may want to add a property to the view: keep: true.

This property will keep the View from being removed.

Example:

var AppendedView = Backbone.Layout.extend({
  keep: true
});

// Insert into my layout.
myLayout.insertView(".insert-region", new AppendedView());

// Render.
myLayout.getView(".insert-region").render();

// Render twice! Doesn't get removed.
myLayout.getView(".insert-region").render();