Front End Workshops EmberJS - In Depth Marc Torrent & Mario García [email protected] [email protected]
Front End WorkshopsEmberJS - In Depth
Marc Torrent & Mario García
[email protected]@visual-engin.com
In previous workshops of EmberJS...
Remember theBIG PICTURE
ember-cli Router
Routesapplication.js
Models
Templates
Components
Ember-DataControllers
SERVICES
Ember Data - Adapters & Serializers
Ember Data
ApplicationRoute/Controller/Component
Can be thought as a read-through cache
Single store - Central repository of models in your application
Beware of cache-backend desynchronization!
Ember Data
Communication with backend
How we access dataDS.Adapter
DS.Serializer
As of Ember Data 2.0, the default Adapter and Serializer follow JSON API format.
REST API
Ember
Ember DataFormat of the data
JSON API Specification
Content-Type: application/vnd.api+json
Top level attribute “data”
Attribute names are dasherized
Attribute “type” should be pluralized
It’s about the data...
JSON API Specification
..but also about the end points!
Action HTTP Verb URL Response status*
Find GET /articles/123200 - Successful404 - Not found
Find All GET /articles200 - Successful404 - Not found
Update PATCH /articles/123200 - Successful204 - No Content
Create POST /articles201 - Created204 - No Content
Delete DELETE /articles/123200 - Successful204 - No Content
*More response statuses are possible. See the complete specification at http://jsonapi.org/format/
Adapters
Extend/build your own adapters to fit your needs!
Priority rules apply between adapters!
DS.Adapter
DS.JSONAPIAdapter
DS.RESTAdapter
How to communicate with backend
Customize your adapter (1 of 3)
// app/adapters/application.js
export default DS.JSONAPIAdapter.extend({
host: 'https://api.example.com',
namespace: 'api/v1'
});
store.findRecord('post', 1) GET request to http://api.example.com/api/v1/posts/1
Host customization - Endpoint path customization
store.findAll('person') GET request to http://api.example.com/api/v1/people
store.findAll('user-profile') GET request to http://api.example.com/api/v1/user-profiles
Customize your adapter (2 of 3)
Type path customization
// app/adapters/application.js
export default DS.JSONAPIAdapter.extend({
pathForType(type) {
return Ember.String.underscore(type);
}
});
store.findRecord('post', 1) GET request to /post/1
store.findAll('person') GET request to /person
store.findAll('user-profile') GET request to /user_profile
Headers customization
// app/adapters/application.jsexport default DS.JSONAPIAdapter.extend({ headers: { 'API_KEY': 'secret key', 'ANOTHER_HEADER': 'Some header value' }});
Customize your adapter (3 of 3)
Pluralization customization
Ember Inflector at your rescue!
Allows definition of irregular and uncountable pluralizations
Houston...
Our API REST serves at the following endpoints:➔ /formulae (instead of the regular form /formulas)➔ /advice (instead of the pluralized form /advices)
...do we have a PROBLEM?
Serializers
DS.Serializer
DS.JSONAPISerializer
DS.JSONSerializer
DS.RESTSerializer
Extend/build your own serializers to fit your needs!
Priority rules apply between serializers!
Define the format of the data
Serializing data sent to backend
Customize your serializer (1 of 4)
// JSON generated by Ember Data{ "data": { "attributes": { "name": "Product", "amount": 100, "currency": "SEK" }, "type": "product" }}
// But server expects...{ "data": { "attributes": { "name": "Product", "cost": { "amount": 100, "currency": "SEK" } }, "type": "product" }}
Change the format of the data sent to backend with the serialize() hook.
Normalizing data received from backend
Customize your serializer (2 of 4)
// Server response{ "id": "1", "body": "A comment!", "date": { "offset": "GMT+0200 (CEST)", "value": "Tue Apr 05 2016" }}
// But Ember Data expects...{ "id": "1", "body": "A comment!", "offset": "GMT+0200 (CEST)", "value": "Tue Apr 05 2016"}
Change the format of the data received from backend with the normalizeResponse() hook.
Identifier
Customize your serializer (3 of 4)
Specify your resource identifier key through primaryKey property
export default DS.JSONSerializer.extend({
primaryKey: '_id'
});
Attribute names
If the JSON payload does not match your Ember model attribute names...
// app/models/person.js
export default DS.Model.extend({
lastName: DS.attr('string')
});
// app/serializers/person.js
export default DS.JSONAPISerializer.extend({
attrs: {
lastName: 'lastNameOfPerson'
}
});
Custom transforms
Customize your serializer (4 of 4)
Use custom transforms if the server sends data that does not fit any Ember Data built-in attributes (string, number, boolean and date)
A custom transform should implement the serialize() and deserialize()
Once implemented, it can be used as a regular type of attribute
Routing and Navigation
Routing - Implicit routes
ember g route users
ember g route users/edit
applicationapplication-loading
application-error
indexindex-loading
index-error
usersusers-error
users/index
users-loading
users/edit
*All routes have its .hbs associated templates
loading
error
users/edit-error
users/edit-loading
users/index-loading
users/index-error
users/loading
users/error
Routing - Serve more than one model
Promises, promises, promises!
Ember.RSVP.hash resolves when all its promises parameters resolve
Be extra careful when using hash in the model() hook in cases where the hook is not executed*
*See http://stackoverflow.com/questions/20521967/emberjs-how-to-load-multiple-models-on-the-same-route
Alternatively, you can use route nesting to load multiple models
More info in http://emberigniter.com/load-multiple-models-single-route/
Routing - Rendering a specific template
// app/routes/posts/new.js
export default Ember.Route.extend({
renderTemplate() {
this.render('posts/form');
}
});
It is possible to render a specific template through the renderTemplate() hook.
DRY*!!!!!
*Don’t Repeat Yourself
// app/routes/posts/edit.js
export default Ember.Route.extend({
renderTemplate() {
this.render('posts/form');
}
});
Routing - Preventing and retrying transitions
Invoke transition.abort() to immediately abort the current transition
Store the transition object and re-attempt invoking transition.retry()
Every transition attempt fires the willTransition action on the current active routes, starting with the leaf-most route
Ember Router passes a transition object to various hooks on the involved routes
export default Ember.Route.extend({
beforeModel(transition) {},
model(params, transition) {},
afterModel(model, transition) {}
});
Routing - Loading routes
When navigating to /users route, Ember will try to find loading templates*:
users-loading loading or application-loading
The router pauses transitions until the promises returned from beforeModel, model and afterModel hooks fulfill
An intermediate transition into a loading substate happens immediately
The URL won’t be updated on the loading transition
*See https://github.com/emberjs/ember.js/issues/12367
A loading event will be fired on that route
Routing - Error routes
When an error occurs navigating to /users, Ember will try to find templates*:
users-error error or application-error
Analogous approach to loading substates in the case of errors
An intermediate transition into an error substate happens in case of thrown error or rejected promise
The URL won’t be updated on the error transition
*See https://github.com/emberjs/ember.js/issues/12367
An error event will be fired on that route
Routing - Query params
{{#link-to "episodes" (query-params filter="simp")}}Episodes{{/link-to}}
transitionTo('episodes', { queryParams: { filter: 'simp' }});
Query params are declared on route-driven controllers
Use controller’s queryParams property to bind query params and URL
Query params to backend
Query params in frontend
transitionTo
link-to helper
store.query('episodes', { filter: 'simp' });
Templates
➢ Data Binding in template attributes
➢ Links
➢ Actions
➢ Input Helpers
➢ Write your own helpers
Data Binding in Templates
Controller’s Properties
<div class=”_tomster”><img src={{tomster.img}}
width=100></div>
DOM Attributes
<div class=”_tomster”><input type=”checkbox”
disabled={{tomster.disabled}}><img src={{tomster.img}}
width=100></div>
Collections
{{#each cartoons as |cartoon|}} <div class=”_cartoon”>
<input type=”checkbox” disabled={{cartoon.disabled}}>
<img src={{cartoon.img}} width=100></div>{{/each}}
Links: {{#link-to }}
A helper for links in our application without the need of using anchors and href.
Router.map( function() {
this.route(‘about’);
this.route(‘cartoons’, function() {
this.route(‘cartoon’, {path: ‘/:cartoon_id’});
});
});
<ul>
{{#each model as |cartoon|}}
<li>{{#link-to "cartoons.cartoon"
cartoon}}{{cartoon.name}}{{/link-to}}
</li>
{{/each}}
</ul>
<ul>
<li><a href="/cartoon/1">Columbus Tomster</a></li>
<li><a href="/cartoon/2">LTS Tomster</a></li>
</ul>
Links: {{#link-to }} - Multiple Segments
We pass the model to the most inner nested route. The model() hook won’t be called on routes where we specify the model through the link-to.
Router.map( function() {
this.route(‘cartoons’, function() {
this.route(cartoon, {path: ‘/:cartoon_id’},
function() {
this.route(‘specs’);
this.route(‘spec’, {path: ‘/specs/:
spec_id’});
});
});
});
<ul>
{{#each cartoons as |cartoon|}}
{{#each cartoon.specs as |spec|}}
<li>{{#link-to "cartoons.cartoon.
spec" spec}}{{spec.name}}{{/link-to}}
</li>
{{/each}}
{{/each}}
</ul>
Links: The Route that went away… (I)
Be careful with the structure of the templates and routes and beware of models that you pass into the routes
Links: The Route that went away… (II)
Be careful with the structure of the templates and routes and beware of models that you pass into the routes
Keep an eye on how you distribute your outlets and the relation between index.hbs and the current template in our nested route.
Let’s see it in action!Now you tell me what should I
place in the router - routes - templates and link-to
Actions
1. Template helper for binding a DOM Event with a Controller/Route/Component.
2. It can accept parameters such as Ember.Objects, Models or basic data structures.
3. You can also specify the type of event by using the “on” option
4. By default, actions prevent the default browser action on a DOM Element. To turn down this behavior, pass the option preventDefault=false
Input Helpers {{input}} {{textarea}}
1. Template helper for <input> and <textarea> DOM Elements.
2. Forget about actions with inputs and use the the input helpers as EmberJS provides out of the box 2-way data-binding
3. Pass HTML and HTML5 attributes
4. Use it also with radio and checkboxes
Write Your Own Helpers
Custom template helpers for your own needs. Keep an eye on addons to see if someone else has already written the helper you need.
ember generate helper i18n
That file should export a function wrapped with Ember.Helper.helper()
The function receives a bundle destination as string as the first parameter (params[0]) and the key to look for as the second parameter (params[1])
To pass multiple arguments to a helper, add them as a space-separated list after the helper name
Use named Parameters to make behavior of helpers configurable
export function i18n([bundle, key], {lang,
toUpperCase}) {
let resource = dictionary[bundle][key],
translation = lang? resource[lang] :
resource[DEFAULT_LANG];
return toUpperCase? translation.
toUpperCase() : translation;
}
export default Ember.Helper.helper(i18n);
<p>{{i18n "catalog" "category"
lang="es"}}</p>
<p>{{i18n "catalog" "category"}}</p>
<p>{{i18n "catalog" "category"}}</p>
<p>{{i18n "catalog" "product"
toUpperCase="true"}}</p>
Services
Routes
Controllers
Components
Helpers
...
Service
Persistent Object that can be accessed for all the application
modules though:
Ember.inject.service()
Services - For What?
Services are useful for features that require shared state or persistent connections.
User/session authenticationGeolocation
Web SocketsServer-sent events or notifications
Server-backed API calls that may not fit Ember Data
Third-party APIs
LoggingInternationalization
Services - and How can I use it?
Create the Service:
ember generate service i18n
Services must extend theEmber.Service base class:
Ember.Service.extend({});
Basic Service Advanced Service
Use the service in an Ember Object or inherited:
i18n: Ember.inject.service()
The Service will be injected by the same name we are assigning to a variable (i18n)
USE YOUR IMAGINATION, EVERYTHING IS POSSIBLE!
Components
LA JOYA DE LA CORONA
AND ROUTABLE COMPONENTS ARE COMING!!
Components: Definition
➢ A reusable partial view (template with Handlebars) with controller that can be combined with other components and contain other components.
❖ Components are used in route templates.❖ They have a specific life cycle and can handle actions of their template and
also pass actions to their containing components, routes and controllers.❖ At this moment components are not routable but soon they will become
routable and then, controllers will disappear.❖ At this moment, EmberJS 2.4.0, All components need to be included inside
a template route that, by definition, has a Shim Controller. ❖ You can think of Components as widgets that are reusable through all our
application and that expose a template helper per usage.
ember generate component my-component→ components/my-component.js→ templates/my-component.hbs
<ul><li>{{my-component}}</li>
</ul>
Components: Passing Properties
➢ Pass Properties TOP - DOWN to components. Always from route templates or other components to the destination component.
export default Ember.Route.extend({
model() {
return this.store.findAll('actor');
}
});
<ul>
{{#each model as |actor|}}
<li>{{actor-component actor=actor}}</li>
{{/each}}
</ul>
<div>{{actor.name}}<span {{action "toggleSection"}} style="margin-left: 5px;">{{more}}
</span></div>
{{#if showImage}}
<div><img src="{{actor.img}}" width="200px"></div>
{{/if}}
Components: Wrapping Content
➢ Let the developer use your component by passing custom content into it.○ The template using your component will pass an html block inside the
component○ The component MUST define the {{yield}} expression in its template○ The component MUST be used in block form: {{#my-component}}...
{{/my-component}}
<div>{{yield}}<span {{action
"toggleSection"}}>{{more}}</span>
</div>
{{#if showImage}}
<div>
<img src="{{actor.img}}" width="200px">
</div>
{{/if}}
<ul>
{{#each model as |actor|}}
<li>{{#actor-component actor=actor}}
<p>{{actor.name}}</p>
{{/actor-component}}
</li>
{{/each}}
</ul>
Components: LifeCycle
➢ Access to hooks when the component is going to be rendered or destroyed.
➢ It enables you to:○ Direct DOM manipulation○ Listening and responding to browser events○ Using 3rd party JavaScript libraries in your Ember app
Initial Rendering:
1 init
2 didReceiveAttrs
3 willRender
4 didInsertElement
5 didRender
Re-Rendering:
1 didUpdateAttrs
2 didReceiveAttrs
3 willUpdate
4 willRender
5 didUpdate
Component Destroy:
1 willDestroyElement
2 willClearRender
3 didDestroyElement
6 didRender
Components: Data Down - Actions Up
➢ Keep the idea that state (properties) always travels in one direction:TOP - DOWN.
➢ Instead, event handling (actions) always travel in the opposite direction:BOTTOM-UP.
➢ Components and Controllers can handle actions from their contained Components. See it as a data synchronization hub.
export default Ember.Component.extend({
actions: {
toggleSection() {
this.toggleProperty('showImage');
this.get('onSelected')(this);
}
}
export default Ember.Controller.extend({
actions: {
sectionShown(selectedActor) {
this.set('selectedActor', selectedActor);
}
}
});
{{actor-component actor=actor onSelected=(action "sectionShown")}}
Components: Transversal Communication
➢ In some situations components can not be on the same parent.➢ We need a data-layer and a BUS using Pub-Sub➢ For this purpose, use a Service extending Ember.Evented
export default Ember.Component.extend({
Appointment: Ember.inject.service(),
actions: {
complete() {
this.get(‘appointment’).trigger(‘complete’);
}
}
});
export default Ember.Component.extend({
appointment: Ember.inject.service(),
willRender() {
this.get(‘appoinment’).on(‘complete’,
this, ‘onComplete’);
},
onComplete() {
// Do Something
}
});
Integration with 3rd party libraries
Integrate into the Ember using the asset manifest file ember-cli-build.js
Integration with 3rd party libraries
If an Ember addon exists... Install via Ember CLI and you are ready to go!
Front-end dependencies can be installed through Bower Package Manager
Other assets can (and should) just be placed in the vendor folder of the project
app.import({
development: 'bower_components/moment.js',
production: 'bower_components/moment.min.js'
});
app.import('vendor/amd.js', {
exports: {
'amd': ['raw', 'request']
}
});
Globals AMD Modules
Remember theBIG PICTURE
ember-cli Router
Routes Models
Templates
Components
Ember-DataControllers
SERVICES
Thanks for your time!
Do you have any questions?