lukehamilton
5/29/2015 - 3:48 AM

Introduction To Ember Notes

Introduction To Ember Notes

Routing

If a route doesn’t have a model it looks to its parent route for its model (by default nested routes inherit the parent’s model).

this.resource(‘album’, {path: ‘/album/:album_id’}, function() {
  this.resource(‘album.comments’);
});

this.resource(’song’, {path: ‘/song/:song_id’}, function() {
  this.resource(’song.comments’);
});

Then must use namespaced route name in link-to helpers and put route files in directories reflecting the same namespace.

{{#link-to 'album.comments'}}See the album comments{{/link-to}}
{{#link-to 'song.comments'}}See the album comments{{/link-to}}
  • Resources are the nouns of your application.
  • Routes are the refinements (adjectives / verbs) i.e. edit, approve, new, favorited.
  • Use nesting in your router to create nested urls that reflect the ui that the user should see when using the app.

Ember Data

Inside app/models/user.js

import DS from 'ember-data'

var attr = DS.attr,
    hasMany = DS.hasMany;

export default DS.Model.extend({
  name: attr('string'),
  age: attr('number'),
  preferredMember: attr('boolean'),
  posts: hasMany('post')
});

Inside app/models/post.js

import DS from 'ember-data'

var attr = DS.attr,
    belongsTo = DS.belongsTo;

export default DS.Model.extend({
  name: attr('string'),
  body: attr('string'),
  user: belongsTo('user')
});

The Store object (available in routes / controllers)

  • Manages requests to server and acts as caching layer to avoid redundant requests.
  • Use 'store' inside of route model hooks in order to get access to models.
import Ember from 'ember';

export default Ember.Route.extend({
  model: function() {
    this.store.find('user', 1); // Ember Data makes a GET request to /users/1
  }
});

Post index page where we want to see all of the posts together.

import Ember from 'ember';

export default Ember.Route.extend({
  model: function() {
    this.store.find('post'); // using find w/ out an id makes a GET request to /posts
    this.store.find('post', {page: 2, per_page: 20}); // GET /posts?page=2&per_page=20
  }
});

Custom Helpers

Helpers and components use hyphenated names to differentiate themselves from properties and to adhere to the current W3C specification for custom elements which require element names with hyphens.

Template

<div class='field'>
  <label>Your Name</label>
  {{input type='text' value='name'}}
</div>

<div class='field'>
  <label>First Name</label>
  {{input type='text' value='firstName'}}
</div>

<div class='field'>
  <label>Last Name</label>
  {{input type='text' value='lastName'}}
</div>

<div class='field'>
  <label>Secret Name</label>
  <p>{{format-secret name}}</p> // using custom helper
  <p>{{reverseNames this}}</p> // using Ember Object (user model)
</div>

Custom Helper

import Ember from 'ember';

function formatSecret(value)  {
  if (!value) { return ''; }
  return value.split('').reverse().join('');
}

export default Ember.Handlebars.makeBoundHelper(formatSecret)

Custom Helper with Ember Object

import Ember from 'ember';

function reverse(value) {
  if (!value) { return ''; }
  return value.split('').reverse().join('');
}

function reverseNames(user) {
  var names = [user.get('firstName'), user.get('lastName')];
  return names.map(reverse).join(' ');
}

export default Ember.Handlebars.makeBoundHelper(reverseNames, 'firstName', 'lastName')
  • Dependent keys indicate what properties the helper depends on (i.e. 'firstName' and 'lastName').

Returning HTML from a Custom Helper

Template

<div class='left'>
  <label>Post Body</label>
  {{textarea value=body}}
</div>

<div class='right'>
  <label>Preview</label>
  <div class='preview'>
    {{format-markdown body}}
  </div>
</div>

<div class='row'>
  <button {{action 'save' model}}>Save</button>
</div>

Custom Helper returning HTML

/* global marked */ // -> Indicates use of global variable 'marked' to make dependencies clear and JSHint happy
import Ember from 'ember';

function formatMarkdown(text) {
  if (!value) { return ''; }
  return marked(text).htmlSafe();
}

export default Ember.Handlebars.makeBoundHelper(formatMarkdown)

Computed Properties

Ember introduces an object model on top of JavaScript's builtin prototypal inheritance (why we have to use getters).

  • Similar to inheritance systems in other languages.
  • Let's you model your application more concisely than using JavaScript's built-in prototypal inheritance.
  • Ember.Object is the root class that all other objects inherit from.
  • Whenever we are using "extend" we are using inheritance to make a subclass of an Ember object.
  • When you extend an Ember object you can give it a list of mixins before the object literal of methods.
Album = Ember.Oject.extend({

})

Album.create() // to instantiate

If details about how a property is managed leak into other parts of our application it means we have poor encapsulation.

To update the DOM when a model's property changes Ember uses an abstraction called computed properties which use an observer pattern to always keep the view layer up to date.

  • Using this syntax we can tell Ember that a function on an object should behave like a property not a method.
  • To declare a function a computed property call 'property' on it and pass in the dependent keys.
  • Dependent keys can go n levels deep i.e. 'assignees.length'.
export default Ember.object.extend({
  isReadyForWork: function() {
    return this.get('assignee') && this.get('description');
  }.property('assignee', 'description')
});

Ember Object improves JavaScript's object model in the following ways.

  • Uniform access to properties with get().
  • Observable properties (mechanism for templating system to observe changes that happen and update the DOM accordingly).
  • Computed properties have built in caching layer and only recalculate if dependent keys change.
  • Declarative way of modeling data flow (through composability of computed properties).

@each syntax in computed properties (used to manage lists of items)

function(){}.property('keyForArray.@each.propertyOnEachItem')

Example: 'tasks.@each.isReadyForWork' means

  • Recompute if an item is added to tasks.
  • Recompute if an item is removed from tasks.
  • Recompute if isReadyForWork on a task changes.

Computed macros express common computed properties in a concise way.

Components

  • Main benefit is components are isolated from each other so they are not affected by the outside world (the app they reside in).
  • Create a custom html element where you describe what it looks like with a handlebars template and you describe the behavior using JavaScript.
  • Pass attributes into components the same way you set attributes on an html element.
  • Components can send actions out to parent components.
  • Use components in block form to pass content in and then use "yield" inside to render content.
  • Trigger "actions" via user input, then update UI via "data-binding".

Naming Convention = {{name-of-component}}

  • app/templates/components/name-of-component.hbs
  • app/components/name-of-component.js

Ember Run Loop

The run loop lets ember officially schedule the work your application does in different queues to get the best performance and smoothest rendering.

  • Batches multiple changes to your model together and updates the browser once.
  • To take full advantage of the ember run loop you should wrap asynchronous callbacks from non ember libraries and browser apis with run loop functions.
  • Run.bind let's us integrate a function with ember's run loop and bind the context at the same time.
run(function()  {
  // code to execute within a RunLoop
});

Queues

  • sync
  • actions
  • routerTransitions
  • render
  • afterRender
  • destroy

Wrapping a function in run.debounce means it will be called in a certain amount of time as long as the function is not invoked again.

run.debounce(this, myFunc, 1000);
// wait 500ms
run.debounce(this, myFunc, 1000);
// myFunc invoked after 1 second (after the second time it was invoked)

Services

  • Services are singleton objects that hold onto global state
  • Does not have a specific defined role within the Ember ecosystem
  • Services are only instantiated once

Examples of what you might use services for

  • Ember data accessible to both routes and controllers.
  • Interfacing with a geolocation api.
  • Coordinating drag-and-drop events.
  • Consuming push events from a server.
  • Using a non-crud server api.

Services can be injected into other ember objects using the injected property syntax.

  • The name passed into the service fn() isn't required if your property name matches the service name.
export default Ember.Service.extend({
  serviceName: Ember.inject.service('service-name');

  setup: function() {
    this.get('serviceName') // If no part of the object 'gets' the service then it will never be created
  }.on('init')

})

Testing

  • A unit test for a component is an integration test on only its part of the DOM that does not touch the outside app.

Unit Tests

  • Exercise a small unit of your application while trying to isolate it from its dependencies.
  • Call functions or methods on an object then either assert against the resulting return value or assert that some side effect like updating the DOM occurred.

Integration Tests

  • Simulate user interaction and exercise many parts of your application together.

End to End Tests (acceptance tests / feature tests)

  • Exercise your entire application stack including your server.
  • Usually involve driving a web browser through a realistic test setup of your application.