Building real world apps in EmberJS

Filippos Vasilakis

Web Dev

What is EmberJS?

  • Javascript front-end framework
  • It's suppose to be a framework for "ambitious web applications"
  • In reality it enforces some patterns to make our life easier
    • MVC (MVVM specifically)
    • (CoC) Convention over configuration
    • (DRY) Don't repeat yourself

Now

  • EmberJS/EmberData 2.5 (stable)
  • 2500 addons
  • Small but growing community

History

  • SproutCore 1.0 by Charles Jolley
  • SproutCore 2.0 on top of 1.0 was schedule, never made it
  • AmberJS was a complete rewrite
  • Renamed to EmberJS

Today

  • Ember
    • ​Router
    • Routes
    • Templates
    • Controllers
    • Components​
    • Helpers
  • Ember-cli
  • ​ember-data
    • Store
    • Models
    • Serializers
    • Adapters

We will go through an Ember app and we will touch the following:

EmberJS Object Model

EmberJS adds some sugar on top of Javascript

  • To define a new Ember class, call the extend() method on Ember.Object:

 

Person = Ember.Object.extend({
  say(thing) {
    alert(thing);
  }
});
  • To create a subclass from any existing class by calling its extend() method:

 

Developer = Person.extend({
  isDeveloper: true.
  say(thing) {
    return this._super(thing) + 'world';
  }
});
  • To create new instances of a class call the create() method:

 

var person = Person.create();
person.say('Hello'); // alerts " says: Hello"
  • To initialize an instance override init() (called automatically on create())

 

Person = Ember.Object.extend({
  init() {
    var name = this.get('name');
    alert(`${name}, reporting for duty!`);
  }
});
  • When accessing the properties of an object, use the get() and set() accessor methods:
var person = Person.create();
var name = person.get('name');
person.set('name', 'Tobias Fünke');

If you don't use those methods, computed properties won't recalculate, observers won't fire, and templates won't update

Our real App

A micro Twitter

It's the same app that Michael Hartl has build for the Rails tutorial

The API

Built in JSONAPI

GET /api/v1/session
POST /api/v1/session
GET /api/v1/users
POST /api/v1/users
GET /api/v1/users/:id
PUT /api/v1/users/:id
DELETE /api/v1/users/:id
GET /api/v1/microposts
GET (feed) /api/v1/microposts?feed_for_user_id=:id
POST /api/v1/microposts
GET /api/v1/microposts/:id
PUT /api/v1/microposts/:id
DELETE /api/v1/microposts/:id
GET /api/v1/users/:user_id/followers
POST /api/v1/users/:user_id/followers/:id
DELETE /api/v1/users/:user_id/followers/:id
GET /api/v1/users/:user_id/followings
POST /api/v1/users/:user_id/followings/:id
DELETE /api/v1/users/:user_id/followings/:id

Let's start!

npm install -g ember-cli
npm install -g bower
ember new micro-twitter
  • minifys your code
  • asset pipelining
  • dependency management
  • runtime configuration (environments)
  • running tests from CLI
  • automation!

Ember CLI

ember Prints out a list of available commands.
ember new <app-name> Creates a directory called <app-name> and in it, generates an application structure. If git is available the directory will be initialized as a git repository and an initial commit will be created. Use --skip-git flag to disable this feature.
ember init Generates an application structure in the current directory.
ember build Builds the application into the dist/ directory (customize via the --output-path flag). Use the--environment flag to specify the build environment (defaults to development). Use the --watch flag to keep the process running and rebuilding when changes occur.
ember server Starts the server. The default port is 4200. Use the --proxy flag to proxy all ajax requests to the given address. For example, ember server --proxy http://127.0.0.1:8080 will proxy all ajax requests to the server running at http://127.0.0.1:8080. Aliases: ember s, ember serve
ember generate <generator-name> <options> Runs a specific generator. To see available generators, run ember help generate. Alias: ember g
ember destroy <generator-name> <options> Removes code created by the generate command. If the code was generated with the --pod flag, you must use the same flag when running the destroy command. Alias: ember d
ember test Run tests with Testem in CI mode. You can pass any options to Testem through a testem.json file. By default, Ember CLI will search for it under your project’s root. Alternatively, you can specify a config-file. Alias:ember t
ember install <addon-name> Installs the given addon into your project and saves it to the package.json file. If provided, the command will run the addon’s default blueprint.

Ember CLI

app/ Contains your Ember application’s code. Javascript files in this directory are compiled through the ES6 module transpiler and concatenated into a file called <app-name>.js. See the table below for more details.
dist/ Contains the distributable (optimized and self-contained) output of your application. Deploy this to your server!
public/ This directory will be copied verbatim into the root of your built application. Use this for assets that don’t have a build step, such as images or fonts.
tests/ Includes your app’s unit and integration tests, as well as various helpers to load and run the tests.
tmp/ Temporary application build-step and debug output.
bower_components/ Bower dependencies (both default and user-installed).
node_modules/ npm dependencies (both default and user-installed).
vendor/ Your external dependencies not installed with Bower or npm.
.jshintrc JSHint configuration.
.gitignore Git configuration for ignored files.
ember-cli-build.js Contains the build specification for Broccoli.
bower.json Bower configuration and dependency list. See Managing Dependencies.
package.json npm configuration and dependency list. Mainly used to list the dependencies needed for asset compilation.

