Skip to content
Find file
Fetching contributors…
Cannot retrieve contributors at this time
531 lines (384 sloc) 16.2 KB

Using Handlebars

This guide covers displaying content in the browser using Handlebars templates and SC.TemplateView. After reading this guide, you will be able to:

  • Describe the markup of your application using Handlebars-flavored HTML.
  • Have your HTML update automatically when your data model changes.
  • Integrate templates into existing applications.
  • Build new applications entirely with templates.

endprologue.

Handlebars

SproutCore comes bundled with Handlebars, a semantic templating language. These templates look like regular HTML, with embedded handlebars expressions:

Hello, {{name}}!

You should store your Handlebars templates inside your application’s resources/templates directory. Make sure you save them with a .handlebars extension. At runtime, SproutCore will compile and load these templates so they are available for you to use in your views.

SC.TemplateView

An SC.TemplateView is a view that is responsible for rendering a Handlebars template and inserting it into the DOM.

To tell the TemplateView which template to use, set its templateName property. For example, if I saved a template to a file called sayhello.handlebars, I would set the templateName property to “sayhello”.

Hello, {{name}}!

var view = SC.TemplateView.create({
templateName: ‘sayhello’,
name: “Bob”
});

NOTE: For the remainder of the guide, the templateName property will be omitted from most examples. You can assume that if we show a code sample that includes an SC.TemplateView and a Handlebars template, the view has been configured to display that template via the templateName property.

Handlebars Basics

As you’ve already seen, you can print the value of a property by enclosing it in a Handlebars expression, or a series of braces, like this:

My new car is {{color}}.

This will look up and print the TemplateView’s color property. For example, if your view looks like this:

App.CarView = SC.TemplateView.extend({
color: ‘blue’
});

Your view would appear in the browser like this:

My new car is blue.

All of the features described in this guide are bindings aware. That means that if the values used by your templates ever change, your HTML will be updated automatically. It’s like magic.

In order to know which part of your HTML to update when an underlying property changes, Handlebars will insert marker elements with a unique ID. If you look at your application while it’s running, you might notice these extra elements:

My new car is blue.

Because all Handlebars expressions are wrapped in these markers, make sure each HTML tag stays inside the same block. For example, you shouldn’t do this:

