Introducing Ember.js & ember-cli

David Thompson

Today

  • Learn what Ember.js and ember-cli are
  • Get an overview of the key concepts
  • See an example project, using the Marvel API
  • Get some pointers to next steps

Ember.js

Web application framework

Opinionated

Designed to communicate with an API

Ember-Data is an optional add-on to work with API data

ember-cli

Broccoli: asset compilation

QUnit: write tests

Testem: run tests

Babel: ES 2015 support

Express: local server

ember s
ember t --server

Marvel API

https://developer.marvel.com

Complex JSON objects in a non-standard format

{
  "posts": [
    {
      "id": 1,
      "title": "Ember is great",
      "author": "David Thompson"
    },
    {
      "id": 2,
      "title": "I am bored",
      "author": "Chad"
    }
  ]
}

Standard?

{
  "post": {
    "id": 1,
    "title": "The Name of the Rose",
    "author": "Umberto Eco"
  }
}

/posts

/posts/1

DS.RESTAdapter

Non-standard

 {
    "code": 200,
    "status": "Ok",
    "copyright": "© 2015 MARVEL",
    "attributionText": "Data provided by Marvel. © 2015 MARVEL",
    "attributionHTML": "<a href=\"http://marvel.com\">Data provided by Marvel. © 2015 MARVEL</a>",
    "etag": "dec2c14b0d86d0abb13a402343744796cefb4b84",
    "data": {
      "offset": 0,
      "limit": 20,
      "total": 1485,
      "count": 20,
      "results": [
        {
          "id": 1011334,
          "name": "3-D Man",
          "description": "",
          "modified": "2014-04-29T14:18:17-0400",
          "thumbnail": {
            "path": "http://i.annihil.us/u/prod/marvel/i/mg/c/e0/535fecbbb9784",
            "extension": "jpg"
          },
          "resourceURI": "http://gateway.marvel.com/v1/public/characters/1011334",
          "comics": {
            "available": 11,
            "collectionURI": "http://gateway.marvel.com/v1/public/characters/1011334/comics",
            "items": [
              {
                "resourceURI": "http://gateway.marvel.com/v1/public/comics/21366",
                "name": "Avengers: The Initiative (2007) #14"
              },
              {
                "resourceURI": "http://gateway.marvel.com/v1/public/comics/24571",
                "name": "Avengers: The Initiative (2007) #14 (SPOTLIGHT VARIANT)"
              },
              {

Lucky Pierre

Example Application

Base Template

Where the HTML ends up

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>Superhero</title>
    <meta name="description" content="">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    {{content-for 'head'}}

    <link rel="stylesheet" href="assets/vendor.css">
    <link rel="stylesheet" href="assets/superhero.css">

    {{content-for 'head-footer'}}
  </head>
  <body>
    {{content-for 'body'}}

    <script src="assets/vendor.js"></script>
    <script src="assets/superhero.js"></script>

    {{content-for 'body-footer'}}
  </body>
</html>

/app/index.html

Router

Application State

import Ember from 'ember';
import config from './config/environment';

var Router = Ember.Router.extend({
  location: config.locationType
});

export default Router.map(function() {
    this.route('about');
    this.resource('characters', function() {
      this.route('show', {
        path: ':character_id'
      });
    });
});

/app/router.js

=
=

/

/loading

/about

/characters

/characters/loading/

/characters/:character_id

Defining a path in the router means Ember looks for a matching Route, Controller, View, and Template 

Route

Load the model

import Ember from 'ember';

export default Ember.Route.extend({
    queryParams: {
      query: {
        refreshModel: true
      },
      offset: {
        refreshModel: true
      }
    },

    model: function(params) {
      if (!params.query) {
        return this.store.find('character', { offset: params.offset });
      }
      return this.store.find('character', { nameStartsWith: params.query, offset: params.offset });
    }
});

/app/routes/characters/index.js

=
=

Load JSON from API into model

Handle query parameters

Model

Represent the data

import Ember from 'ember';

export default Ember.Route.extend({
    queryParams: {
      query: {
        refreshModel: true
      },
      offset: {
        refreshModel: true
      }
    },

    model: function(params) {
      if (!params.query) {
        return this.store.find('character', { offset: params.offset });
      }
      return this.store.find('character', { nameStartsWith: params.query, offset: params.offset });
    }
});

/app/models/character.js

=
=

Load JSON from API into model

Handle query parameters

Controller

Decorates the model

import Ember from 'ember';

export default Ember.ArrayController.extend({
  queryParams: ['query', 'offset'],
  query: null,
  offset: 0,
  increment: 20,

  queryField: Ember.computed.oneWay('query'),

  meta: function() {
    return this.get("content.meta");
  }.property('content.[]'),

  actions: {
    search: function() {
      this.set('query', this.get('queryField'));
      this.set('offset', 0);
    },
    next: function() {
      // TODO: check if there are any more results - compare offset to total
      var next = this.get('increment') + this.get('meta').offset;
      this.set('offset', next);
    }
  }
});

/app/controllers/characters/index.js

=
=

Templates get properties from controller

Controllers handle events

View

"The role of the view in an Ember.js application is to translate primitive browser events into events that have meaning to your application."

Template

Presents the UI

<div class="clearfix mxn2">
  <div class="col col-12 p2 mx-auto">
    <div class="flex flex-wrap flex-stretch p1">
      {{#each model as |character|}}
        {{characters/character-card  character=character}}
      {{/each}}
    </div>
  </div>
</div>

/app/templates/characters/index.js

=
=

Use Handlebars to present data

Ember helpers for forms and UI elements

Wait! What about that API?!

Serializer

Format API data

import DS from 'ember-data';

export default DS.RESTSerializer.extend({

  extract: function(store, type, payload, id, requestType) {
    var results = {};
    results['characters'] = payload.data.results;
    delete payload.data.results;
    results['meta'] = payload.data;
    return this._super(store, type, results, id, requestType);
  }
});

/app/seriaizers/character.js

Transform

Custom attribute

import DS from 'ember-data';

export default DS.Transform.extend({
  deserialize: function(serialized) {
    if (serialized) {
      serialized = serialized.path + "." + serialized.extension;
    }
    return serialized;
  },

  serialize: function(deserialized) {
    return deserialized;
  }
});

/app/transforms/thumbnail.js

=
=

Custom attribute available to models 

Next Steps

  • ember-cli 101
  • Ember.js guides
  • ember-cli guides
  • https://github.com/foraker/ember-reading-group
  • make something

Introducing Ember.js & ember-cli

By David Thompson

Introducing Ember.js & ember-cli

  • 706