Ember CLI folder structure

app/app.js Your application’s entry point. This is the first executed module.
app/index.html The only page of your single-page app! Includes dependencies, and kickstarts your Ember application. See app/index.html.
app/router.js Your route configuration. The routes defined here correspond to routes in app/routes/.
app/styles/ Contains your stylesheets, whether SASS, LESS, Stylus, Compass, or plain CSS (though only one type is allowed, see Asset Compilation). These are all compiled into <app-name>.css.
app/templates/ Your HTMLBars templates. These are compiled to /dist/assets/<app-name>.js. The templates are named the same as their filename, minus the extension (i.e. templates/foo/bar.hbs -> foo/bar).

app/controllers/app/models/, etc.

Ember CLI  app/ folder structure

The first step is to define the basic 'routes' of our app

But let'se see what 'routes' are in EmberJS first..

Routing is the core of the whole framework

When we say Ember Routes we mean the URLs structure but also the UI structure.

(the directory structure of templates/routes/controllers follow the routing as well!)

# config/routes.rb
resources :artist do
  resources :albums do
    resources :album
  end
end
// config/router.js
App.Router.map(function() {
  this.route('artist', { path: 'artists/:artist_id' }, function() {
    this.route('albums', function() {
      // since we passed a function, index is generated for us
      this.route('album', { path: '/:album_id' });
    });
  });
});

At every level of nesting (including the top level), Ember automatically provides a route for the / path named index

 

Each template will be rendered into the {{outlet}} of its parent route's template. 

Ember Routes

<header class="navbar navbar-fixed-top navbar-inverse">
<!-- html(bars) code -->
</header>
<div class="container">


  {{outlet}} <!-- The child(nested) template -->


  <footer class="footer">
   <!-- html(bars) code -->
  </footer>
</div>
<h1> Welcome to our wonderful website! </h1>

Application Root Template

Index Template

This loads everytime when a suburl is accessed

This loads only when the root url is accessed (which happens to be the Application Template)

+

<header class="navbar navbar-fixed-top navbar-inverse">
<!-- html(bars) code -->
</header>
<div class="container">


  <h1> Welcome to our wonderful website! </h1>

  <footer class="footer">
   <!-- html(bars) code -->
  </footer>
</div>

http://localhost:4200/

Ember Routes

<header class="navbar navbar-fixed-top navbar-inverse">
<!-- html(bars) code -->
</header>
<div class="container">


  {{outlet}} <!-- The child(nested) template -->


  <footer class="footer">
   <!-- html(bars) code -->
  </footer>
</div>

Application Root Template

<h2> {{artistName}} </h2>

{{outlet}} <!-- index or albums/songs/bio template -->

Artist Root Template

This loads everytime when a suburl is accessed

+

This loads only when the root url of the Artist Route is accessed

<header class="navbar navbar-fixed-top navbar-inverse">
<!-- html(bars) code -->
</header>
<div class="container">


  <h2> Bob Dylan </h2>
    
  <p> This artist is well known for ....

  <footer class="footer">
   <!-- html(bars) code -->
  </footer>
</div>
<p> This artist is well know for .... </p>

Artist Index Template

This loads only when the root url of the ArtistAlbums Route is accessed

+

http://localhost:4200/artists/bobdylan

Ember Routes

<header class="navbar navbar-fixed-top navbar-inverse">
<!-- html(bars) code -->
</header>
<div class="container">


  {{outlet}} <!-- The child(nested) template -->


  <footer class="footer">
   <!-- html(bars) code -->
  </footer>
</div>

Application Root Template

<h2> {{artistName}} </h2>

{{outlet}} <!-- albums/songs/bio template -->

Artist Root Template

This loads everytime when a suburl is accessed

+

This loads only when the root url of the Artist Route is accessed

<header class="navbar navbar-fixed-top navbar-inverse">
<!-- html(bars) code -->
</header>
<div class="container">


  <h2> Bob Dylan </h2>
    
    <ul>
      <li>Bob Dylan</li>
      <li>The Freewheelin' Bob Dylan</li>
      <li>The Times They Are a-Changin'</li>
    </ul>

  <footer class="footer">
   <!-- html(bars) code -->
  </footer>
</div>
<h3> Albums </h3>

<!-- albums html(bars) code -->

Albums Index Template

This loads only when the root url of the ArtistAlbums Route is accessed

+

http://localhost:4200/artists/bobdylan/albums

Ember Routes

Routes in Ember play a crucial role

Ember Routes

A route in Ember:

  • Retrieves the models (a model could be anything)
  • Initializes the controller and selects template to be shown
  • A route has deliberate access to other routes and controllers using the controllerFor('route') and modelFor('route') functions

Ember Routes

Router Request Lifecycle

  1. enter()
    • (private)

  2. activate()
    • executed when entering the route

  3. Model hooks

    1. beforeModel(transition)
    2. model(params)
    3. afterModel(resolvedModel, transition)
  4. setupController(controller, model)
  5. renderTemplate(controller, model)

