Handlebars templates are strings interpolated by JavaScript functions. When the function comes across double curlies, it tries to resolve the instance of the delimiter with an object property or invoke a nested function to return a string. These nested functions are called helpers and are created in the application. There are a few helpers built into the Handlebars library, and Ember adds some of its own.
Helpers can take two forms. The first are inline helpers, which use the syntax {{[helper name] [arguments]}}
.
The arguments can include a hash of options, such as:
{{input type="text" value=firstName disabled=entryNotAllowed size="50"}}
More complex helpers use a block syntax:
{{#[helper name] [arguments]}} [block content] {{/[helper name]}}
For example, if you wanted to present a sign-in link only to users who were not already logged in, you could use something like the block below:
{{#if notSignedIn}} <a href="/">Sign In</a> {{/if}}
For block helpers, content can be passed to the block to augment the output with dynamic segments. Handlebars’ built-in conditionals, described in the next section, are block helpers.
You are going to use helpers to render sections of your
templates for sightings and cryptids as well as the NavBar
.
Conditional statements let you introduce basic control flow into Handlebars templates. Their syntax looks like this:
{{#if argument}} [render block content] {{else}} [render other content] {{/if}}
Or, alternatively, like this:
{{#unless argument}} [render block content] {{/unless}}
Conditional statements take a single argument that resolves to a truthy or falsy value. (Those are not typos. A truthy value is one that evaluates to true in a Boolean context. All values are truthy except those defined as falsy: the values false, 0, "", null, undefined, and NaN.)
Time to get to work. Open app/templates/sightings/index.hbs and add a conditional statement so that sighting entries display either the location or, if there is no location data, a polite warning about the missing data.
<div class="panel panel-default"><ul class="list-group">{{#each model as |sighting|}}<li class="list-group-item">{{sighting.location}} - {{sighting.sightedAt}}</li>{{/each}}</ul></div><div class="row"> {{#each model as |sighting|}} <div class="col-xs-12 col-sm-3 text-center"> <div class="media well"> <div class="caption"> {{#if sighting.location}} <h3>{{sighting.location}} - {{sighting.sightedAt}}</h3> {{else}} <h3 class="text-danger">Bogus Sighting</h3> {{/if}} </div> </div> </div> {{/each}} </div>
You changed the DOM structure of
your sightings template to start outputting sighting information
in the form of styled elements using Bootstrap’s
wells style. The use of
{{#if}} and {{else}}
allows you to render different HTML
when the location of the sighting has not been added.
Now you need to change the data sent from the route to the template to see the results of your new conditional.
In your sightings route model in app/routes/sightings.js, set the
location
property of one sighting to the empty
string.
... model(){ ... let record3 = this.store.createRecord('sighting', { location: 'Asilomar', sightedAt: new Date('2016-03-21') }); return [record1, record2, record3]; } });
Start your server and point your browser to http://localhost:4200/sightings to see the new list of sightings (Figure 23.2).
The conditional statement evaluated the truthy value of the empty string for the last sighting instance. Thus, the template rendered the block content with the text “Bogus Sighting.”
You have already used the {{#each}}
block
helper in the index template. This helper renders each object
in the array as an instance of the content in the block. The
argument for {{#each}}
is the
array
as an |instance|
contained in the block argument. The block will only render
if the argument passed in to {{#each}}
is
an array with at least one element.
Like the
{{#if}}
block helper, this helper supports
an {{else}}
block that will render when the
array argument is empty.
Use {{#each}} {{else}} {{/each}}
to create a listing of all recorded cryptids –
or, if there are none, the text “No Creatures” –
in app/templates/cryptids.hbs:
{{outlet}}<div class="row"> {{#each model as |cryptid|}} <div class="col-xs-12 col-sm-3 text-center"> <div class="media well"> <div class="caption"> <h3>{{cryptid.name}}</h3> </div> </div> </div> {{else}} <div class="jumbotron"> <h1>No Creatures</h1> </div> {{/each}} </div>
Similar to sightings, you are listing the cryptids in styled wells.
The {{else}}
block allows you to include a
condition in your template when there are no items
in the array you are listing. Your app can render a different
element for this conditional of an empty model array.
Navigate to http://localhost:4200/cryptids to see your new cryptid listing (Figure 23.3).
Now, in app/routes/cryptids.js, remove the
model
callback by commenting out the
return
statement to exercise the
else
of your conditional.
... model(){ // return this.store.findAll('cryptid'); } });
Reload http://localhost:4200/cryptids. Your cryptid listing is now blank (Figure 23.4).
With {{each}} {{else}} {{/each}}
, you
can have conditional views based on the presence of data and very
little conditional logic. Now that you have seen (and tested) the wonders
of conditional iterators, return app/routes/cryptids.js to its previous
state:
... model(){//return this.store.findAll('cryptid'); } });
Element attribute values can be rendered from
controller properties just as element content
is rendered between DOM element tags. In earlier versions of
Ember, there was a helper to bind
attributes called {{bind-attr}}
. Now, thanks
to HTMLBars, you can you just use {{}}
to
bind a property to the attribute.
Attribute binding is common with element properties like
class
and src
. Your cryptids
have an image path in their data, so you can dynamically bind their
src
property to a model
attribute.
Add an image to the cryptid listing in app/templates/cryptids.hbs:
<div class="row"> {{#each model as |cryptid|}} <div class="col-xs-12 col-sm-3 text-center"> <div class="media well"> <img class="media-object thumbnail" src="{{cryptid.profileImg}}" alt="{{cryptid.name}}" width="100%" height="100%"> <div class="caption"> <h3>{{cryptid.name}}</h3> </div> </div> </div> {{else}} <div class="jumbotron"> <h1>No Creatures</h1> </div> {{/each}} </div>
You will need to add the cryptid images from your course assets to the directory tracker/public/assets/image/cryptids. When the Ember server is running, the public directory is the root for assets. For a production application you may need to configure these paths, but for development, public/assets is a good place to work. These files are copied to the dist directory where your application is compiled and served.
When deploying an application to a server and adding images to persisted data you need to be conscious of the path to the actual image file. In our example, we are serving the images from the same directory as our application, and the database stores the relative path of the image to our application.
Before HTMLBars, the {{bind-attr}}
helper
could be used as an inline ternary operation for assigning
properties based on a Boolean property. Now, you can use the
inline {{if}}
helper. This is common when
your UI has styles that represent true and false states of a
specific property.
Use the ternary form in app/templates/cryptids.hbs to handle missing images:
<div class="row"> {{#each model as |cryptid|}} <div class="col-xs-12 col-sm-3 text-center"> <div class="media well"> <img class="media-object thumbnail"src="{{cryptid.profileImg}}"src="{{if cryptid.profileImg cryptid.profileImg 'assets/images/cryptids/blank_th.png'}}" alt="{{cryptid.name}}" width="100%" height="100%"> <div class="caption"> ...
Unlike the block {{#if}}
helper, the inline
{{if}}
helper does not yield block content.
The inline helper evaluates the first argument as a Boolean and
outputs either the second or third argument.
Here, you are evaluating the truthiness of
{{cryptid.profileImg}}
for the first
argument. If it is truthy, the output is the cryptid’s image path. Otherwise, a
placeholder image is specified.
You can use a dynamic value as an argument to all inline helpers. You can also pass any JavaScript primitive as an argument, such as a string, number, or Boolean.
Before you look at the results of your conditional, create a cryptid without an image path in the beforeModel hook in app/routes/cryptids.js:
import Ember from 'ember'; export default Ember.Route.extend({ beforeModel(){ this.store.createRecord('cryptid', { "name": "Charlie", "cryptidType": "unicorn" }); }, model(){ return this.store.findAll('cryptid'); } });
Now reload http://localhost:4200/cryptids and check out your new images (Figure 23.5).
As discussed in Chapter 20,
routing is unique to browser-based applications. Ember listens
to a couple of event hooks to manage routing in your application.
For this reason, you should create links with {{#link-to}}
block helpers. This helper takes the route (represented by a
string) as the first argument to create an anchor element. For
example, {{#link-to 'index'}}Home{{/link-to}}
creates a link to the root index page.
To see how this works, you are going to update your main navigation with links
using
{{#link-to}}
helpers.
Begin in app/templates/application.hbs.
Replace the NavBar
’s test links with links to your sightings, cryptids, and witnesses:
... <div class="collapse navbar-collapse" id="top-navbar-collapse"> <ul class="nav navbar-nav"><li><a href="#">Test Link</a></li><li><a href="#">Test Link</a></li><li> {{#link-to 'sightings'}}Sightings{{/link-to}} </li> <li> {{#link-to 'cryptids'}}Cryptids{{/link-to}} </li> <li> {{#link-to 'witnesses'}}Witnesses{{/link-to}} </li> </ul> </div><!-- /.navbar-collapse -->
Now that you have links to your listing pages, reload your app and test them. Click around. Hit the back button. You have a working web app! Take a moment to celebrate.
Your next task is to make the images on the cryptids page link to an individual page for each creature.
To do this, you will take advantage of the fact that the {{#link-to}}
helper can take multiple
arguments to customize the link.
In app/templates/cryptids.hbs, wrap the <img>
tag with a link to a
specific cryptid using cryptid.id
as the second argument:
... <div class="media well"> {{#link-to 'cryptid' cryptid.id}} <img class="media-object thumbnail" src="{{if cryptid.profileImg cryptid.profileImg 'assets/images/cryptids/blank_th.png'}}" alt="{{cryptid.name}}" width="100%" height="100%"> {{/link-to}} <div class="caption"> <h3>{{cryptid.name}}</h3> </div> </div> ...
Now that you have links to the cryptid
route
and the anchor has a path to cryptids/[cryptid_id], you will need to edit
router.js so the
CryptidRoute
knows to expect a dynamic value.
... Router.map(function() { this.route('sightings', function() { this.route('new'); }); this.route('sighting', function() { this.route('edit'); }); this.route('cryptids'); this.route('cryptid', {path: 'cryptids/:cryptid_id'}); this.route('witnesses'); this.route('witness'); }); ...
Try it out. You should see a blank page after clicking
one of the cryptid images. This is good. Your app is routing you
to the CryptidRoute
, which is rendering the
app/templates/cryptid.hbs, singular.
That file is currently blank.
If you clicked on Charlie the unicorn’s image, you probably got an error.
Recall that his record was created in the beforeModel hook
and does not have an id
. That means that
the value it tries to pass to the
{{#link-to}}
helper is
null
.
This is a good time to remove that beforeModel hook. Creation should be reserved for pages creating new cryptids, which we will cover in Chapter 24.
Remove the hook from app/routes/cryptids.js.
...beforeModel(){this.store.createRecord('cryptid', {"name": "Charlie","cryptidType": "unicorn"});},model(){ return this.store.findAll('cryptid'); } ...
Next, add the request for cryptid data in the app/routes/cryptid.js (singular).
import Ember from 'ember'; export default Ember.Route.extend({ model(params){ return this.store.findRecord('cryptid', params.cryptid_id); } });
The cryptid_id
dynamic route parameter is
passed to the route’s model
hook as an
argument. You use this parameter to call the
store
’s findRecord method.
Now, edit the template for an individual cryptid, app/templates/cryptid.hbs, to show the cryptid’s image and name.
{{outlet}}<div class="container text-center"> <img class="img-rounded" src="{{model.profileImg}}" alt="{{model.type}}"> <h3>{{model.name}}</h3> </div>
The model passed to this template is a single object, not an array of objects to iterate over. The this.store.findRecord method returns a single cryptid instance. In the template, the model is this instance and the properties are retrieved using {{model.[property-name]}}.
In the browser, use your NavBar
to navigate to
Cryptids and then click one of the cryptid images to
view its detail page (Figure 23.6).
You will explore {{#link-to}}
more in
future chapters. Remember, helpers are functions that are
invoked when the template is rendered. Ember comes with its own,
but you are not limited to the built-in helpers.
3.138.105.215