Você está na página 1de 181

Level 1

Getting Started
What Is Ember?

Your Knowledge Coming In


Must Know

Not as Important
Ruby, Python, Java, PostgreSQL, MySQL, etc.

Should Have

Others Using

Others Using

Others Using

Others Using

Others Using

Others Using

Separating Responsibilities
Model-view-controller (MVC) helps to create reusable and extendable code by separating and
isolating responsibilities.

model
Manipulates

Updates
view

controller

Displays

Uses
user

Current Responsibilities
Today, most web applications send data to the servers to validate, process, and render HTML.
Web Server

Web Browser

Request URL
validation
processing
rendering

Receive HTML & Assets

Shifting Responsibilities
Frameworks like Ember are shifting responsibilities from the server to the web browser.
Web Server

Web Browser

Request URL
Receive HTML & Assets

validation
processing
rendering

More client responsibilities


mean fewer network requests,
which make an app feel faster.

All Encompassing
Ember provides the client-side framework plus development tooling, conventions, and standards.

framework

tooling

conventions

standards

Capturing Community Best Practices

Best Practices

Embers holistic approach gives you benets from the entire JavaScript community.

framework
tooling
convention
standards

Ember core team members


participate in W3C committees,
specifications, and standards.

Woodland Wanderer Whatchamacallits

Woodland Wanderer Whatchamacallits

Woodland Wanderer Whatchamacallits

Wilderness Safety Quiz

Wilderness Safety Quiz

Wilderness Safety Quiz

Installing
Steps for installing Ember CLI and creating our Ember application:
1. Ensure Node.js is installed and npm is available.

https://nodejs.org

Installing
Steps for installing Ember CLI and creating our Ember application:
1. Ensure Node.js is installed and npm is available.
2. Install Ember CLI using npm.
Console
$ npm install -g ember-cli

This installs the package


globally, making package
executables available
everywhere on the system.

This is the name of the npm


package to install.
Ember CLI is packaged as
ember-cli.

Checking for
Once Ember CLI is installed, youll have an ember executable available.
Console
$ npm install -g ember-cli
ember-cli@2.x.x /path/to/node/lib/node_modules/ember-cli
$ ember version
version: 2.x.x

After installing Ember CLI, running ember


version shows the running Ember CLI version.

Getting Help in
Console
$ ember help
Usage: ember <command (Default: help)>
Available commands in ember-cli:
ember
ember
ember
ember
ember
ember
ember
ember
ember
ember
ember

addon <addon-name> <options...>


build <options...>
destroy <blueprint> <options...>
generate <blueprint> <options...>
help <command-name (Default: all)> <options...>
init <glob-pattern> <options...>
install <addon-name>
new <app-name> <options...>
serve <options...>
test <options...>
version <options...>

Creating an App With ember new


ember new <app-name> <options...>

This is used to create a new Ember application.

Creating an App With ember new


ember new <app-name> <options...>

Console
$ ember new woodland
installing app
create app/app.js
create app/index.html
create app/router.js

Installed packages for tooling via npm.


Installed browser packages via Bower.

Starting the Development Server


Console

Builds the project and starts a livereloading development server.

$ cd woodland
$ ember serve
Livereload server on http://localhost:49154
Serving on http://localhost:4200/

Level 2.1

Routing and Templating


Templates and the Router

Getting Started

This is the default Ember CLI


application heading. Lets change it.

Working in App
Most of your work will be done within the ember new-generated app directory.
app

Console

app.js

$ ember new
installing app
create app/app.js
create app/index.html
create app/router.js

components
index.html

Ember CLI generated several


directories with many HTML,
JavaScript, and CSS files for us.

models
router.js
routes
styles
app.css
templates
application.hbs

This listing is truncated for brevity.

Introducing Templates
Templates tell Ember what HTML to generate and display in the web browser.

This is the application template, named based on the file name.


app
app.js
components
index.html
models

app/templates/application.hbs
<h2 id="title">Welcome to Ember</h2>
{{outlet}}

router.js
routes
templates
application.hbs

Ember uses the Handlebars


templating library.

By convention, all Ember applications use an application template.

Changing the Heading


app/templates/application.hbs
<h2 id="title"> Welcome to Ember</h2>
{{outlet}}

Changing the Heading


app/templates/application.hbs
<h2 id="title"> Woodland Wanderer Whatchamacallits</h2>
{{outlet}}

The browser automatically


reloads whenever
application files change
using live reload.

Introducing Handlebars Expressions


Handlebars expressions mark locations of logic or dynamic content.

app/templates/application.hbs
<h2 id="title"></h2>
{{outlet}}

Handlebars expressions are wrapped


with curly braces (or brackets) and
sometimes called mustaches.

This is an outlet expression. It acts as a placeholder and


tells Ember where to place other templates on the page.

Layering Templates with {{outlet}}


The application template is the outermost layer into which other templates get rendered.

app/templates/application.hbs

Rendered HTML

<h2 id="title"></h2>

<h2 id="title"></h2>

{{outlet}}

<h3>I am from user.hbs</h3>

app/templates/user.hbs
<h3>I am from user.hbs</h3>

Changing {{outlet}} Content


When the template content changes, the outlet content is updated as well.

app/templates/application.hbs

Rendered HTML

<h2 id="title"></h2>

<h2 id="title"></h2>

{{outlet}}

<h3>I am from post.hbs</h3>


<p>I have more content</p>

app/templates/post.hbs
<h3>I am from post.hbs</h3>
<p>I have more content</p>

The application template is always displayed and often


where headers, footers, and common navigation live.

Forgetting an {{outlet}}
Without an outlet in the application template, there is nowhere for other templates to go.

app/templates/application.hbs
<h2 id="title"></h2>
</h2>

app/templates/post.hbs
<h3>I am from post.hbs</h3>
<p>I have more content</p>

Bad

Rendered HTML
<h2 id="title"></h2>

Auto-generating Templates
Ember creates templates in memory automatically whenever you do not provide one.

app/templates/application.hbs
<h2 id="title">Woodland Wanderer
Whatchamacallits</h2>
{{outlet}}

Auto-generated index Template


Auto-generated Templates are empty.

Ember generates sane defaults across the


application automatically.

Rendered HTML
<h2 id="title">Woodland Wanderer
Whatchamacallits</h2>

Customizing the Index Template


The empty auto-generated index template can be overridden by using an index.hbs le.

app/templates/index.hbs
Hello from Index

An index template uses an