Usually model hooks and setupController() are used

Ember Routes

Route model hooks

  • They are used to load route data.
  • It could be anything:
    • a request to the API using Ember Data
    • a request to an API using regular JS
    • a Javascript Object
    • create a webRTC connection
  • Ember will wait until the data finishes loading (until the promise is resolved) before continuing
  • each hook has a different use case:
    • beforeModel(transition)
    • model(params)
    • afterModel(model, transition)

Ember Routes

setupController(controller, model)

  • It's the place to setup your controller(s)
  • It doesn't block like model hooks, so some people use it to setup secondary models
    • i.e. if a model takes too much time to load while the template can live without it, you can move it here
    • It has been removed from Ember docs so I guess it's not best practice anymore :)

renderTemplate(controller, model)

  • Helps  you render a different template in the {{outlet}} of the parent template
  • Usefull if you have multiple {{outlet}} in a template

Ember Controllers

A controller in Ember:

  • Used to be a model decorator/proxy but now it's a model presenter
  • It automatically gets the model by the default Ember Route (from setupController() as we will see later)
  • In essence, it initializes all the components that the template includes while they act like a wrapping component themselves
  • Ember community wants to eliminate controllers and move on to routable components
    • IMHO this will not happen anytime soon and they will stay for the next 2 years
    • they offer some good features that can't be found elsewhere in EmberJS stack (like query params)

Ember Templates

A template in Ember:

  • Organize the layout of HTML in an application
  • Htmlbars templates contain static HTML and dynamic content inside Handlebars expressions, which are invoked with double curly braces: {{}}
  • A template is "rendered" by a route or in the lower level by a component

Ember Components

A component in Ember:

  • They are isolated and the controller (or parent component) should inject anything that is needed
  • In reallity components can have access to global state if we want to but it's a bad practice :)
  • A component has it's own lifecycle and template that it renders
  • Components must have at least one dash in their name
  • While templates describe how a user interface looks, components control how the user interface behaves.
//app/config.js
import Ember from 'ember';
import config from './config/environment';

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

Router.map(function() {
  this.route('help');
  this.route('about');
  this.route('contact');

  this.route('sessions.new', {path: '/login'});
  this.route('users.new', {path: '/signup'});

  this.route('users', function() {
  });
  this.route('user', {path: '/users/:user_id'}, function() {
    this.route('edit');
    this.route('following');
    this.route('followers');
  });
});

export default Router;

Back to our micro-twitter app

Having ready the app/router.js, we have defined the basic specifications of our app

We nest 'edit', 'followings' and 'followers' routes under user to reuse 'user' route model

Back to our micro-twitter app

Implementing our first route/template