<div {{#if isUrgent}}class="urgent"{{/if}}>

{{#if}}, {{else}}, and {{#unless}}

Sometimes you may only want to display part of your template if a property exists. For example, let’s say we have a view with a person property that contains an object with firstName and lastName fields:

App.SayHelloView = SC.TemplateView.extend({
person: SC.Object.create({
firstName: “Joy”,
lastName: “Clojure”
})
});

In order to display part of the template only if the person object exists, we can use the {{#if}} helper to conditionally render a block:

{{#if person}}
Welcome back, {{person.firstName}} {{person.lastName}}!
{{/if}}

NOTE: Handlebars will not render the block if the argument passed evaluates to false, undefined, null or [] (i.e., any “falsy” value).

If the expression evaluates to falsy, we can also display an alternate template using {{else}}:

{{#if person}}
Welcome back, {{person.firstName}} {{person.lastName}}!
{{else}}
Please log in.
{{/if}}

To only render a block if a value is falsy, use {{#unless}}:

{{#unless hasPaid}}
You owe: ${{total}}
{{/unless}}

{{#if}} and {{#unless}} are examples of block expressions. These allow you to invoke a helper with a portion of your template. Block expressions look like normal expressions except that they contain a hash (#) before the helper name, and require a closing expression.

{{#with}}

Sometimes you may want to invoke a section of your template with a context different than the SC.TemplateView. For example, we can clean up the above template by using the {{#with}} helper:

{{#with person}}
Welcome back, {{firstName}} {{lastName}}!
{{/with}}

NOTE: {{#with}} changes the context of the block you pass to it. The context is the object on which properties are looked up. By default, the context is the SC.TemplateView to which the template belongs.

Binding Element Attributes with {{bindAttr}}

In addition to text, you may also want your templates to dictate the attributes of your HTML elements. For example, imagine a view that contains a URL:

App.LogoView = SC.TemplateView.extend({
logoUrl: ‘http://www.mycorp.com/images/logo.png’
});

The best way to display the URL as an image in Handlebars is like this:

This generates the following HTML:

If you use {{bindAttr}} with a Boolean value, it will add or remove the specified attribute. For example, given this SproutCore view:

App.InputView = SC.TemplateView.extend({
isSelected: true
});

And this template:

<input type=“checkbox” {{bindAttr checked="isSelected"}}>

Handlebars will produce the following HTML element:

Binding Class Names with {{bindAttr}}

The class attribute can be bound like any other attribute, but it also has some additional special behavior. The default behavior works like you’d expect:

App.AlertView = SC.TemplateView.extend({
priority: “p4”,
isUrgent: true
});

<div {{bindAttr class="priority"}}>
Warning!

This template will emit the following HTML:

Warning!

If the value to which you bind is a Boolean, however, the dasherized version of that property will be applied as a class:

<div {{bindAttr class="isUrgent"}}>
Warning!

This emits the following HTML:

Warning!

Unlike other attributes, you can also bind multiple classes:

<div {{bindAttr class=“isUrgent priority”}}>
Warning!

Localized Strings with {{loc}}

SproutCore has built-in support for localized applications. To emit a localized version of a string, use the {{loc}} helper:

{{loc myLocalizedString}}

Building a View Hierarchy

So far, we’ve discussed writing templates for a single view. However, as your application grows, you will often want to create a hierarchy of views to encapsulate different areas on the page. Each view is responsible for handling events and maintaining the properties needed to display it.

{{view}}

To add a child view to a parent, use the {{view}} helper, which takes a path to a view class.

// Define parent view
App.UserView = SC.TemplateView.extend({
templateName: ‘user’,

firstName: “Albert”, lastName: “Hofmann”

});

// Define child view
App.InfoView = SC.TemplateView.extend({
templateName: ‘info’,

posts: 25, hobbies: “Riding bicycles”

});

User: {{firstName}} {{lastName}}
{{view App.InfoView}}

Posts: {{posts}}

Hobbies: {{hobbies}}

If we were to create an instance of App.UserView and render it, we would get a DOM representation like this:

User: Albert Hofmann


Posts: 25

Hobbies: Riding bicycles

Relative Paths

Instead of specifying an absolute path, you can also specify which view class to use relative to the parent view. For example, we could nest the above view hierarchy like this:

App.UserView = SC.TemplateView.extend({
templateName: ‘user’,

firstName: “Albert”, lastName: “Hofmann”, InfoView: SC.TemplateView.extend({ templateName: ‘info’, posts: 25, hobbies: “Riding bicycles” })

});

User: {{firstName}} {{lastName}}
{{view InfoView}}

Setting Child View Templates

If you’d like to specify the template your child views use (instead of having to place them in a separate Handlebars file), you can use the block form of the {{view}} helper. We might rewrite the above example like this:

App.UserView = SC.TemplateView.extend({
templateName: ‘user’,

firstName: “Albert”, lastName: “Hofmann”

});

App.InfoView = SC.TemplateView.extend({
posts: 25,
hobbies: “Riding bicycles”
});

User: {{firstName}} {{lastName}}
{{#view App.InfoView}}
Posts: {{posts}}

Hobbies: {{hobbies}}
{{/view}}

When you do this, it may be helpful to think of it as assigning views to portions of the page. This allows you to encapsulate event handling for just that part of the page.

Setting Up Bindings

So far in our examples, we have been setting static values directly on the views. But to best implement an MVC architecture, we should actually be binding the properties of our views to the controller layer.

Let’s set up a controller to represent our user data:

App.userController = SC.ObjectController.create({
content: SC.Object.create({
firstName: “Albert”,
lastName: “Hofmann”,
posts: 25,
hobbies: “Riding bicycles”
})
});

INFO: An SC.ObjectController is a controller that serves as a proxy to another object, contained in the controller’s content property. If you want to change the object that your views are displaying, just set the content property to the new object. Since your views are all bound to the controller, they will update automatically without having to reconfigure their bindings.

Now let’s update App.UserView to bind to App.userController:

App.UserView = SC.TemplateView.extend({
templateName: ‘user’,

firstNameBinding: ‘App.userController.firstName’, lastNameBinding: ‘App.userController.lastName’

});

When we only have a few bindings to configure, like with App.InfoView, it is sometimes useful to be able to declare those bindings in the template. You can do that by passing additional arguments to the {{#view}} helper. If all you’re doing is configuring bindings, this often allows you to bypass having to create a new subclass.

User: {{firstName}} {{lastName}}
{{#view App.InfoView postsBinding=“App.userController.posts”
hobbiesBinding=“App.userController.hobbies”}}
Posts: {{posts}}

Hobbies: {{hobbies}}
{{/view}}

NOTE: You can actually pass any property as a parameter to {{view}}, not just bindings. However, if you are doing anything other than setting up bindings, it is generally a good idea to create a new subclass.

Modifying a View’s HTML

When you append a view, it creates a new HTML element that holds its content. If your view has any child views, they will also be displayed as child nodes of the parent’s HTML element.

By default, new instances of SC.View create a

element. You can override this by passing a tagName parameter:

{{view App.InfoView tagName="span"}}

You can also assign an ID attribute to the view’s HTML element by passing an id parameter:

{{view App.InfoView id=“info-view”}}

This makes it easy to style using CSS ID selectors:

/* Give the view a red background. */
#info-view {
background-color: red;
}

You can assign class names similarly:

{{view App.InfoView class=“info urgent”}}

You can bind class names to a property of the view by using classBinding instead of class. The same behavior as described in bindAttr applies:

App.AlertView = SC.TemplateView.extend({
priority: “p4”,
isUrgent: true
});

{{view App.AlertView classBinding=“isUrgent priority”}}

This yields a view wrapper that will look something like this:

Displaying a List of Items

If you need to display a basic list of items, use Handlebar’s {{#each}} helper:

App.PeopleView = SC.TemplateView.extend({
people: [ SC.Object.create({ name: ‘Steph’ }),
SC.Object.create({ name: ‘Tom’ }) ]
});

{{#each people}}
Hello, {{name}}!
{{/each}}

This will print a list like this:

  • Hello, Steph!
  • Hello, Tom!

SC.TemplateCollectionView

Sometimes you will need each item in your list to handle events. In that case, you will need more sophistication than what {{#each}} can provide. You can use the {{#collection}} helper to create a new SC.TemplateCollectionView. You can bind the instance of SC.TemplateCollectionView to an array, and it will create a new SC.TemplateView for each item.

Usually, you will bind the collection to an SC.ArrayController, like this:

App.peopleController = SC.ArrayController.create({
content: [‘Steph’, ‘Tom’, ‘Ryan’, ‘Chris’, ‘Jill’]
});

{{#collection SC.TemplateCollectionView contentBinding=“App.peopleController”}}
Hi, {{content}}!
{{/collection}}

NOTE: The template you pass to the block helper will look up properties relative to each child view. To access the item in the array that the view should represent, refer to the content property via {{content}}.

Remember that under the hood, SC.TemplateCollectionView creates a new view for each item in the bound array. By default, each new view will be an instance of SC.TemplateView. You can tell the collection which type of view to create instances of by subclassing SC.TemplateCollectionView and supplying a custom class:

App.PeopleCollectionView = SC.TemplateCollectionView.extend({
itemView: SC.TemplateView.extend({
mouseDown: function(evt) {
window.alert(‘You clicked on ’ + this.get(’content’));
}
})
});

{{#collection App.PeopleCollectionView contentBinding=“App.peopleController”}}
Hi, {{content}}!
{{/collection}}

If you run this code, you should see an alert every time you click on one of the items.

The {{#collection}} helper takes the same options as {{#view}}, as described above. For example, you can set an HTML id attribute on the container of SC.TemplateCollectionView like this:

{{collection App.MyCollectionView id=“my-collection”}}

What if you want to set the class name of every child view, though? If you prepend an option with item, that option will instead be set on the child. For example, let’s say you wanted to set a class name on each item in your collection:

{{collection App.MyCollectionView itemClass=“collection-item”}}

Using SC.TemplateView Inside Desktop Controls

If you’re building a desktop-style application, you can get the best of both native and web worlds by integrating SC.TemplateViews into SproutCore’s other controls. For example, you can easily embed an SC.TemplateView inside an SC.ScrollView:

App.mainPage = SC.Page.design({
mainPane: SC.MainPane.extend({
childViews: [‘scrollView’],

scrollView: SC.ScrollView.design({ contentView: SC.TemplateView.design({ templateName: ‘main_page’ }) })

});

Changelog

  • May 5, 2011: Initial version by Tom Dale
Something went wrong with that request. Please try again.