index.hbs file name.

Adding New Templates


Templates may be added to the application, but Ember wont know about them unless we tell it.

app/templates/orders.hbs
Hello from Orders

The index template worked because


its an Ember convention to be there.
The new orders template is not.

Introducing the Router


The router manages your application state and maps it to the path of the URL.

States in This Application


State

URL

View the menu

List and create orders

/orders

View a receipt
for Order #123

/orders/123

Customizing the Router


This is the default Ember CLI-generated router contents.
app
app.js
components
index.html
models
router.js
routes
templates

app/router.js
import Ember from 'ember';
import config from './config/environment';
const Router = Ember.Router.extend({
location: config.locationType
});
Router.map(function() {
});
export default Router;

Application endpoints are


mapped in here.

Adding a New Mapping to the Router


An orders mapping needs to be added to let Ember know about the new endpoint.

Router.map(function() {
this.route('orders', { path: '/orders' });
});

I should see the orders template when I navigate to /orders.

Revealing the Index


If the index were explicitly dened in router.js, it would look like this:

Router.map(function() {
this.route('orders', { path: '/orders' });
this.route('index', { path: '/' });
});

I should see the index template when I navigate to /.


Ember automatically maps the index for you, so
its usually omitted from the router file.

Cleaning Up With the Defaults


If the path matches the name, we can omit the path.

Router.map(function() {
this.route('orders', { path: '/orders' });
});

These are equivalent routers.

Router.map(function() {
this.route('orders');
});

Navigating Between Endpoints


Users wont often navigate by manipulating the browser location bar.

How do we link from index to orders?

Using HTML to Navigate


While you can use any HTML that you like, hard-coding links to endpoints should be avoided.

app/templates/index.hbs
Hello from Index
<a href="/orders">Orders</a>

Bad

While this works, it causes a


browser reload and loses much of
the value of Ember.

Navigating With {{link-to}}


Use the Handlebars {{link-to}} expression to navigate while avoiding page reloads.

app/templates/index.hbs

Good

Hello from Index