//app/templates/application.hbs
<header class="navbar navbar-fixed-top navbar-inverse">
  <div class="container">
    {{#link-to 'index' id='logo'}}sample app{{/link-to}}
    <nav>
      <ul class="nav navbar-nav pull-right">
        <li>{{link-to 'Home' 'application'}}</li>
        <li>{{link-to 'Help' 'help'}}</li>
      </ul>
    </nav>
  </div>
</header>
<div class="container">

  {{outlet}}

  <footer class="footer">
    <nav>
      <ul>
        <li>{{link-to 'About' 'about'}}</li>
        <li>{{link-to 'Contact' 'contact'}}</li>
      </ul>
    </nav>
  </footer>
</div>

Our basic Application Template

//app/templates/help.hbs
<h1>Help</h1>
<p>
  Do you need help? Open a gihub issue in the repository
  <a href="https://github.com/vasilakisfil/rails_tutorial_ember">
    <em>rails_tutorial_ember</em>
  </a>.
</p>

Back to our micro-twitter app

http://localhost:4200/help

//app/routes/help.js
import Ember from 'ember';

export default Ember.Route.extend({});

If your route is empty, you don't need to define it (ember does that automatically)

  1. User navigates to http://localhost:4200/help
  2. Ember fires the Route object
  3. Route retrieves the model (empty)
  4. Route sets up the (default) controller and renders the (default) template

Implementing our first route/template

Back to our micro-twitter app

Implementing our first route/template

Ember Templates

  • Ember templates are quite powerful, utilizing htmlbars
{{#if isAtWork}}
  Ship that code!
{{else if isReading}}
  You can finish War and Peace eventually...
{{/if}}
{{#each people as |person|}}
  Hello, {{person.name}}!
{{else}}
  Sorry, nobody is here.
{{/each}}
<div id="logo">
  <img src={{logoUrl}} alt="Logo">
</div>

Conditionals

Iterating lists

Binding attributes

{{input type="text" value=firstName disabled=entryNotAllowed size="50"}}

Input helpers

<input type="text" value={{firstName}} disabled={{entryNotAllowed}} size="50">

Personally I would like to see inputs like that

But that's not possible, yet.

After Ember 2.0 and htmlbars Templates are very powerful and they will never cause you a problem.
(before Ember 1.12 we had many issues though )

{{#link-to "photos.edit" 1}}First Photo Ever{{/link-to}}

link-to helper

Ember Actions

Ember actions are events that are propagated from template to controller and then to the routes until one of them catches the event.

  • Ember community tries to eliminate controllers completely
  • But until this happens, you have to include ember-route-actions-helper addon if you want seamless bubbling (otherwise you might run into some edge cases in which you need to catch and re-send the action in the controller)

 

<h3><button {{action "toggleBody"}}>{{title}}</button></h3>
{{#if isShowingBody}}
  <p>{{{body}}}</p>
{{/if}}
export default Ember.Route.extend({
  actions: {
    toggleBody() {
      this.toggleProperty('isShowingBody');
    }
  }
});
  • Action handlers can be defined in a Route, Controller or a Component
  • It's a good idea to try to define them as high as possible in the hierarchy so that  you can reuse them across multiple templates
  • Data Down, Actions Up (DDAU) axiom is a good start

Ember Actions

Data Down, Actions Up (DDAU)

Simpler than what it seems

 

  1. You create an object in the Route and you inject it in the controller/template
  2. You (user) modify the object in the template
  3. You change the state of the object or of the app by sending an action up (with a copy of the modified data) and catch the action by an action handler (ideally in the route)

It's common sense that if you try to change the app's state from the low level (component)  you will increase the complexity it in the long run leading into more bugs

But if the change is local (only inside a component) and not app-wide don't afraid to break the axiom :)

Always handle actions in the Route when the change is app-wide because routes have access to other routes,controllers and models!

//app/routes/session/new.js
import Ember from 'ember';

export default Ember.Route.extend({
  model() {
    return Ember.Object.create({
      identifier: null,
      password: null
    });
  }
});

Let's build the login form! (http://localhost:4200/login)

Back to our micro-twitter app...

We create the object to be passed down to controller automatically by setupController()

The route

//app/templates/sessions/new.hbs
<h1>Log in</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">

    {{#if loginError}}
    <div id="error_explanation">
      <div class="alert alert-danger">
        Invalid email/password combination
      </div>
    </div>
    {{/if}}

    <form class="form-basic" role="form" {{action (route-action 'createSession' model) on='submit'}}>
      <label for="user_email">Email</label>
      {{input class="form-control" type="email" value=model.identifier size="50"}}

      <label for="user_password">Password</label> {{link-to '(forgot password)' 'users.new'}}
      {{input class="form-control" type="password" value=model.password size="50"}}

      <label class="checkbox inline" for="session_remember_me">
        {{input type="checkbox" name="session_remember_me" checked=isAdmin}}
        <span>Remember me on this computer</span>
      </label>

      <input type="submit" value="Log in" class="btn btn-primary">
    </form>
    <p>New user? {{link-to 'Sign up now!' 'users.new'}}</p>
  </div>
</div>

The template

Back to our micro-twitter app...

Building the login form! (http://localhost:4200/login)

//app/routes/session/new.js
import Ember from 'ember';

export default Ember.Route.extend({
  model() {
    return Ember.Object.create({
      identifier: null,
      password: null
    });
  },

  actions: {
    createSession(session) {
      var _this = this;
      //handle session creation
      Ember.$.ajax({
        url: 'http://localhost:3000/api/v1/session',
        type: 'POST',
        data: session.toJson(),
        contentType: 'application/json;charset=utf-8',
        dataType: 'json'
      }).then(
        function(response) {
          //handle the token etc
        },
        function(error) {
          _this.controllerFor('login').set('loginError', true);
      });
    },
  },
});

Back to our micro-twitter app...

We handle the action in the route

Building the login form! (http://localhost:4200/login)

Ember Addons

//app/routes/session/new.js
import Ember from 'ember';

export default Ember.Route.extend({
  model() {
    return Ember.Object.create({
      identifier: null,
      password: null
    });
  },

  actions: {
    createSession(session) {
      var _this = this;
      this.get('session').authenticate(
        'authenticator:devise',
        session.get('identifier'),
        session.get('password')
      ).catch((reason) => {
        _this.controllerFor('login').set(
          'errorMessage',
          reason.error || reason
        );
      });
    },
    destroySession() {
      this.get('session').invalidate();
    },
  },
});

Ember has 2500 addons

Usually you will always find an addon for every common problem

Here we use ember-simple-auth one of the most popular authentication addons for Ember

 

You can create an authenticator/authorizer for any API in no time


If you own the API, you can structure it in such a way to use an already implemented authenticator/authorizer

Here we use Devise authenticator

Implementing the user edit form

Back to our micro-twitter app...

But let's talk about Ember Data first that help us to sync data to our backend

Ember Data

It provides an ORM over the data, regadless the transfer medium that is used below (HTTP API, WebSockets, IndexDB etc)

//app/models/user.js
import Model from 'ember-data/model';
import attr from 'ember-data/attr';

export default Model.extend({
  name: attr('string'),
  email: attr('string'),
  password: attr('string'),
  updatedAt:  attr('date'),
  createdAt:  attr('date')
});
{
    "data": {
    "id": "1",
    "type": "users",
    "attributes": {
        "name": "Example User",
        "email": "example@embertutorial.org",
        "created-at": "2016-05-26T02:57:38Z",
        }
    }
}
{
    "user": {
    "id": 1,
        "name": "Example User",
        "email": "example@railstutorial.org",
        "created_at": "2016-05-26T02:57:38Z"
    }
}

JSONAPI spec

regular JSON

Mapping to Ember Data

Ember Data

The serializer and adapter define the way EmberData will interact with the API

Since Ember 2.0, JSONAPI is the default

But regular (AMS) JSON is very popular as well

Creating your own serializer and adapter is easy which means any API can work with EmberData if you have right adapter/serializer

The serializer is responsible for the data(json) format

The adapter is responsible for the endpoints, headers and anything else not related the data itself

Ember Data

It provides an ORM over the data, regadless the transfer medium that is used below (HTTP API, WebSockets, IndexDB etc)

//app/models/user.js
import Model from 'ember-data/model';
import attr from 'ember-data/attr';

export default Model.extend({
  name: attr('string'),
  email: attr('string'),
  password: attr('string'),
  updatedAt:  attr('date'),
  createdAt:  attr('date')
});
var user = this.store.findRecord('user', 1); // => GET /users/1
var user = this.store.peekRecord('user', 1); // => no network request

var users = this.store.findAll('user'); // => GET /users
var users = this.store.peekAll('user'); // => no network request

// GET to /users?filter[name]=Peter
this.store.query('users', { filter: { name: 'Peter' } }).then(function(peters) {
  // Do something with `peters`
});

//if you know that your filter is unique
//this picks up the only one resource from the collection (if there is any)
this.store.queryRecord('user', { filter: { email: 'tomster@example.com' } }).then(function(tomster) {
  // do something with `tomster`
});

Querying records..

Ember Data

It provides an ORM over the data, regadless the transfer medium that is used below (HTTP API, WebSockets, IndexDB etc)

//app/models/user.js
import Model from 'ember-data/model';
import attr from 'ember-data/attr';

export default Model.extend({
  name: attr('string'),
  email: attr('string'),
  password: attr('string'),
  updatedAt:  attr('date'),
  createdAt:  attr('date')
});
var user = store.createRecord('user', {
  name: 'Tom Dale',
  email: 'tomdale@email.com',
  password: '123123123'
});
user.save() //POST /users

store.findRecord('user', 1).then(function(user) {
  user.get('name'); // => "Tom Dale"
  user.set('updatedAt', new Date());
  user.save(); // => PATCH to '/users/1'
});

store.findRecord('user', 2).then(function(user) {
  user.destroyRecord(); // => DELETE to /users/2
});

Creating, Updating and Deleting

Keep in mind that Ember Data is separate from Ember, which means you can even build you own ORM library if you want and work with that in your Ember app!

Implementing the user edit form

Back to our micro-twitter app...

//app/models/user.js
import Model from 'ember-data/model';
import attr from 'ember-data/attr';

export default Model.extend({
  name: attr('string'),
  email: attr('string'),
  password: attr('string'),
  updatedAt:  attr('date'),
  createdAt:  attr('date'),

  passwordConfirmation: null,

  valid() {
    if (!this.get('password')) { return true; }
    this.get('errors')._clear();
    if (this.get('password') === this.get('passwordConfirmation')) { return true;}
    this.get('errors')._add(
      'passwordConfirmation',
      'is not the same with the password'
    );
    return false;
  }
});
//app/routes/user.js
import Ember from 'ember';

export default Ember.Route.extend({
  model(params) {
    return this.store.findRecord('user', params.user_id);
  }
});
//app/routes/user/edit.js
import Ember from 'ember';

export default Ember.Route.extend({
  model() {
    return this.modelFor('user');
  }
});

Remember our routes definition in app/router.js ?

Implementing the user edit form

Back to our micro-twitter app...

//app/templates/user/edit.hbs
<h1>Update your profile</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">

    {{#if model.errors}}
      <div id="error_explanation">
        <div class="alert alert-danger">
          The form contains {{model.errors.length}} errors.
        </div>
        <ul>
          {{#each model.errors as |error|}}
            <li>{{error.attribute}} {{error.message}}</li>
          {{/each}}
        </ul>
      </div>
    {{/if}}

    <form class="form-basic" role="form" {{action (route-action 'updateUser' model) on='submit'}}>
      <label for="user_name">Name</label>
      {{input class="form-control" type="text" value=model.name size="50"}}

      <label for="user_email">Email</label>
      {{input class="form-control" type="text" value=model.email size="50"}}

      <label for="user_password">Password</label>
      {{input class="form-control" type="password" value=model.password size="50"}}

      <label for="user_password_confirmation">Confirmation</label>
      {{input class="form-control" type="password" value=model.passwordConfirmation size="50"}}

      <input type="submit" value="Save changes" class="btn btn-primary">
    </form>
  </div>
</div>

The template

Don't forget that this is rendered inside ApplicationTemplate

Implementing the user edit form

Back to our micro-twitter app...

//app/templates/user/edit.hbs
<h1>Update your profile</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">

    {{resource-errors errors=model.errors}}

    <form class="form-basic" role="form" {{action (route-action 'updateUser' model) on='submit'}}>
      <label for="user_name">Name</label>
      {{input class="form-control" type="text" value=model.name size="50"}}

      <label for="user_email">Email</label>
      {{input class="form-control" type="text" value=model.email size="50"}}

      <label for="user_password">Password</label>
      {{input class="form-control" type="password" value=model.password size="50"}}

      <label for="user_password_confirmation">Confirmation</label>
      {{input class="form-control" type="password" value=model.passwordConfirmation size="50"}}

      <input type="submit" value="Save changes" class="btn btn-primary">
    </form>
  </div>
</div>

Moving the errors in a component

//app/templates/components/resource-errors.hbs
{{#if model}}
  <div id="error_explanation">
    <div class="alert alert-danger">
      The form contains {{model.length}} errors.
    </div>
    <ul>
      {{#each model as |error|}}
        <li>{{error.attribute}} {{error.message}}</li>
      {{/each}}
    </ul>
  </div>
{{/if}}

Implementing the user edit form

Back to our micro-twitter app...

We need to format the text a bit

  • errors -> error if only 1 error
  • passwordConfirmation -> Password confirmation

We have 3 options:

  • use the component class itself to define some computed properties
  • use Ember helpers
  • use another component in which we inject the necessary data

Implementing the user edit form

Back to our micro-twitter app...

//app/components/resource-errors.js
import Ember from 'ember';

export default Ember.Component.extend({
  errorsText: Ember.computed('model.length', function() {
      var inflector = new Ember.Inflector(Ember.Inflector.defaultRules);
      var word = 'error';    

      if (this.get('model.length') > 1) {
        return inflector.pluralize(word);
      } else {
        return inflector.singularize(word);
      }
  }),
});

Using the component itself

//app/templates/components/resource-errors.hbs
{{#if model}}
  <div id="error_explanation">
    <div class="alert alert-danger">
      The form contains {{model.length}} {{errorsText}}.
    </div>
    <ul>
      {{#each model as |error|}}
        <li>{{error.attribute}} {{error.message}}</li>
      {{/each}}
    </ul>
  </div>
{{/if}}

You can think components class like a small controller

To humanize the list of attributes is tricky though: use Ember Helpers instead

Ember Helpers

//app/helpers/pluralize.js
import Ember from 'ember';

export function pluralize([word], {amount}) {
  var inflector = new Ember.Inflector(Ember.Inflector.defaultRules);

  if (amount > 1) {
    return inflector.pluralize(word);
  } else {
    return inflector.singularize(word);
  }
}

export default Ember.Helper.helper(pluralize);

Helpers are most useful for transforming raw values from models and components into a format more appropriate for your users

//app/helpers/humanize.js
import Ember from 'ember';

export function humanize([word]) {
  return word.decamelize().split('_').join(' ').capitalize();
}

export default Ember.Helper.helper(humanize);

Usually helpers are used when component is a bit too much

Helper names don't need to have a dash like components

Implementing the user edit form

Back to our micro-twitter app...

//app/templates/components/resource-errors.hbs
{{#if model}}
  <div id="error_explanation">
    <div class="alert alert-danger">
      The form contains {{model.length}} {{pluralize 'error' amount=model.length}}.
    </div>
    <ul>
      {{#each model as |error|}}
        <li>{{humanize error.attribute}} {{error.message}}</li>
      {{/each}}
    </ul>
  </div>
{{/if}}

The resource-errors template with the helpers

//app/templates/user/edit.hbs
<h1>Update your profile</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">

    {{resource-errors errors=model.errors}}

    <form class="form-basic" role="form" {{action (route-action 'updateUser' model) on='submit'}}>
      <label for="user_name">Name</label>
      {{input class="form-control" type="text" value=model.name size="50"}}

      <label for="user_email">Email</label>
      {{input class="form-control" type="text" value=model.email size="50"}}

      <label for="user_password">Password</label>
      {{input class="form-control" type="password" value=model.password size="50"}}

      <label for="user_password_confirmation">Confirmation</label>
      {{input class="form-control" type="password" value=model.passwordConfirmation size="50"}}

      <input type="submit" value="Save changes" class="btn btn-primary">
    </form>
  </div>
</div>

Implementing the user edit form

Back to our micro-twitter app...

Much  nicer :)

Implementing the user edit form

Back to our micro-twitter app...

//app/routes/user.js
import Ember from 'ember';

export default Ember.Route.extend({
  model() {
    return this.modelFor('user');
  },

  actions: {
    updateUser(user) {
      var _this = this;
      if (!user.valid()) { return false; }

      user.save().then(function() { //PUT /api/v1/users/:id
        user.setProperties({
          'password': null,
          'passwordConfirmation': null
        });
        //notify the user that the profile has been saved
      });
    }
  }
});

The action handler

You can define the action in any parent route in the route hierarchy

Action will bubble until one of them handles it

Implementing the index page

Back to our micro-twitter app...

The models we need:

  • A new micropost record
  • The microposts feed
  • User record
  • User's followers count
  • User's followings count

The API

Built in JSONAPI

GET /api/v1/session
POST /api/v1/session
GET /api/v1/users
POST /api/v1/users
GET /api/v1/users/:id
PUT /api/v1/users/:id
DELETE /api/v1/users/:id
GET /api/v1/microposts
GET (feed) /api/v1/microposts?feed_for_user_id=:id
POST /api/v1/microposts
GET /api/v1/microposts/:id
PUT /api/v1/microposts/:id
DELETE /api/v1/microposts/:id
GET /api/v1/users/:user_id/followers
POST /api/v1/users/:user_id/followers/:id
DELETE /api/v1/users/:user_id/followers/:id
GET /api/v1/users/:user_id/followings
POST /api/v1/users/:user_id/followings/:id
DELETE /api/v1/users/:user_id/followings/:id

Restructuring our models

Back to our micro-twitter app...

//app/models/person.js
import Model from 'ember-data/model';
import attr from 'ember-data/attr';

export default Model.extend({
  email: attr('string'),
  name:  attr('string'),
  password:  attr('string'),

  created_at: attr('moment'),
  updated_at: attr('moment'),

  size: Ember.computed('meta', function() {
    return this.get('meta.total-count');
  })

/*
  //old style
  size: function() {
    return this.get('meta.total-count');
  }).property('meta')
*/
});
//app/models/user.js
import Person from './person';

export default Person.extend({
  passwordConfirmation: null,

  valid() {
    if (!this.get('password')) { return true; }
    this.get('errors')._clear();
    if (this.get('password') === this.get('passwordConfirmation')) { return true;}
    this.get('errors')._add(
      'passwordConfirmation',
      'is not the same with the password'
    );
    return false;
  }
});
//app/models/follower.js
import Person from './person';

export default Person.extend({
});
//app/models/following.js
import Person from './person';

export default Person.extend({
});

Implementing the index page

Back to our micro-twitter app...

import Ember from 'ember';

export default Ember.Route.extend({
  model() {
    if (this.get('session.isAuthenticated')) {
      return Ember.RSVP.hash({
        user: this.store.findRecord(
          'user', this.get('session.data.authenticated.id')
        ),
        userFollowing: this.store.query('following', {
          user_id: this.get('session.data.authenticated.id'),
          per_page: 1 //optimization, we only need the meta info
        }),
        userFollowers: this.store.query('follower', {
          user_id: this.get('session.data.authenticated.id'),
          per_page: 1 //optimization, we only need the meta info
        }),
        microposts: this.store.query('feed', {
          feed_for_user_id: this.get('session.data.authenticated.id')
        }),
        micropost: this.store.createRecord('micropost', {})
      });
    }
  }
});

Ember.RSVP.hash doesn't resolve until all promises have been resolved

Implementing the index page

Back to our micro-twitter app...

{{#if session.isAuthenticated}}
<div class="row">
  <aside class="col-md-4">
    {{user-profile user=model.user followingsCount=model.followings.size followersCount=model.followers.size}}

    {{new-micropost model=model.micropost createMicropost="createMicropost"}}
  </aside>
  <div class="col-md-8">
    {{microposts-index title="Micropost Feed" model=model.microposts}}
  </div>
</div>
{{else}}
  <div class="center jumbotron">
    <h1>Welcome to the Sample App</h1>
    
    <h2>
      This is the home page for the 
      <a href="https://github.com/vasilakisfil/rails_tutorial_ember">Ember on Rails Tutorial</a>
      sample application.
    </h2>
    
    {{link-to "Sign up now!" 'users.new' class="btn btn-lg btn-primary"}}
  </div>

  <a href="http://emberjs.com/">
    <img src="https://pbs.twimg.com/profile_images/2326095089/3s1seyc0csl75btyw1vl.png" alt="Ember logo" class="ember-logo">
  </a>
  
{{/if}}

It's a good practice to break down our template in a number of components

Implementing the user-profile component

Back to our micro-twitter app...

//app/templates/components/user-profile.hbs

<section class="user_info">
  <h1>
    <!-- an ember addon for gravatar -->
    {{gravatar-image email=model.user.email alt=model.user.name size=80 class='gravatar'}}
    {{user.name}}
  </h1>
</section>

<section class="stats">
  <div class="stats">
    {{#link-to 'user.following' model.user}}
      <strong id="following" class="stat">
        {{followingsCount}}
      </strong>
      following
    {{/link-to}}

    {{#link-to 'user.followers' model.user}}
      <strong id="followers" class="stat">
        {{followersCount}}
      </strong>
      followers
    {{/link-to}}
  </div>
</section>

Implementing the microposts-index component

Back to our micro-twitter app...

//app/templates/components/microposts-index.hbs
{{#if model.length}}
  <h3>{{title}}</h3>

  <ol class="microposts">
    {{#each model as |micropost|}}
      <li class={{micropost.id}}>
        {{gravatar-image email=micropost.user.email alt=micropost.user.name size=50 class='gravatar'}}

        <span class="user">{{link-to micropost.user.name 'user.index' micropost.user.id}}</span>
        <span class="content">
          {{micropost.content}}
          {{#if micropost.picture}}
            <img src={{micropost.pictureUrl}}>
          {{/if}}
        </span>
        <span class="timestamp">
          <!-- Ember addon that reports the datetime in "3 minutes ago"-style -->
          Posted about {{moment-from-now micropost.createdAt interval=10000}}.
        </span>
      </li>

    {{/each}}
  </ol>

  <!-- Ember addon for pagination -->
  {{page-numbers content=model numPagesToShow=4}}
{{/if}}

Implementing the new-micropost component

Back to our micro-twitter app...

//app/templates/components/new-micropost.hbs
<section class="micropost_form">
  <form class="form-basic" role="form" {{action (route-action 'createMicropost' model) on='submit'}}>
    <div class="field">
      {{textarea cols="80" value=model.content rows="4" placeholder="Compose new micropost..."}}
    </div>

    <input type="submit" class="btn btn-primary">
  </form>
</section>

Implementing the index page

Back to our micro-twitter app...

{{#if session.isAuthenticated}}
<div class="row">
  <aside class="col-md-4">
    {{user-profile user=model.user followingsCount=model.followings.size followersCount=model.followers.size}}

    {{new-micropost model=model.micropost createMicropost="createMicropost"}}
  </aside>
  <div class="col-md-8">
    {{microposts-index title="Micropost Feed" model=model.microposts}}
  </div>
</div>
{{else}}
  <div class="center jumbotron">
    <h1>Welcome to the Sample App</h1>
    
    <h2>
      This is the home page for the 
      <a href="https://github.com/vasilakisfil/rails_tutorial_ember">Ember on Rails Tutorial</a>
      sample application.
    </h2>
    
    {{link-to "Sign up now!" 'users.new' class="btn btn-lg btn-primary"}}
  </div>

  <a href="http://emberjs.com/">
    <img src="https://pbs.twimg.com/profile_images/2326095089/3s1seyc0csl75btyw1vl.png" alt="Ember logo" class="ember-logo">
  </a>
  
{{/if}}

It's a good practice to break down our template in a number of components

Implementing the index page

Back to our micro-twitter app...

import Ember from 'ember';

export default Ember.Route.extend({
  model() {
    if (this.get('session.isAuthenticated')) {
      return Ember.RSVP.hash({
        user: this.store.findRecord(
          'user', this.get('session.data.authenticated.id')
        ),
        userFollowing: this.store.query('following', {
          user_id: this.get('session.data.authenticated.id'),
          per_page: 1 //optimization, we only need the meta info
        }),
        userFollowers: this.store.query('follower', {
          user_id: this.get('session.data.authenticated.id'),
          per_page: 1 //optimization, we only need the meta info
        }),
        microposts: this.store.query('feed', {
          feed_for_user_id: this.get('session.data.authenticated.id')
        }),
        micropost: this.store.createRecord('micropost', {})
      });
    }
  },

  actions: {
    createMicropost(micropost) {
      var _this = this;
      //this.model() will run the model hook again!
      micropost.set('user', this.modelFor('index').user);
      micropost.save().then(function(/*micropost*/) {
        _this.refresh();
      });
    }
  }
});

We make the final touches to the model before saving it to the server

Once the HTTP request has finished (server has responded) we reload the model so that we see the new micropost in the feed

Implementing the index page

Back to our micro-twitter app...

import Ember from 'ember';

export default Ember.Route.extend({
  model() {
    if (this.get('session.isAuthenticated')) {
      return Ember.RSVP.hash({
        user: this.store.findRecord(
          'user', this.get('session.data.authenticated.id')
        ),
        userFollowing: this.store.query('following', {
          user_id: this.get('session.data.authenticated.id'),
          per_page: 1 //optimization, we only need the meta info
        }),
        userFollowers: this.store.query('follower', {
          user_id: this.get('session.data.authenticated.id'),
          per_page: 1 //optimization, we only need the meta info
        }),
        microposts: this.store.query('feed', {
          feed_for_user_id: this.get('session.data.authenticated.id')
        }),
        micropost: this.store.createRecord('micropost', {})
      });
    }
  },

  actions: {
    createMicropost(micropost) {
      var _this = this;
      //this.model() will run the model hook again!
      micropost.set('user', this.modelFor('index').user);
      micropost.save().then(function(micropost) {
        Ember.set(
          this.modelFor('index'),
          'micropost',
          this.store.createRecord('micropost', {})
        );
        _this.modelFor('index').microposts.unshiftObject(micropost._internalModel);
      });
    }
  }
});

Instead of running the model hook again, we can just push the new object in the feed array

Implementing the index page

Back to our micro-twitter app...

I could go on but I am pretty sure you are already tired of me :)

Summary

  • Ember is a modern js web framework
  • It provides a lot of utilities to fit your use case
  • it has good conventions but allows you to be flexible as well
  • EmberData is nice and it's not the only library for backend if you don't like it
  • In my experience if you work on adapters/serializers a bit you can have a very good ORM on top of HTTP
    • it lacks some features/consistency around associations though

Get started now!

(slides contain sometimes slightly different code from the repo for the sake of simplicity)

If you have any question or need help ping me on twitter (https://twitter.com/vasilakisfil)

or comment in the talk page so that other users see it as well :)

Questions ?

Thanks!

Building real world apps in EmberJS

By Filippos Vasilakis

Building real world apps in EmberJS

Ember Meetup, Stockholm, Sweden, May 2016

  • 1,128
Loading comments...

More from Filippos Vasilakis