{{#link-to "orders"}}Orders{{/link-to}}

Router.map(function() {
this.route('orders');
});

orders is the name mapped in the


router, so its used here.

Navigating With {{link-to}}


Use the Handlebars {{link-to}} expression to navigate while avoiding page reloads.

app/templates/index.hbs
Hello from Index
{{#link-to "orders"}}Orders{{/link-to}}

Router.map(function() {
this.route('orders');
});

The href path is determined by the router.

Hello from Index


<a id="ember3" href="/orders" class="ember-view">Orders</a>

Customizing {{link-to}}
The generated anchor tag may be customized by passing additional attributes to {{link-to}}.

app/templates/index.hbs
Hello from Index
{{#link-to "orders" class="orders-link" }}Orders{{/link-to}}

The custom class is added to the generated class list.

Hello from Index


<a id="ember3" href="/orders" class="orders-link ember-view">Orders</a>

With no ID attribute defined, Ember auto-generated one.

Customizing the Generated Tag


Most Handlebars helpers accept a tagName property to change the generated HTML tag.

app/templates/index.hbs
Hello from Index
{{#link-to "orders" tagName="div" }}Orders{{/link-to}}

The tagName changed the generated anchor element to a div.

Hello from Index


<div id=ember3" class="ember-view">Orders</div>

Clicking the div still works because Ember is managing


the navigation events via link-to.

Additional Helpers
Ember ships with many other Handlebars helpers for tag generation and logic.
debugger

Stop processing to enter the debugger.

each

Iterate over a collection of objects.

if

Conditionally render sections of content.

input

Create an HTML input element.

link-to

Create an HTML anchor element.

log

Console log for simple debugging.

textarea

Create an HTML textarea element.

unless

Conditionally render sections of content.

Changing the URL


If all page changes are happening on the client side, why bother updating the URL?
1. Ember uses the URL to keep track of where the user is in the application.

aka, Application State


2. Updating the URL means that site links are shareable and the back button works.

Routing With the URL


The router writes state to and reads state from the URL.
State

URL path

index

orders

/orders

url
User activity within the
app changes the state
and updates the URL.

Changes to the URL


aect the state of the
application.
router

Level 2.2

Routing and Templating


Routes

Driving With Data


Ember is built for delivering dynamic, data-driven applications.

app/templates/index.hbs

This is all static content.

Hello from Index


{{#link-to "orders"}}Orders{{/link-to}}

How do we put dynamic


data into a template?

Recalling What We Know


Where does it get the data?

Manages state

It does not manage templates.

Denes HTML

Often from dynamic data.

Discovering a Hidden Layer


Ember has been automatically generating a hidden layer between the router and templates.

???
Manages state

Collects needed data and


renders templates

Denes HTML

Introducing Routes
Routes are responsible for collecting data and rendering the proper templates.

Router

Routes

Templates

Manages state

Collects needed data and


renders templates

Denes HTML

Auto-generated Routes
Auto-generated routes render the template of the same name.

app/router.js

app/templates/orders.hbs

Router.map(function() {
this.route('orders');
});

Hello from Orders


Auto-generated
orders route

The generated orders route renders the orders template.

Generating a Route
Ember CLI provides a generator for creating a route and updating the router.
ember generate route <route-name>
Console

This is the orders route.


$ ember generate route orders
installing route
create app/routes/orders.js
create app/templates/orders.hbs
updating router
add route orders

Adds to the router


this.route('orders');

Examining the Route


Routes are dened in app/routes with a le name matching their route name.
app/routes/orders.js
import Ember from 'ember';
export default Ember.Route.extend({
});

Using extend is creating a subclass of the


Ember.Route object.

This is the orders route.


Currently, its identical to the
auto-generated default.

Customizing the Routes Model


The model hook returns the data used in the route and its template.

app/routes/orders.js
import Ember from 'ember';
export default Ember.Route.extend({
model() {
return 'Nate';
}
});

app/templates/orders.hbs
Order for {{model}}

The routes model is available to the


template as model.

Working With an Object


The model hook may return objects.

app/routes/orders.js
import Ember from 'ember';
export default Ember.Route.extend({
model() {
return { id: '1', name: 'Nate' };
}
});

app/templates/orders.hbs
Order {{model.id}} for {{model.name}}

Here, model is an object and its properties


are accessed with {{model.property}}.

Working With Collections


The {{#each}} helper iterates over a collection and renders the block once for each item.
import Ember from 'ember';
export default Ember.Route.extend({
model() {
return [
{ id: '1', name: 'Nate' },
{ id: '2', name: 'Gregg' }
];
}
});
app/templates/orders.hbs
{{#each model as |order|}}
Order {{order.id}} for {{order.name}}<br>
{{/each}}

order is populated with one


successive order each iteration.

Customizing a Route
Routes have several hooks for customizing their behavior.

Commonly Used Route Hooks


Name

Usage

activate

Triggered when navigation enters the route.

deactivate

Triggered when navigation leaves the route.

model

Returns model data to use in the route and template.

redirect

Optionally used to send the user to a dierent route.

Linking to a Single Item


Now that were displaying the orders, we still need to link to each one.

app/templates/orders.hbs
{{#each model as |order|}}
{{#link-to ???}}
Order {{order.id}} for {{order.name}}<br>
{{/link-to}}
{{/each}}

But how do we link to a single item?


And where do we link it to?

Navigating to a Single Item


In order to link to a single item, we need a route that can load and render a single item.

show me order 1

order route,
find order 1

order template,
render order 1

/orders/1

router
State

URL

View the menu

List and create orders

/orders

View a receipt
for Order #

/orders/###

This route doesnt exist yet, but its


functionality is described above.

order route

order template

Dening Dynamic Segments in the Router


Dynamic segments are a part of the URL path that holds variable data, like identiers.

show me order 1

order route,
find order 1

order template,
render order 1

/orders/1

router

order route

app/router.js
Router.map(function() {
this.route('orders');
this.route('order', { path: '/orders/:order_id' });
});

Dynamic segments start with a colon.

order template

Dening Dynamic Segments in the Router


Dynamic segments are a part of the URL path that holds variable data, like identiers.

show me order 1

order route,
find order 1

order template,
render order 1

/orders/1

router

order route

order template

app/router.js
Router.map(function() {
this.route('orders');
this.route('order', { path: '/orders/:order_id' });
});

/orders/1

Dening Dynamic Segments in the Router


Dynamic segments are a part of the URL path that holds variable data, like identiers.

show me order 1

order route,
find order 1

order template,
render order 1

/orders/1

router

order route

order template

app/router.js
Router.map(function() {
this.route('orders');
this.route('order', { path: '/orders/:order_id' });
});

/orders does not match /orders/1.

/orders/1

Dening Dynamic Segments in the Router


Dynamic segments are a part of the URL path that holds variable data, like identiers.

show me order 1

order route,
find order 1

order template,
render order 1

/orders/1

router

order route

order template

app/router.js
Router.map(function() {
this.route('orders');
this.route('order', { path: '/orders/:order_id' });
});

/orders/1

Dening Dynamic Segments in the Router


Dynamic segments are a part of the URL path that holds variable data, like identiers.

show me order 1

order route,
find order 1

order template,
render order 1

/orders/1

router

order route

order template

app/router.js
Router.map(function() {
this.route('orders');
this.route('order', { path: '/orders/:order_id' });
});

/orders/:order_id matches /orders/1


because dynamic segments are placeholders.

/orders/1
/orders/
1

Dening Dynamic Segments in the Router


Dynamic segments are a part of the URL path that holds variable data, like identiers.

show me order 1

order route,
find order 1

order template,
render order 1

/orders/1

router

order route

order template

app/router.js
Router.map(function() {
this.route('orders');
this.route('order', { path: '/orders/:order_id' });
});

/orders/ :order_id
/orders/1

Dynamic portion is extracted.

Dening Dynamic Segments in the Router


Dynamic segments are a part of the URL path that holds variable data, like identiers.

show me order 1

order route,
find order 1

order template,
render order 1

/orders/1

router

order route

order template

app/router.js
Router.map(function() {
this.route('orders');
this.route('order', { path: '/orders/:order_id' });
});

:order_id
1

Dening Dynamic Segments in the Router


Dynamic segments are a part of the URL path that holds variable data, like identiers.

show me order 1

order route,
find order 1

order template,
render order 1

/orders/1

router

order route

order template

app/router.js
Router.map(function() {
this.route('orders');
this.route('order', { path: '/orders/:order_id' });
});

{.:order_id 1}

Dening Dynamic Segments in the Router


Dynamic segments are a part of the URL path that holds variable data, like identiers.

show me order 1

order route,
find order 1

order template,
render order 1

/orders/1

?
router

order route

order template

app/router.js
Router.map(function() {
this.route('orders');
this.route('order', { path: '/orders/:order_id' });
});

{order_id:
1}}
{.order_id: 1

Data sent to the


route

Dening Dynamic Segments in the Router


Dynamic segments are a part of the URL path that holds variable data, like identiers.

show me order 1

order route,
find order 1

order template,
render order 1

/orders/1

{order_id: 1}

router

order route

order template

app/router.js
Router.map(function() {
this.route('orders');
this.route('order', { path: '/orders/:order_id' });
});

{.order_id: 1}

Data sent to the


route

Using the Dynamic Segment Values


The router passes the dynamic segment values to the routes model hook.

show me order 1

order route,
find order 1

order template,
render order 1

/orders/1

{order_id: 1}

{id: '1', }

router

order route

order template

app/routes/order.js
export default Ember.Route.extend({
model(params) {
return [
{ id: '1', name: 'Nate' },
{ id: '2', name: 'Gregg' }
].findBy('id', params.order_id);
}
});

{order_id: 1}

findBy(property, value) is
provided by Ember and returns
the first matching item.

Displaying the Order


The order template displays the given model data as weve done before.

show me order 1

order route,
find order 1

order template,
render order 1

/orders/1

{order_id: 1}

{id: '1', }

router

order route

app/routes/order.hbs
<p>Order {{model.id}} for {{model.name}}</p>
<p>The order is ready!</p>

order template

Linking to a Single Item


Now that we have a route to link to, how do we use it?

app/templates/orders.hbs
{{#each model as |order|}}
{{#link-to ???}}
Order {{order.id}} for {{order.name}}<br>
{{/link-to}}
{{/each}}

Linking to a Single Item


The {{#link-to}} helper accepts one or more objects to populate the dynamic segments.

order route name

order object

app/templates/orders.hbs
{{#each model as |order|}}
{{#link-to "order" order}}
Order {{order.id}} for {{order.name}}<br>
{{/link-to}}
{{/each}}

{{#link-to "<route-name>" <object-to-view>}}

This links to the order


route and passes the order
that wed like to view.

Linking to Objects With Their ID


The {{link-to}} helper automatically uses the given objects ID as the dynamic segment value.

app/templates/orders.hbs
{{#each model as |order|}}
{{#link-to "order" order}}
Order {{order.id}} for {{order.name}}<br>
{{/link-to}}
{{/each}}

Rendered HTML
<a href="/orders/1">Order 1 for Nate<br></a>
<a href="/orders/2">Order 2 for Gregg<br></a>

Automatically set from the ID property of the order instance.

Navigating to an Order

Viewing an order detail loses the order listing.

Visualizing the Current Structure


The at route mappings create a at hierarchy where each route replaces the other.

Router.map(function() {
this.route('orders');
this.route('order', { path: '/orders/:order_id' });
});
application.hbs
page header

index.hbs

page content
{{outlet}}

orders.hbs

page footer

order.hbs

Visualizing the Current Structure


Navigating to the root path renders the index template into the application outlet.

Router.map(function() {
this.route('orders');
this.route('order', { path: '/orders/:order_id' });
});

/
URL path

Route name index

application.hbs
page header

index.hbs

page content
{{outlet}}

orders.hbs

page footer

order.hbs

Route

index.js

Template

index.hbs

Visualizing the Current Structure


Navigating to the /orders path replaces the index template with the rendered orders template.

Router.map(function() {
this.route('orders');
this.route('order', { path: '/orders/:order_id' });
});

/orders
URL path

/orders

Route name orders

application.hbs
page header

index.hbs

page content
{{outlet}}

orders.hbs

page footer

order.hbs

Route

orders.js

Template

orders.hbs

Visualizing the Current Structure


Navigating to the /orders/1 path replaces the orders template with the order template.

Router.map(function() {
this.route('orders');
this.route('order', { path: '/orders/:order_id' });
});

/orders/1
URL path

/orders/1

Route name order

application.hbs
page header

index.hbs

page content
{{outlet}}

orders.hbs

page footer

order.hbs

The order template replaced the orders list!

Route

order.js

Template

order.hbs

Nesting the Order Routes


A nested mapping allows multiple routes and templates to be displayed.

Router.map(function() {
this.route('orders', function() {
this.route('order', { path: '/:order_id' });
});
});

Adding an anonymous
function defines a parent route.

application.hbs
page header

index.hbs

page content
{{outlet}}

orders.hbs

page footer

list

order detail
{{outlet}}

orders/index.hbs
orders/order.hbs

Just like application, the orders template needs an {{outlet}} now!

Nesting the Order Routes


Navigating to the root path renders the index template into the application outlet.

Router.map(function() {
this.route('orders', function() {
this.route('order', { path: '/:order_id' });
});
});
application.hbs
page header

index.hbs

page content
{{outlet}}

orders.hbs

page footer

list

order detail
{{outlet}}

/
URL path

Route name index


Route

index.js

Template

index.hbs

The same as before.


orders/index.hbs
orders/order.hbs

Nesting the Order Routes


A nested mapping allows multiple routes and templates to be displayed.

Router.map(function() {
this.route('orders', function() {
this.route('order', { path: '/:order_id' });
});
});
application.hbs
page header

index.hbs

page content
{{outlet}}

orders.hbs

page footer

list

order detail
{{outlet}}

/orders
URL path

/orders

Route name orders.index


Route

orders/index.js

Template

orders/index.hbs

orders/index.hbs
orders/order.hbs

A new orders.index
route is automatically
created.

Index routes are always created for parent routes.


Theyre whats rendered when you go to the parent.

Nesting the Order Routes


A nested mapping allows multiple routes and templates to be displayed.

Router.map(function() {
this.route('orders', function() {
this.route('order', { path: '/:order_id' });
});
});
application.hbs
page header

index.hbs

page content
{{outlet}}

orders.hbs

page footer

list

order detail
{{outlet}}

/orders/1
URL path

/orders/1

Route name orders.order


Route

orders/order.js

Template

orders/order.hbs

orders/index.hbs
orders/order.hbs

An orders
namespace is added
to the order route.

Fixing Broken Links


Nesting introduced the orders. route namespace. Links need to get updated to match.

app/templates/orders.hbs
{{#each model as |order|}}
{{#link-to "orders.order" order}}
Order {{order.id}} for {{order.name}}<br>
{{/link-to}}
{{/each}}

Change order to orders.order.

{{outlet}}

Add the {{outlet}} so child templates may be displayed.

Relocating Nested Files


Nesting introduced an orders/ directory namespace. Files must move to match.
app
routes
order.js
orders.js
templates
order.hbs
orders.hbs

Relocating Nested Files


Nesting introduced an orders/ directory namespace. Files must move to match.
app
routes
orders
order.js
orders.js
templates
orders
order.hbs
orders.hbs

Relocating Nested Files


Nesting introduced an orders/ directory namespace. Files must move to match.
app
routes
orders
order.js
orders.js
templates
orders
order.hbs
orders.hbs

Retaining the Listing

Level 3

Models and Services

Organizing the Data


The same data structures are being used across the app and should be centralized.

app/routes/orders.js

app/routes/order.js

export default Ember.Route.extend({


model() {
return [
{ id: '1', name: 'Nate' },
{ id: '2', name: 'Gregg' }
];
}
});

export default Ember.Route.extend({


model(params) {
return [
{ id: '1', name: 'Nate' },
{ id: '2', name: 'Gregg' }
].findBy('id', params.order_id);
}
});

We need product and order data throughout the app.


Duplicating it everywhere makes it difficult to keep in sync.

Generating a Service
Services are long-living objects (aka, singletons) that are available throughout your app.

ember generate service <service-name>

Console
$ ember generate service store
installing service
create app/services/store.js

Services are good for:


Centralized logging
User sessions
WebSocket management
Data repositories

Well store our data in a


data repository service.

Dening a Service
Services are dened in app/services and extend Ember.Service.

The file name matches the service name (store).


app/services/store.js
import Ember from 'ember';
export default Ember.Service.extend({
});

This was generated from ember generate.

Centralizing the Data


With a service in place, the shared data can be moved from the routes.

app/services/store.js

app/routes/orders.js

import Ember from 'ember';

export default Ember.Route.extend({


model() {
return [
{ id: '1', name: 'Nate' },
{ id: '2', name: 'Gregg' }
];
}
});

export default Ember.Service.extend({


});

Centralizing the Data


With a service in place, the shared data can be moved from the routes.

app/services/store.js

app/routes/orders.js

import Ember from 'ember';

export default Ember.Route.extend({


model() {

export default Ember.Service.extend({


getOrders() {
return [
{ id: '1', name: 'Nate' },
{ id: '2', name: 'Gregg' }
];
}.
});

New function to return the data

}
});

Injecting the Service


Service objects are made available within another object using Ember.inject.service().

app/services/store.js

app/routes/orders.js

import Ember from 'ember';

export default Ember.Route.extend({


model() {
const store = this.get('store');
return store.getOrders();
},

export default Ember.Service.extend({


getOrders() {
return [
{ id: '1', name: 'Nate' },
{ id: '2', name: 'Gregg' }
];
}.
});

store: Ember.inject.service('store')
});

The local name of


the service

The name of the


service to inject

After injection, the store service becomes available as the store property.

Injecting the Service


Service objects are made available within another object using Ember.inject.service().

app/services/store.js

app/routes/orders.js

import Ember from 'ember';

export default Ember.Route.extend({


model() {
const store = this.get('store');
return store.getOrders();
},

export default Ember.Service.extend({


getOrders() {
return [
{ id: '1', name: 'Nate' },
{ id: '2', name: 'Gregg' }
];
}.
});

store: Ember.inject.service()
});

Because the service name matches the


local property name, we can leave it off.

Centralize the Data Filtering


Now that the data is in the service, the service can be used to nd and lter the app data.

app/services/store.js

app/routes/orders/order.js

import Ember from 'ember';

export default Ember.Route.extend({


model(params) {
return [
{ id: '1', name: 'Nate' },
{ id: '2', name: 'Gregg' }
].findBy('id', params.order_id);
}/
});

export default Ember.Service.extend({


getOrders() {
return [
{ id: '1', name: 'Nate' },
{ id: '2', name: 'Gregg' }
];
}.
});

Centralize the Data Filtering


Now that the data is in the service, the service can be used to nd and lter the app data.

app/services/store.js

app/routes/orders/order.js

import Ember from 'ember';

export default Ember.Route.extend({


model(params) {
return [
{ id: '1', name: 'Nate' },
{ id: '2', name: 'Gregg' }
].findBy('id', params.order_id);
}/
});

export default Ember.Service.extend({


getOrderById(id) {
const orders = this.getOrders();
return orders.findBy('id', id);
},
getOrders() { /* */ }
});

Centralize the Data Filtering


Now that the data is in the service, the service can be used to nd and lter the app data.

app/services/store.js

app/routes/orders/order.js

import Ember from 'ember';

export default Ember.Route.extend({


model(params) {
const id = params.order_id;
const store = this.get('store');
return store.getOrderById(id);
},

export default Ember.Service.extend({


getOrderById(id) {
const orders = this.getOrders();
return orders.findBy('id', id);
},
getOrders() { /* */ }
});

store: Ember.inject.service()
});

Everything Still Works!

Filling Out the Data


Now that the data is centralized into the store service, we can replace the placeholder data.

Filling Out the Product Data


From the menu page, we see that products have four properties.

2
3

4
Product
1. title
2. price
3. description
4. imageUrl

Filling Out the Order Data


From the orders page, we see that orders have two properties.

1
2
Product
1. title
2. price
3. description
4. imageUrl

Order
1. name
2. line items

Filling Out the LineItem Data


From the orders page, we see that LineItems have two properties.

1
Product
1. title
2. price
3. description
4. imageUrl

Order
1. name
2. line items

Line Item
1. product
2. quantity

Formalizing the Data


Weve got three distinct types of objects. Lets start formalizing them using Ember.

Product
1. title
2. price
3. description
4. imageUrl

Order
1. name
2. line items

Line Item
1. product
2. quantity

Well move away from plain, simple JavaScript


placeholder objects and formalize them using Ember.

Introducing Models
Models represent the underlying (and sometimes persisted) data of the application.

Models are defined in app/models.


app/models/product.js
import Ember from 'ember';

Product
1. title
2. price
3. description
4. imageUrl

export default Ember.Object.extend({


});

This defines the product model as a subclass of Ember.Object.


But why extend from Ember.Object?

Extending from Ember.Object


Ember.Object is the base of many of Embers objects (including Ember.Route, for example).

app/models/product.js
import Ember from 'ember';

Product
1. title
2. price
3. description
4. imageUrl

export default Ember.Object.extend({


});
Ember.Object provides:
1. A consistent interface for creating and destroying records
2. Object lifecycle events and hooks
3. Properties and property observation functionality

This is how templates are updated


when properties change.

Interacting With Models


Properties are read and set using get() and set().

app/models/product.js
import Ember from 'ember';
export default Ember.Object.extend({
});

Ember.Object provides
create(). Record properties
may optionally be passed
in at creation.

var product = Product.create({


title: 'Sleeping Bag'
});
product.get('title') //=> 'Sleeping Bag'
product.set('title', 'Matches')
product.get('title') //=> 'Matches'

Creating the Remaining Models


For now, all three models will be identical, empty Ember.Object Models.

This will change later in the course.


app/models/product.js

app/models/line-item.js

import Ember from 'ember';

import Ember from 'ember';

export default Ember.Object.extend({


});

export default Ember.Object.extend({


});

app/models/order.js
import Ember from 'ember';
export default Ember.Object.extend({
});

Using the Ember.Object Models


With basic product, order, and LineItem models dened, lets use them in the store service.

app/services/store.js
import Ember from 'ember';
export default Ember.Service.extend({
getOrderById(id) { /* */ },
getOrders() { /* */ }
});

Importing the Models With Relative Paths


The new models must be imported into the store to make them available for use.

app/services/store.js
import
import
import
import

Ember from 'ember';


LineItem from '../models/line-item';
Order from '../models/order';
Product from '../models/product';

export default Ember.Service.extend({


getOrderById(id) { /* */ },
getOrders() { /* */ }
});

app/
!"" models/
# !"" line-item.js
# !"" order.js
# $"" product.js
$"" services/
$"" store.js

import can be used with relative file path references.


These change when the importing file moves, however.

Importing the Models With Project Paths


The import statement may instead be used with app name-based paths.

app/services/store.js
import
import
import
import

Ember from 'ember';


LineItem from 'woodland /models/line-item';
Order from 'woodland/models/order';
Product from 'woodland/models/product';

export default Ember.Service.extend({


getOrderById(id) { /* */ },
getOrders() { /* */ }
});

$ ember new woodland

app/
!"" models/
# !"" line-item.js
# !"" order.js
# $"" product.js
$"" services/
$"" store.js

woodland was the app name we defined


with ember new, back in Level 1.

Dening the Product Records


With the product model available, lets create the available product records for the app.

app/services/store.js

Product
1. title
2. price
3. description
4. imageUrl

import
import
import
import

Ember from 'ember';


LineItem from 'woodland /models/line-item';
Order from 'woodland/models/order';
Product from 'woodland/models/product';

export default Ember.Service.extend({


getOrderById(id) { /* */ },
getOrders() { /* */ }
});

Dening the Product Records


Product.create() is used to create all four records and hold them in an array.

app/services/store.js
import Product from 'woodland/models/product';

Product
1. title
2. price
3. description
4. imageUrl
getProducts() returns the
product record array to the
caller.

const products = [
Product.create({title:
Product.create({title:
Product.create({title:
Product.create({title:
];

'Tent', price: 10, descript}),


'Sleeping', price: 5, desc}),
'Flashlig', price: 2, desc}),
'First-Ai', price: 3, desc})

export default Ember.Service.extend({


getOrderById(id) { /* */ },
getOrders() { /* */ },
getProducts() { return products; }
});

Dening the Order Records


Order.create() is used to create order records for the listings in an array.

app/services/store.js

Some content is hidden for brevity.

import LineItem from 'woodland/models/line-item';


import Order from 'woodland/models/order';

Order
1. name
2. line items

Line Item
1. product
2. quantity

const orders = [
Order.create({ id: '1234', name: 'Blaise
items: [
LineItem.create({product: products[0],
LineItem.create({product: products[1],
LineItem.create({product: products[2],
LineItem.create({product: products[3],
]
}),

];

Blobfish',
quantity:
quantity:
quantity:
quantity:

1}),
1}),
0}),
0})

Dening the Order Records


The store service is updated to source its order data from the orders array.

app/services/store.js

Some content is hidden for brevity.

import LineItem from 'woodland/models/line-item';


import Order from 'woodland/models/order';
import Product from 'woodland/models/product';
const products = [];
const orders = [];

Order
Line Item
1. name
1. product
2. line items 2. quantity

export default Ember.Service.extend({


getOrderById(id) { return orders.findBy('id', id); },
getOrders() { return orders; },
getProducts() { return products; }
});

Adding the Design Assets


The static HTML structure goes into the appropriate templates.

app/templates/index.hbs
<div class="card">
<h1>Order Today!</h1>
<p class="card-content">Our online store helps</p>
{{#link-to "orders"}}Order Today!{{/link-to}}
</div>
<div class="grid group">
{{#each model as |product|}}
<div class="product-media">
<img src="{{product.imageUrl}}" />
</div>
<div class="product-content">
<h2>{{product.title}}: <b>${{product.price}}</b>

Adding the Design Assets


The designs images go into a new app/public/assets/images directory that we create.
app
public
assets
images
bag.svg
light.svg
kit.svg
tent.svg
styles
app.css

Adding the Design Assets


The designs CSS go into the app/styles/app.css that was generated earlier by Ember CLI.
app
styles
app.css

app/styles/app.css
html {
background-color: #ebe9df;
color: #726157;
font-family: 'Montserrat', sans-serif;
font-size: 16px;
line-height: 1.5;
}
body {
min-height: 100%;
}
/* */

The Ember CLI-generated index.html already includes app.css.

Displaying the design

Level 4

Actions

Missing the Interaction


The app navigates and displays products and orders, but orders cannot yet be created.

We need
something from
which to populate
the order form.

Well start by
initializing a
new, empty
order record.

This doesnt yet work.

Creating a New Order Record


To manage a new orders information, we need an empty order record to work with.
app/services/store.js
export default Ember.Service.extend({
getOrderById(id) { /* */ },
getOrders() { /* */ },
getProducts() { /* */ },
newOrder() {
return Order.create({
items: products.map((product) => {
return LineItem.create({
product: product
});
})
});
}
});

newOrder() returns a new


Order record with one
LineItem record per Product.
The arrow function expression
is an ES2015 feature. Its used
here as a shorthand for
function(product) { }.

Using the New Order Record


The orders.index route is created and uses the new order record.

app/routes/orders/index.js
import Ember from 'ember';
export default Ember.Route.extend({
model() {
const store = this.get('store');
return store.newOrder();
},
store: Ember.inject.service()
});

The order form is on the orders/index


template, so the orders.index route will
use the new order record for its model.

Starting With the Static Form


The designed, static form is added with the addition of the {{#each}} expression and product info.

app/templates/orders/index.hbs
<form>
<label for="name">Name</label>
<input type="text" id="name" >
{{#each model.items as |item|}}
<label>
{{item.product.title}} @
<input type="number" min="0">
</label>
{{/each}}
<input type="submit" type="Order">
</form>

This needs to be bound


to the order name.

This needs to be bound to


the LineItem quantity.
The browser will try to
submit the form to the
server, bypassing Ember.

Binding Properties With Input


Ember provides {{input}} helpers, which keep bound properties and input elds in sync.

app/templates/orders/index.hbs
<form>
<label for="name">Name</label>
{{input type="text" id="name" value=model.name}}
{{#each model.items as |item|}}
<label>
{{item.product.title}} @
{{input type="number" min="0" value=item.quantity}}
</label>
{{/each}}
<input type="submit" type="Order">
</form>

The {{input}} helper


accepts the same
properties as an input
element.

No quotes around the


property gives the
helper direct access to
the property for
manipulation.

Handling the Submission Event


Somehow we need to intercept and handle the form submission event to create the order.

How do we
intercept the
submission?

This doesnt yet work.

Introducing Actions
Actions map generic DOM events to specic application activities and functions.

DOM events

application domain
map to

event

actionName

click

expandArticle

keyup
submit

autoCompleteSearch
createOrder

This is what well use to


capture the form
submission.

Intercepting the Submit Event


How do we intercept the FORMs submit event using an Ember action?

app/templates/orders/index.hbs
<form>
<!-- -->
<button type="submit">Order</button>
</form>

Forms can be submitted through a


submit button, ENTER in an input,
or a JavaScript form.submit() call.

Mapping an Action
Actions are mapped in templates using the {{action}} helper, dened on the element to watch.

{{action "actionName" on="eventName"}}

app/templates/orders/index.hbs
<form>
<!-- -->
<button type="submit">Order</button>
</form>

The event to trigger on


defaults to click.

Mapping an Action
Actions are mapped in templates using the {{action}} helper, dened on the element to watch.

{{action "actionName" on="eventName"}}

app/templates/orders/index.hbs
<form {{action "createOrder" model on="submit"}}>
<!-- -->
<button type="submit">Order</button>
</form>

This fires the


createOrder action.

The action triggers


when the form emits a
submit event.

Any extra parameters are passed to


the triggered action as parameters.

By default, actions prevent the browser default activity (preventDefault).

Handling Actions
Action handlers are functions dened in an actions block on the route or its parents.

{{action "createOrder" model on="submit"}}

app/routes/orders/index.js
export default Ember.Route.extend({
actions: {
createOrder(order) {
const name = order.get('name');
alert(name + ' order saved!');
}
},
model() { return this.get('store').newOrder(); }
});

1. The new order record


populates the template as
the model.
2. The triggered action
passes the new order
record to the action.

Alerting on the Action


The form submission is intercepted by the action and handled by the routes action handler.

But we need to show the


order receipt, not an alert!

Transitioning to a New Route


Routes have a transitionTo function used to navigate to other routes.
app/routes/orders/index.js
export default Ember.Route.extend({
actions: {
createOrder(order) {
this.get('store').saveOrder(order);
this.transitionTo('orders.order', order);
}
},
/* */
});

Target Route name.

Optional model parameters.

This is similar to the Templates link-to helper.

Saving the Order in the Store


Here, new orders are saved by giving them an ID and adding them to the orders collection.
app/services/store.js
export default Ember.Service.extend({
getOrderById(id) { /* */ },
getOrders() { /* */ },
getProducts() { /* */ },
newOrder() {/* */ },
saveOrder(order) {
order.set('id', 9999);
orders.pushObject(order);
}
});

pushObject comes from Ember. Its like


push, but triggers value-changed events.

Setting an ID to pretend
that the order was saved.

Transitioning to the Receipt


The order is submitted and saved, and a receipt is now correctly displayed.

The pushObject added the new


order to the orders list!

Ordering in Bulk
To nish the order form, the design calls for a button to increment item quantities by 10.

app/templates/orders/index.hbs
<!-- -->
{{#each model.items as |item|}}
<label>
{{item.product.title}} @
${{item.product.price}}/ea
{{input type="number" min="0" value=item.quantity}}
<button {{action "addToItemQuantity" item 10}}>+10</button>
</label>
{{/each}}
<!-- -->

This {{action}} is using the


default on=click trigger.

Passing two arguments to the action to


make this action more reusable: the item
and the amount to increment.

Incrementing Property Values


Ember.Object provides incrementProperty and decrementProperty to quickly change numerics.

app/routes/orders/index.js
export default Ember.Route.extend({
actions: {
addToItemQuantity(lineItem, amount) {
lineItem.incrementProperty('quantity', amount);
},
createOrder(order) { /* */ }
},
model() { /* */ }
});

lineItem and amount


values came from the
{{action}} arguments.
incrementProperty
increases the property
value by the amount
given, or 1 by default.

Ordering Complete
All of the functionality for adding a new order is now in the system.

Level 5.1

Properties and Components


Computed Properties

Finishing O the Receipt


The app is nearly complete there are just a few pieces left to implement on the order receipt.

Calculate
LineItem costs
Calculate and
style the cost
percentages

Missing
total price

Cleaning Up the Receipt


Reaching through one object to work with another is bad (the Law of Demeter).

What we want
app/templates/orders/order.hbs

app/templates/orders/order.hbs

{{#each model.items as |lineItem|}}


<tr>
<td>{{lineItem.product.title}}</td>
<td>{{lineItem.quantity}}</td>
<td>{{lineItem.product.price}}</td>
<td>$XX</td>
<td>XX%</td>
</tr>
{{/each}}

{{#each model.items as |lineItem|}}


<tr>
<td>{{lineItem.title}}</td>
<td>{{lineItem.quantity}}</td>
<td>{{lineItem.unitPrice}}</td>
<td>$XX</td>
<td>XX%</td>
</tr>
{{/each}}

The Law of Demeter is the principle of least knowledge.


Units should only talk to their immediate friends.

Introducing Computed Properties


Computed properties are function-calculated, cached properties.
this.get('title')

app/models/line-item.js
import Ember from 'ember';
export default Ember.Object.extend({
title: Ember.computed('product.title', function() {
return this.get('product.title');
})
});

The functions return value is cached


as the LineItems title property value.

Like all properties,


computed properties are
queried with get().

Dependent properties of the function


are listed. When their value changes,
the property is recalculated.

Using Predened Macros


Ember ships with about 30 predened computed property macros.
Macros
app/models/line-item.js
import Ember from 'ember';
export default Ember.Object.extend({
title: Ember.computed('product.title', function() {
return this.get('product.title');
})
});

These are largely equivalent.


title: Ember.computed.alias('product.title')

alias
collect
empty
equal
lterBy
mapBy
sort
sum
uniq

These are just a


few of the many
macros provided.

Adding the Unit Price


The unit price is just another property alias.

app/models/line-item.js
import Ember from 'ember';
export default Ember.Object.extend({
title: Ember.computed.alias('product.title' ),
unitPrice: Ember.computed.alias('product.price')
});

The local property name (unitPrice) doesnt have


to match the aliased property name (price).

Using the Aliases


The template can now be updated to use the lineItem title and unitPrice.

app/templates/orders/order.hbs
{{#each model.items as |lineItem|}}
<tr>
<td>{{lineItem.title}}</td>
<td>{{lineItem.quantity}}</td>
<td>{{lineItem.unitPrice}}</td>
<td>$XX</td>
<td>XX%</td>
</tr>
{{/each}}

Determining the LineItem Price


The LineItem cost is the quantity multiplied by the unitPrice.

$10

$5

$0

$0

Calculating the LineItem Price


The LineItem cost is the quantity multiplied by the unitPrice.

app/models/line-item.js
import Ember from 'ember';
export default Ember.Object.extend({
price: Ember.computed('quantity', 'unitPrice', function() {
return parseInt(this.get('quantity'), 10) * this.get('unitPrice');
}),
title: Ember.computed.alias('product.title'),
unitPrice: Ember.computed.alias('product.price')
});
parseInt() is used because quantity is getting set from a form input. Form inputs return
string values. Adding the base 10 indicator is just a good practice.

Using the Computed Price


With the LineItem price calculated, we can add it to the template.

app/templates/orders/order.hbs
{{#each model.items as |lineItem|}}
<tr>
<td>{{lineItem.title}}</td>
<td>{{lineItem.quantity}}</td>
<td>{{lineItem.unitPrice}}</td>
<td>${{lineItem.price}}</td>
<td>XX%</td>
</tr>
{{/each}}

Progressing Through the Receipt


The item title, quantity, unitPrice, and cost are now on the receipt.

Calculate and
style the cost
percentages
Missing
total price

Determining the Order Price


The order price should be the sum of the orders LineItem prices.

$10
+

=$15

=$15

=$15

$15

Collecting the LineItem prices


The mapBy macro creates a new array containing mapped properties.
Macros
alias

app/models/order.js

collect

import Ember from 'ember';

empty

export default Ember.Object.extend({


itemPrices: Ember.computed.mapBy('items', 'price')
});

This will be an array of


LineItem prices.

Name of the
collection to use

The element
property to map

this.get('itemPrices'); //=> [10, 5, 0, 0]

Now we need to sum the LineItem prices.

equal
lterBy
mapBy
sort
sum
uniq

The mapped
array will
automatically
update if the
items array
changes or its
price values
change.

Performing the Summation


The sum macro calculates a summation value from an array of numerics.
Macros
alias

app/models/order.js

collect

import Ember from 'ember';

empty

export default Ember.Object.extend({


itemPrices: Ember.computed.mapBy('items', 'price')
),
price: Ember.computed.sum('itemPrices')
});

This will contain the order price.

The collection to sum

equal
lterBy
mapBy
sort
sum
uniq

this.get('price'); //=> 15

The calculated sum will automatically update


itemPrices array changes.

Exercising the Macros


The macros and computed properties are all working as expected.

Level 5.2

Properties and Components


Components

Populating the Cost Percentages


The percentages need to be implemented and styled based on their values.

/$10

=0%

/$10

=50%

/$10

=20%

/$10

=30%

Make bold when


at or above 50%.

Considering the Cost Percentage


The LineItem cost percentage has a dynamic presentation and its isolated.

1. The presentation changes if its at or above 50% (its bolded)


2. The value isnt used anywhere else in the app
3. If a user clicks on the percentage, itd be nice to show how it is
calculated

Introducing Components
Components are a reusable way to combine a template with action handling and behavior.

Component

Template

Actions

Used for:
Charts
Tabs
Tree widgets
Buttons with conrmations
Date range selectors
Wrap libraries and services

Components follow closely


with the W3C Custom
Elements spec.

http://w3c.github.io/webcomponents/spec/custom/

{{link-to}} and
{{input}} are
components!

Generating a Component
Ember CLI provides a generator for creating a component and its template.

ember generate component <component-name>

Console
$ ember generate component item-percentage
installing component
create app/components/item-percentage.js
create app/templates/components/item-percentage.hbs

The component file

The components template

Creating the Component


Components are dened in app/components and extend Ember.Component.

app/components/item-percentage.js
import Ember from 'ember';
export default Ember.Component.extend({
});

Components must have at least one


hyphen in their name. This conforms
with the Custom Elements spec.

Dening the Component Logic


Components have their own properties, functions, and behaviors.

app/components/item-percentage.js
import Ember from 'ember';
export default Ember.Component.extend({
percentage: Ember.computed('itemPrice', 'orderPrice', function() {
return this.get('itemPrice') / this.get('orderPrice') * 100;
})
});

Well provide the itemPrice and orderPrice


values from the calling template.

Customizing the Components Template


Component templates are dened in app/templates/components.

app/templates/components/item-percentage.hbs
<span>
{{percentage}}%
</span>

The template name matches


the component name.

Rendering a Component From a Template


Components are called by name and may be passed additional data.

app/templates/orders/order.hbs
{{#each model.items as |lineItem|}}
<tr>
<td>{{lineItem.title}}</td>
<td>{{lineItem.quantity}}</td>
<td>{{lineItem.unitPrice}}</td>
<td>${{lineItem.price}}</td>
<td>
{{item-percentage itemPrice=lineItem.price orderPrice=model.price}}
</td>
</tr>
{{/each}}

This renders the item-percentage


component into this location.

This passes the item and order


prices to the component.

Viewing the Un-styled Percentage


The correct percentage is calculated and displayed, but the style does not yet change.

But were
missing the
bold styling.

Adding an isImportant Property


Using the percentage value, the component determines if its an important (high) value.

app/components/item-percentage.js
import Ember from 'ember';
export default Ember.Component.extend({
isImportant: Ember.computed.gte('percentage', 50),
percentage: Ember.computed('itemPrice', 'orderPrice', function() {
return this.get('itemPrice') / this.get('orderPrice') * 100;
})
});

The greater than or equal to (gte) macro


returns true if the property value is
greater than or equal to the value given.

Adding the Template Styling


Use the Handlebars {{if}} helper to dynamically set or unset an important class.

app/templates/components/item-percentage.hbs
<span class="{{if isImportant 'important'}}">
{{percentage}}%
</span>

The span gets an important class if the


components isImportant property is true.

Viewing the Dynamic Styling


The percentages are now dynamically styled based on their value.

How about
clicking to show
the details?

Adding a Component Action


Well handle a toggleDetails action to toggle an isShowingDetails component property.

app/components/item-percentage.js
import Ember from 'ember';
export default Ember.Component.extend({
actions: {
toggleDetails() {
this.toggleProperty('showDetails');
}
},

toggleProperty() switches a
propertys value between true
and false each time it is called.

isImportant: Ember.computed.gte('percentage', 50),


percentage:
});

Actions are handled in a components actions block, just like routes.

Mapping the toggleDetails Action


Clicking on the span should toggle the details, so the action is mapped on the span element.

app/templates/components/item-percentage.hbs
<span class="{{if isImportant 'important'}}" {{action "toggleDetails"}}>
{{percentage}}%
</span>

This action is using the default


on=click trigger.

Conditionally Showing Details


The components template is now dynamically lled with either percentage or detail contents.

app/templates/components/item-percentage.hbs
<span class="{{if isImportant 'important'}}" {{action "toggleDetails"}}>
{{#if showDetails}}
${{itemPrice}} / ${{orderPrice}}
{{else}}
{{percentage}}%
{{/if}}
</span>

Application Complete!

Você também pode gostar