Ember.js Tutorial

Building a complex web application with Ember.js Octane

Latest update:

Welcome! This is an Ember.js tutorial from the absolute beginner level. This tutorial is continuously improved and updated, it uses Ember Octane (v3.19). The work on documentation below is in progress and will be updated in the next couple of days to cover the latest Ember.js.

Please check the Live Demo page and play with the app what we are going to build together.

You can clone the original repository from GitHub and launch on your desktop anytime. (Already updated to Ember Octane v3.19)

If you have any comment, suggestion or you have a question, please feel free to contact me.

Contents

Prerequisites

Lesson 1

This tutorial uses the latest Ember CLI tool (v3.19).

Install Ember CLI

The following npm/yarn command installs Ember CLI version 3.19 in the global namespace. Ember CLI generates app with the Ember.js and Ember Data. (If you have an earlier version of Ember CLI, the following command automatically updates it to the latest.) Based on your preference, please use one of the following command.

$ yarn global add ember-cli

$ npm install -g ember-cli

If you use the latest Node.js a few alert message would show up on your console, but don’t worry about those, your app will work as expected. ;)

You have now a new ember command in your console. Check with

$ ember --version

You should see something similar:

version: 3.19
node: 14.4.0
os: darwin x64

(Node version, OS version may be different in your configuration.)

Please read more about Ember CLI here: cli.emberjs.com

Create the app

In your terminal, navigate in the folder where you usually create your web applications. For example, if you have a projects folder use

$ cd ~/projects

Inside this folder run the following command. (If you prefer npm package manager, please omit --yarn parameter.)

$ ember new library-app --yarn

This command will create the new app for you.

Please change to your new app directory with:

$ cd library-app

Open this folder in your favorite code editor and look around. You will see a few files and folders. Ember CLI is scaffolded for you everything what need to run and create an amazing web application.

Launch the app

Your skeleton app is ready and you can run it with the following command. Type in your terminal:

$ ember server

Open your new empty app in your browser: http://localhost:4200

If you use the latest Ember v3.19, you should see a page with our happy Tomster and “Congratulations, you made it”. This is the Ember Welcome Page.

Well Done! You have your first Ember.js application. :)

You should see the following lines in your ./app/templates/application.hbs:

{{!-- The following component displays Ember's default welcome message. --}}
<WelcomePage />
{{!-- Feel free to remove this! --}}

{{outlet}}

As it suggested, feel free to remove the first three lines and update it with your own welcome message. ;)

For example, open this file in your favorite editor and add the following html code above the outlet line.

<h1>Welcome to Ember</h1>

{{outlet}}

With this step, you’ve just learned, how can you create a custom home page. You are amazing! ;) Make sure that the development server is still running in your console:

$ ember server

Reload your app in your browser. Our favorite Tomster disappeared but we have a clean welcome message in our amazing app.

You can open Ember Inspector in your browser. Hope you’ve already installed it. Ember Inspector exists in Chrome and in Firefox as an extension. After installation you should have a new tab in your developer console in your browser. Check it out, look around. More details about Ember Inspector here.

For instance, you can click on Info tab in the Ember Inspector, where you can check the version number of Ember and Ember Data. Actually, this trick works on most of the website which built with Ember. Check out LinkedIn.com, Tvnz.co.nz or the famous Covid-19 Dashboard.

Add Bootstrap and Sass to Ember.js App

Let’s add some basic styling to our application. We use Bootstrap with Sass. Ember CLI can install addons and useful packages. These addons simplify our development process, because we don’t have to reinvent the wheel, we get more out of the box automatically. You can find various packages, addons on Ember Observer.

We are going to use this well rounded ember-bootstrap addon in our application.

Please exit your ember server with Ctrl+C in your terminal.

Run the following two commands:

$ ember install ember-bootstrap

You will see, that your ./package.json is extended with a couple of lines.

We also would like to use Sass as a CSS preprocessor. Our new ember addon can setup our application, just run the following:

$ ember generate ember-bootstrap --preprocessor=sass

It will create our main application scss file: ./app/styles/app.scss, but it does not delete the original app.css, so we have to do it manually:

$ rm ./app/styles/app.css

Relaunch your app with ember server or with yarn start. You should see in the browser, that ‘Welcome to Ember’ uses Bootstrap default font.

OMG! Have you realized how quickly you have a working application, with a custom home page and with a full Sass and Bootstrap support? Yep, you are amazing! ;)

Add navigation

We will use bootstrap navigation bar to create a nice header section for our app.

Update your main template file. Delete the example content and add the following code to your ./app/templates/application.hbs.

<div class="container">
  
  <nav class="navbar navbar-inverse">
    <div class="container-fluid">
      <div class="navbar-header">
        <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#main-navbar">
          <span class="sr-only">Toggle navigation</span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
        </button>
        {{#link-to 'index' class="navbar-brand"}}Library App{{/link-to}}
      </div>

      <div class="collapse navbar-collapse" id="main-navbar">
        <ul class="nav navbar-nav">
              {{#link-to 'index' tagName="li"}}<a href="">Home</a>{{/link-to}}
        </ul>
      </div><!-- /.navbar-collapse -->
    </div><!-- /.container-fluid -->
  </nav>

  {{outlet}}
</div>

Ember uses handlebar syntax in templates. It is almost the same as plain html, but you could have dynamic elements with {{}}.

Ember provides a bunch of useful handlebar helpers. The {{#link-to}}{{/link-to}} helps to create links. In this case we use as a “block helper”. The first parameter is the route name (index). Inside the block goes the label of the link. link-to uses <a> tag as default, but you can set up a different tag with the tagName property. We need this slightly hacky solution because of Bootstrap, however later we will implement a nicer component to manage navigation links.

The outlet helper is a general helper, a placeholder, where deeper level content will be inserted. The outlet in application.hbs means that almost all content from other pages will appear inside this section. For this reason, application.hbs is a good place to determine the main structure of our website. In our case we have a container div, a navigation bar, and the real content.

Launch your application with ember server. You should see your new navigation bar in your browser.

You can update your app.scss file to add some extra padding to the top. The updated ./app/styles/app.scss content:

@import "bootstrap";

body {
  padding-top: 20px;
}

Let’s create a new About page.

As you already realized, Ember CLI is an amazing tool and help us a lot. It has a generate command which can create our skeleton files during the development process. List all the options with ember generate --help.

Run the following command in your terminal

$ ember generate route about

A new route and template created in your project.

Open your new ./app/templates/about.hbs file in your editor, delete its content and add the following line:

<h1>About Page</h1>

You can launch your app with ember server and navigate to http://localhost:4200/about, where you should see the newly created About Page header message. If you click on Home in your menu bar, your page will be empty. Let’s fix that.

Create a new index template with the following command in your terminal:

$ ember generate template index

Open in your editor the newly generated ./app/templates/index.hbs file and add the following:

<h1>Home Page</h1>

If you launch your app, you should see the above message on your home page, however we still don’t have an About link in our menu bar.

Open your ./app/templates/application.hbs and add the following line to the ul section under the Home link:

{{#link-to 'about' tagName="li"}}<a href="">About</a>{{/link-to}}

Your ul section in application.hbs should look like this:

<ul class="nav navbar-nav">
  {{#link-to 'index' tagName="li"}}<a href="">Home</a>{{/link-to}}
  {{#link-to 'about' tagName="li"}}<a href="">About</a>{{/link-to}}
</ul>

If you check your app in the browser, you should see Home and About links in your menu bar. You can click on them and see how the page content and the url are changed. The active state of the link changes the style of the menu link automatically as well. However Bootstrap expect the active class in li and expects an anchor tag inside the li tag. For this reason we have to insert that empty anchor around menu labels. We will fix this with a nice component later.

Homework

Create a Contact page. Extend the navigation bar with a “Contact” menu item.

Lesson 2

Coming Soon homepage with an email input box

Let’s create a coming soon “jumbotron” on the home page with an email input box, where users can subscribe for a newsletter.

Only static html5 and style

Add a static jumbotron, an input box and a button to /app/templates/index.hbs.

<div class="jumbotron text-center">
  <h1>Coming Soon</h1>

  <br/><br/>

  <p>Don't miss our launch date, request an invitation now.</p>

  <div class="form-horizontal form-group form-group-lg row">
    <div class="col-xs-10 col-xs-offset-1 col-sm-6 col-sm-offset-1 col-md-5 col-md-offset-2">
      <input type="email" class="form-control" placeholder="Please type your e-mail address." autofocus="autofocus"/>
    </div>
    <div class="col-xs-10 col-xs-offset-1 col-sm-offset-0 col-sm-4 col-md-3">
        <button class="btn btn-primary btn-lg btn-block">Request invitation</button>
    </div>
  </div>

  <br/><br/>
</div>

Requirements

We would like to cover the following requirements:

isDisabled

We can add dynamic values to standard html properties using conditionals. We can use our controller to add or modify the value of a variable, which we use in our template. Check the following solution.

We use a boolean variable, let’s call it isDisabled, which will help us to turn on and off the disabled html attribute on our button. We have access to these variables in our controllers and in our templates.

From the official guide: “Each template has an associated controller: this is where the template finds the properties that it displays. You can display a property from your controller by wrapping the property name in curly braces.”

First, update your index.hbs template with this variable.

Add disabled property with {{isDisabled}} boolean variable.

<button disabled={{isDisabled}} class="btn btn-primary btn-lg btn-block">Request invitation</button>

Now we can create our index controller (ember g is the short version of ember generate):

$ ember g controller index

Please read more about Ember controllers here: https://guides.emberjs.com/v3.19.0/controllers/

Note: Ember.js still uses controllers, however there were rumors, that the controller layer will be deprecated and removed from Ember.js 3.0. It looks like controllers will stay with us for a while so don’t worry too much. For now, we will use controllers to practice some interesting features, but later we will refactor our app and move most of the view related logic inside components.

Add isDisabled property to the controller. Default value is true.

//app/controllers/index.js
import Controller from '@ember/controller';

export default Controller.extend({

  isDisabled: true

});

If you check your app, you will see that the button is disabled by default. We want to add some logic around this feature. We have to learn a couple of new Ember.js tricks for that.

Computed Properties and Observers

Computed Properties and Observers are important features of Ember.js. Please read more about it in the official guide first.

Please note, I will use the new, preferred syntax in our project. You could ask, was there some other syntax before? Yes.

Computed properties and observers still could be written in two ways, however the classic syntax will be deprecated soon, but it is important to know the “old” syntax and the “new” syntax, so when you see older project, you will recognise this pattern.

Previously .property() and .observes() were attached to the end of the functions. Nowadays we use Ember.computed() and Ember.observer() functions instead. (Two more things. From Ember v2.17 there is a shorter syntax also. From Ember v3.1 we can simplify further our code. Yey!) Let’s see in examples.

Old (with ES5 string concatenation):

//...
fullName: function() {
  return this.get('firstName') + ' ' + this.get('lastName');
}.property('firstName', 'lastName')
//...

New (with ES6 string interpolation, which uses back-tick, dollar sign and curly braces), using global import:

import Ember from 'ember';

//...

fullName: Ember.computed('firstName', 'lastName', function() {
  return `${this.get('firstName')} ${this.get('lastName')}`;
})
//...

Preferred short syntax from Ember v2.17, using direct import:

import { computed } from '@ember/object';

//...

fullName: computed('firstName', 'lastName', function() {
  return `${this.get('firstName')} ${this.get('lastName')}`;
})
//...

From Ember v3.1, it will work without .get() as well:

import { computed } from '@ember/object';

//...

fullName: computed('firstName', 'lastName', function() {
  return `${this.firstName} ${this.lastName}`;
})
//...

The computed() function could have more parameters. The first parameters are always variables/properties in string format; what we would like to use inside our function. The last parameter is a function(). Inside this function we will have access to the properties with this.get() or from Ember v3.1 just simply this.propertyName. For example, read firstName property with this.get('firstName') or (from Ember v3.1) this.firstName and update properties with this.set('firstName', 'someNewValue'). (Please note, we still have to use this.set() in the latest Ember also.)

From Ember version 2.17, we import directly the computed function instead of using the global Ember namespace. It means, you will use computed() mainly in your codebase and not Ember.computed().

From Ember version 3.1, we can omit .get() in computed properties. However, there are some special cases when we still have to use it. Please read more about this changes here: ES5 Getters for Computed Properties.

Back to our project. Let’s play with these new features.

Update the html code with input component syntax and add a value to the email input box.

Modify <input> line as follow in index.hbs, please note we changed our angle brackets to curly braces:

{{input type="email" value=emailAddress class="form-control" placeholder="Please type your e-mail address." autofocus="autofocus"}}

As you can see, we use the emailAddress variable, or in other words, a “property” where we would like to store the value of the input box.

If you type something in the input box, it will update this variable in the controller as well.

You can use the following code in your controller to demonstrate the differences between computed properties and observers:

//app/controllers/index.js
import { computed, observer } from '@ember/object';
import Controller from '@ember/controller';

export default Controller.extend({

  isDisabled: true,

  emailAddress: '',

  actualEmailAddress: computed('emailAddress', function() {
    console.log('actualEmailAddress function is called: ', this.get('emailAddress'));
  }),

  emailAddressChanged: observer('emailAddress', function() {
    console.log('observer is called', this.get('emailAddress'));
  })

});

Observers will always be called when the value of the emailAddress changes, while the computed property only changes when you go and use that property. Open your app in your browser, and activate Ember Inspector. Click on /# Routes section, find the index route, and in the same line, under the Controller column, you will see an >$E sign; click on it. Open the console in Chrome and you will see something like this: Ember Inspector ($E): Class {__nextSuper: undefined, __ember_meta__: Object, __ember1442491471913: "ember443"}

If you type the following in the console: $E.get('actualEmailAddress'), you should see the console.log output message defined above inside “actualEmailAddress”. You can try out $E.set('emailAddress', 'example@example.com') in the console. What do you see?

Please play with the above examples and try to create your own observers and computed properties.

isDisabled with Computed Property

We can rewrite our isDisabled with computed property as well.

// app/controllers/index.js
import { computed } from '@ember/object';
import Controller from '@ember/controller';

export default Controller.extend({

  emailAddress: '',

  isDisabled: computed('emailAddress', function() {
    return this.get('emailAddress') === '';
  })

});

There are a few predefined computed property functions, which saves you some code. In the following example we use Ember.computed.empty(), which checks whether a property is empty or not. Please note, from Ember version 2.17 you can directly import the predefined functions, so in our code we can use the short empty() function. Don’t forget to import it.

// app/controllers/index.js
import Controller from '@ember/controller';
import { empty } from '@ember/object/computed';

export default Controller.extend({

  emailAddress: '',

  isDisabled: empty('emailAddress')

});

Try out the above example in your code.

isValid

Let’s go further. It would be a more elegant solution if we only enabled our “Request Invitation” button when the input box contained a valid email address.

We’ll use the Ember.computed.match() or match() short computed property function to check the validity of the string. But isDisabled needs to be the negated version of this isValid computed property. We can use the Ember.computed.not() or not() for this.

// app/controllers/index.js
import Controller from '@ember/controller';
import { match, not } from '@ember/object/computed';

export default Controller.extend({

  emailAddress: '',

  isValid: match('emailAddress', /^.+@.+\..+$/),
  isDisabled: not('isValid')

});

Great, it works now as expected. You see, we can write really elegant code with Ember.js, can’t we? ;)

Adding our first Action

Great we have an input box and a button on our screen, but it does nothing at the moment. Let’s implement our first action.

Update the <button> line in index.hbs to read like this.

<button class="btn btn-primary btn-lg btn-block" disabled={{isDisabled}} {{action 'saveInvitation'}}>Request invitation</button>

You can try it out in your browser and see that if you click on the button, you will get a nice error message, alerting you that you have to implement this action in your controller. Let’s do that.

// app/controllers/index.js
import Controller from '@ember/controller';
import { match, not } from '@ember/object/computed';

export default Controller.extend({

  responseMessage: '',
  emailAddress: '',

  isValid: match('emailAddress', /^.+@.+\..+$/),
  isDisabled: not('isValid'),

  actions: {

    saveInvitation() {
      alert(`Saving of the following email address is in progress: ${this.get('emailAddress')}`);
      this.set('responseMessage', `Thank you! We've just saved your email address: ${this.get('emailAddress')}`);
      this.set('emailAddress', '');
    }
  }

});

If you click on the button, the saveInvitation action is called and shows an alert box, sets up a responseMessage property, and finally deletes the content of emailAddress.

We have to show the response message. Extend your template.

<!-- app/templates/index.hbs -->
<div class="jumbotron text-center">
   <h1>Coming Soon</h1>

   <br/><br/>

   <p>Don't miss our launch date, request an invitation now.</p>

   <div class="form-horizontal form-group form-group-lg row">
     <div class="col-xs-10 col-xs-offset-1 col-sm-6 col-sm-offset-1 col-md-5 col-md-offset-2">
       {{input type="email" value=emailAddress class="form-control" placeholder="Please type your e-mail address." autofocus="autofocus"}}
     </div>
     <div class="col-xs-10 col-xs-offset-1 col-sm-offset-0 col-sm-4 col-md-3">
       <button class="btn btn-primary btn-lg btn-block" {{action 'saveInvitation'}} disabled={{isDisabled}}>Request invitation</button>
     </div>
   </div>

   {{#if responseMessage}}
     <div class="alert alert-success">{{responseMessage}}</div>
   {{/if}}

   <br/><br/>

</div>

We use the {{#if}}{{/if}} handlebar helper block to show or hide the alert message. Handlebar conditionals are really powerful. You can use {{else}} as well.

Brilliant. You learned a lot about Ember.js and you have just implemented these great features.

Homework

It is time to practice what you have just learned.

Let’s start with a simple task. Instead of directly implement the “Coming Soon” header text in the app/templates/index.hbs, replace it with a property in the app/controllers/index.js. (Hint: Create a new simple property in the controller, for example: headerMessage: 'Comming Soon' and use this constant in the template with the double curly braces syntax: <h1>{{headerMessage}}</h1>.)

The next task is more complex. You already have an amazing Contact page, where we would like to add a contact form. Here’s what your solution should be able to do or should look like.

Hint: you already have a contact.hbs template but you need a controller for it, to manage its logic.

Bonus point if you can add validation to the textarea. One option: the textarea should not be empty. Another option: the length of the message has to be at least 5 characters long.

{{textarea class="form-control" placeholder="Your message. (At least 5 characters.)" rows="7" value=message}}

Short version of computed property for greater than or equal:

Ember.computed.gte('yourProperty', number)

(Please note, it is computed.gte and not computed.get.)

You can use the latest syntax with importing gte function directly:

import { gte } from '@ember/object/computed';

//...

isLongEnough: gte("yourProperty.length", 5),

//...

(Tip: You can get a string computed property length with .length. If your computed property is message, the length of that message is message.length.)

If you have two computed properties, and both must be true, you can use a third computed property to compute the and logic.

Ember.computed.and('firstComputedProperty', 'secondComputedProperty')

With the latest syntax:

import { and } from '@ember/object/computed';

//...

isBothTrue: and('firstComputedProperty', 'secondComputedProperty'),

//...

Please try to implement the above requirements. When you’re finished, you can check out my repository (the contact logic in my repository is already located at library-app/app/models/contact.js because of how we refactor things later, however you can see in this earlier commit, that validation was placed in the controller). I am pretty sure that your solution will be much better than mine. ;)

Lesson 3

Our first Ember.js Model

We ask for email addresses on the home page, but we don’t save them in the database at the moment. It is time to implement this feature in our website.

Let’s create our first model where we save email addresses for invitation. Type the following command in your command line.

$ ember g model invitation email:string

The above Ember Generator created a new model file (and a skeleton test file). Our first model:

// app/models/invitation.js
import DS from 'ember-data';

export default DS.Model.extend({
  email: DS.attr('string')
});

It means, if we use the invitation model, it will have an email property, which data type is a string.

Hope you read about store in The Official Guide. Let’s use it.

Update your app/controllers/index.js controller action. Instead of showing a useless alert message, we try to save our data.

// app/controllers/index.js
import Controller from '@ember/controller';
import { match, not } from '@ember/object/computed';

export default Controller.extend({

  headerMessage: 'Coming Soon',
  responseMessage: '',
  emailAddress: '',

  isValid: match('emailAddress', /^.+@.+\..+$/),
  isDisabled: not('isValid'),

  actions: {

    saveInvitation() {
      const email = this.get('emailAddress');

      const newInvitation = this.store.createRecord('invitation', { email: email });
      newInvitation.save();

      this.set('responseMessage', `Thank you! We have just saved your email address: ${this.get('emailAddress')}`);
      this.set('emailAddress', '');
    }
  }

}); 

Open the app in your browser, and open the browser’s console. Try to save an invitation email address on the home page. You will see an error message in the console.

Ember.js tried to send that data to a server, but we don’t have a server yet. Let’s build one.

Setup a server on Firebase

Important note if you use Ember v3.4 or later with EmberFire. Add ember-cli-shims to your project, otherwise you will see the following error in your console, when you try to launch your app: Uncaught Error: Could not find module 'ember' imported from 'emberfire/initializers/emberfire'. As you already know, adding a new addon to our project is quite easy. Run the following command in your project folder:

$ ember install ember-cli-shims

It is important to highlight, that EmberFire is compatible only with the latest LTS version of Ember. Probably the best if you keep using Ember v3.4 at the moment and hopefully Google release an updated version from EmberFire soon, so after we can update our Ember version as well.

What is Firebase? Firebase is a server and API service. Try it out and you will realize how simple and easy to use. http://firebase.google.com

  1. Create an account on Firebase website.
  2. You can learn more about EmberFire addon, which connects your Ember App to the Firebase service here:
  3. First, run the following command in your terminal to install EmberFire addon: ember install emberfire
  4. You will see instructions in the console. We have to manually add a few lines to our configuration file. Copy and paste those lines in config/environment.js.
  5. Go back to Firebase and create a project there. When your new Firebase project ready, click on “</> Add Firebase to your web app” icon on Overview page. Check those params in the popup window (“apiKey”, “authDomain”, etc.) and copy-paste the values in your config/environment.js file in your Ember application accordingly.
// config/environment.js

module.exports = function(environment) {
  let ENV = {
    modulePrefix: 'library-app',
    environment,
    rootURL: '/',
    locationType: 'auto',

    firebase: {
      apiKey: 'xyz',
      authDomain: 'YOUR-FIREBASE-APP.firebaseapp.com',
      databaseURL: 'https://YOUR-FIREBASE-APP.firebaseio.com',
      projectId: 'project-12341234',
      storageBucket: 'YOUR-FIREBASE-APP.appspot.com',
      messagingSenderId: '1234'
    },

    // if using ember-cli-content-security-policy
    contentSecurityPolicy: {
      'script-src': "'self' 'unsafe-eval' apis.google.com",
      'frame-src': "'self' https://*.firebaseapp.com",
      'connect-src': "'self' wss://*.firebaseio.com https://*.googleapis.com"
    },

    EmberENV: {
      FEATURES: { ...

Please note, that Firebase closes the database by default, you have to manually add permissions. Please read this section on Firebase website.

Change your database to public, so we don’t have to implement authentication in this stage. To do this, navigate to your Firebase Console, select your new app, click “Database” from the left menu and click on “Rules” tab. Change the content in the rules editor to read:

{
  "rules": {
    ".read": true,
    ".write": true
  }
}

Try out Request Invitation button again, check your browser’s console messages and open the Firebase website, and check your app dashboard. You will see, that the email address, which you just saved on your home page, is sent to Firebase and it is saved on the server.

Well done!

Please note that your database is public with the above settings. This is fine for practicing and learning. However you should implement authentication and using a closed database when you build your next real application. You can learn more about it in the official EmberFire Guide.

Promise and the this context in javascript (+ playing with ES5 and ES6 a little)

Promises are a unique asynchronous feature in javascript. Basically, they’re objects that haven’t completed yet, but are expected to in the future. You can read more about promises on the Mozilla website: https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Promise

In our code, we use a Promise: .save()

The save method on Ember Data Model is a Promise. It promises us that it is trying to save our data. It could be successful or maybe return with an error.

We can catch the result of a Promise with a chained .then(). In our example:

newInvitation.save().then(function(response) {
  console.log('Email address is saved.')
})

If the saving process is successful, ‘fulfilled’, then we will get back a response from the server, which we can catch in our function parameter.

We have to relocate the code that displays our success message to be inside this new function, because we would like to show the success message only when the data is actually saved.

If you simply copy and paste the snippet below (which uses ES5 JavaScript syntax), you will see that the code does not work as expected. (In the following snippet, I mix the ES5 function and ES6 string interpolation syntax. It is not nice.)

newInvitation.save().then(function(response) {
  this.set('responseMessage', `Thank you! We have just saved your email address: ${this.get('emailAddress')}`);
  this.set('emailAddress', '');
})

In javascript, this always points to the object that wraps around it. In the above example, this will be undefined because we are inside the function after the Promise. Please learn more about it here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this

This kind of problem is solved nicely in ES6/ES2015, the new JavaScript, which is what I mainly use in this tutorial. However, it’s not a problem if you see the old, traditional way also. We’ll look at it for just a second.

In ES5 syntax, we have to save the controller context (the controller’s this) in a local variable. We can set the controller’s this as the variable _that so we can use it inside our then.

In ES5 syntax our controller would look like this. (You don’t have to use this code in your project. I will show the preferred ES6/ES2015 version below.)

import Ember from 'ember';

export default Ember.Controller.extend({

    headerMessage: 'Coming Soon',
    responseMessage: '',
    emailAddress: '',

    isValid: Ember.computed.match('emailAddress', /^.+@.+\..+$/),
    isDisabled: Ember.computed.not('isValid'),

    actions: {

        saveInvitation: function() {

            var _that = this;
            var email = this.get('emailAddress');

            var newInvitation = this.store.createRecord('invitation', {
                email: email
            });

            newInvitation.save().then(function(response) {
                _that.set('responseMessage', "Thank you! We saved your email address with the following id: " + response.get('id'));
                _that.set('emailAddress', '');
            });
        }
    }
});

We save the this controller context in a _that local variable. We use this local variable inside our function after the save() Promise. The above example uses the response from Firebase and shows the id of the generated database record. The ES5 solution uses the var declaration a lot, but in ES2015 JavaScript, we don’t use var anymore; only let and const. Good bye var and goodbye _that! ;)

Let’s look at the cleaner ES2015 version. (You can use this code in your project.)

// app/controllers/index.js
import Controller from '@ember/controller';
import { match, not } from '@ember/object/computed';

export default Controller.extend({

  headerMessage: 'Coming Soon',
  responseMessage: '',
  emailAddress: '',

  isValid: match('emailAddress', /^.+@.+\..+$/),
  isDisabled: not('isValid'),

  actions: {

    saveInvitation() {
      const email = this.get('emailAddress');

      const newInvitation = this.store.createRecord('invitation', { email });

      newInvitation.save().then(response => {
        this.set('responseMessage', `Thank you! We saved your email address with the following id: ${response.get('id')}`);
        this.set('emailAddress', '');
      });

    }
  }

});

You can see the new => syntax here. We don’t need to use the function keyword so much. The context of the saveInvitation() method is automatically passed deeper, into our asynchronous callback function. Now you can just use a simple this. Much nicer and cleaner. Do you like it?

After save(), the model from the server will be sent to the callback as response. This model object will contain the id of our model. The id comes from our database. You can use it in the response message.

In ES2015 you can use shorthand property names also. Instead of writing { email: email }, you can just use { email }. As you see in the arrow function, you can omit braces if the function has only one param. Instead of .then((response) => {}), we use only .then(response => { }).

Great, our home page is ready.

Create an Admin page

We would like to list out from the database the persisted email addresses.

Let’s create a new route and page what we can reach with the following url: http://localhost:4200/admin/invitations

$ ember g route admin/invitations

Add this new page to the application.hbs with a dropdown.

<!-- app/templates/application.hbs -->
<nav class="navbar navbar-inverse">
  <div class="container-fluid">
    <div class="navbar-header">
      <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#main-navbar">
        <span class="sr-only">Toggle navigation</span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
      </button>
      {{#link-to 'index' class="navbar-brand"}}Library App{{/link-to}}
    </div>

    <div class="collapse navbar-collapse" id="main-navbar">
      <ul class="nav navbar-nav">
        {{#link-to 'index' tagName="li"}}<a href>Home</a>{{/link-to}}
        {{#link-to 'about' tagName="li"}}<a href>About</a>{{/link-to}}
        {{#link-to 'contact' tagName="li"}}<a href>Contact</a>{{/link-to}}
      </ul>

      <ul class="nav navbar-nav navbar-right">
        <li class="dropdown">
          <a class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
            Admin<span class="caret"></span>
          </a>
          <ul class="dropdown-menu">
          {{#link-to 'admin.invitations' tagName="li"}}<a href="">Invitations</a>{{/link-to}}
          </ul>
        </li>
      </ul>
    </div><!-- /.navbar-collapse -->
  </div><!-- /.container-fluid -->
</nav>

Add a table to app/templates/admin/invitations.hbs

<!-- app/templates/admin/invitations.hbs -->

<h1>Invitations</h1>

<table class="table table-bordered table-striped">
  <thead>
    <tr>
      <th>ID</th>
      <th>E-mail</th>
    </tr>
  </thead>
  <tbody>
  {{#each model as |invitation|}}
    <tr>
      <th>{{invitation.id}}</th>
      <td>{{invitation.email}}</td>
    </tr>
  {{/each}}
  </tbody>
</table>

We use the {{#each}}{{/each}} handlebar block helper to generate a list. The model variable will contain an array of invitations which we will retrieve from the server. Ember.js automatically populates responses from the server, however we have not implemented this step yet.

Let’s retrieve our data from the server using a Route Handler and Ember Data.

Add the following code to your app/routes/admin/invitations.js file:

// app/routes/admin/invitations.js
import Route from '@ember/routing/route';

export default Route.extend({

  model() {
    return this.store.findAll('invitation');
  }

});

Launch your app and check your table in Admin. What do you think? :)

CRUD interface for libraries

We will implement a new section in our app, where we can create new libraries and list the previously created data.

Firstly we create our library model.

$ ember g model library name:string address:string phone:string

Secondly we create our new route. At the moment we’ll do it without Ember CLI; just manually add the following lines to our router.js:

// app/router.js

import EmberRouter from '@ember/routing/router';
import config from './config/environment';

const Router = EmberRouter.extend({
  location: config.locationType,
  rootURL: config.rootURL
});

Router.map(function() {

  this.route('about');
  this.route('contact');

  this.route('admin', function() {
    this.route('invitations');
  });

  this.route('libraries', function() {
    this.route('new');
  });
});

export default Router;

Now we create 3 new templates. Our main libraries.hbs, a libraries/index.hbs for displaying the list, and a libraries/new.hbs for a library creation form.

$ ember g template libraries
$ ember g template libraries/index
$ ember g template libraries/new

Update your application.hbs main navigation section as follows.

<!-- app/templates/application.hbs -->
<ul class="nav navbar-nav">
  {{#link-to 'index' tagName="li"}}<a href="">Home</a>{{/link-to}}
  {{#link-to 'libraries' tagName="li"}}<a href="">Libraries</a>{{/link-to}}
  {{#link-to 'about' tagName="li"}}<a href="">About</a>{{/link-to}}
  {{#link-to 'contact' tagName="li"}}<a href="">Contact</a>{{/link-to}}
</ul>

Add a submenu to libraries.hbs

<!-- app/templates/libraries.hbs -->
<h1>Libraries</h1>

<div class="well">
  <ul class="nav nav-pills">
    {{#link-to 'libraries.index' tagName="li"}}<a href="">List all</a>{{/link-to}}
    {{#link-to 'libraries.new' tagName="li"}}<a href="">Add new</a>{{/link-to}}
  </ul>
</div>

{{outlet}}

Check your app; you should see a new menu item, and a submenu with two items under /libraries.

The other two templates should have the following content.

<!-- app/templates/libraries/index.hbs -->
<h2>List</h2>

{{#each model as |library|}}
  <div class="panel panel-default">
    <div class="panel-heading">
      <h3 class="panel-title">{{library.name}}</h3>
    </div>
    <div class="panel-body">
      <p>Address: {{library.address}}</p>
      <p>Phone: {{library.phone}}</p>
    </div>
  </div>
{{/each}}

We generate a list from our model which will be retrieved in the route. We are using panel style from bootstrap here.

<!-- app/templates/libraries/new.hbs -->
<h2>Add a new local Library</h2>

<div class="form-horizontal">
  <div class="form-group">
    <label class="col-sm-2 control-label">Name</label>
    <div class="col-sm-10">
      {{input type="text" value=model.name class="form-control" placeholder="The name of the Library"}}
    </div>
  </div>
  <div class="form-group">
    <label class="col-sm-2 control-label">Address</label>
    <div class="col-sm-10">
      {{input type="text" value=model.address class="form-control" placeholder="The address of the Library"}}
    </div>
  </div>
  <div class="form-group">
    <label class="col-sm-2 control-label">Phone</label>
    <div class="col-sm-10">
      {{input type="text" value=model.phone class="form-control" placeholder="The phone number of the Library"}}
    </div>
  </div>
  <div class="form-group">
    <div class="col-sm-offset-2 col-sm-10">
      <button type="submit" class="btn btn-default" {{action 'saveLibrary' model}}>Add to library list</button>
    </div>
  </div>
</div>

We use model as our value store. You will soon see that our model will be created in the route. The action attached to the submit button will call a saveLibrary function that we’ll pass the model parameter to.

In your app/routes folder create libraries folder and add two js files: index.js and new.js

// app/routes/libraries/index.js
import Route from '@ember/routing/route';

export default Route.extend({

  model() {
    return this.store.findAll('library');
  }

});

In the route above, we retrieve all the library records from the server.

// app/routes/libraries/new.js
import Route from '@ember/routing/route';

export default Route.extend({

  model() {
    return this.store.createRecord('library');
  },

  actions: {

    saveLibrary(newLibrary) {
      newLibrary.save().then(() => this.transitionTo('libraries'));
    },

    willTransition() {
      // rollbackAttributes() removes the record from the store
      // if the model 'isNew'
      this.controller.get('model').rollbackAttributes();
    }
  }
});

In the above route’s model method, we create a new record and that will be the model. It automatically appears in the controller and in the template. In the saveLibrary action we accept a parameter and we save that model, and then we send the application back to the Libraries home page with transitionTo.

There is a built-in Ember.js action (event) called willTransition that is called when you leave a page (route). In our case, we use this action to reset the model if we haven’t saved it in the database yet.

As you can see, we can access the controller from the route handler using this.controller, however we don’t have a real controller file for this route (/libraries/new.js). Ember.js’s dynamic code generation feature automatically creates controllers and route handlers for each route. They exists in memory. In this example, the model property exists in this “virtual” controller and in our template, so we can still “destroy” it.

Open your browser and please check out these automatically generated routes and controllers in Ember Inspector, under the “Routes” section. You will see how many different elements are dynamically created.

What is that nice one liner in our saveLibrary() method?

newLibrary.save().then(() => this.transitionTo('libraries'));

In ES2015, with the => syntax, if we only have one line of code (like return + something) we can use a cleaner structure, without curly braces and return.

Update: In a previous version we had the following code in willTransition().

willTransition() {
  let model = this.controller.get('model');

  if (model.get('isNew')) {
    model.destroyRecord();
  }
}

Thanks for Kiffin’s comment, we have a simpler solution. Using rollbackAttributes() is cleaner. It destroys the record automatically if it is new.

Homework

Option 1: Further improve your Contact Page.

  1. Create a contact model with email and message fields.
  2. Save the model on the server when someone clicks on the “Send” button on the Contact form. Update your contact.js controller to contain validations and actions.
  3. Create an Admin page under http://localhost:4200/admin/contacts
  4. List all saved messages in a table.

Option 2: Refactor your app’s contact section with usage of model in route.

  1. Move contact validation into the contact model.
  2. Copy and paste the actions from the controller to the route and try sending a message. Why don’t the message and email boxes clear? Refactor it so they do clear. You’ll also need to correct the same problem in the validation code you moved to the contact model.
  3. Remove the contact controller.

Lesson 4

Deploy your app using Firebase Hosting service

Follow the guide on Firebase:

$ npm install -g firebase-tools
$ ember build --prod
$ firebase login
$ firebase init

Questions and answers:

There is a new firebase.json file in your project folder, check it out:

{
  "database": {
    "rules": "database.rules.json"
  },
  "hosting": {
    "public": "dist",
    "rewrites": [
      {
        "source": "**",
        "destination": "/index.html"
      }
    ]
  }
}

And deploy:

$ firebase deploy

Check your app.

$ firebase open

Select Hosting: Deployed Site

It’s live! :-D Congratulations!

Add Delete, Edit button and Edit route

Upgrading the library list view to a grid view

Let’s upgrade our app/templates/libraries/index.hbs to show a nice grid layout. We have to tweak our stylesheet a little bit as well. You can see below that there are two buttons in panel-footer. The first button is a link to the Edit screen, and the second is a Delete button with the action deleteLibrary. We send library as a parameter to that action.

<!-- app/templates/libraries/index.hbs -->
<h2>List</h2>
<div class="row">
  {{#each model as |library|}}
    <div class="col-md-4">
      <div class="panel panel-default library-item">
        <div class="panel-heading">
          <h3 class="panel-title">{{library.name}}</h3>
        </div>
        <div class="panel-body">
          <p>Address: {{library.address}}</p>
          <p>Phone: {{library.phone}}</p>
        </div>
        <div class="panel-footer text-right">
          {{#link-to 'libraries.edit' library.id class='btn btn-success btn-xs'}}Edit{{/link-to}}
          <button class="btn btn-danger btn-xs" {{action 'deleteLibrary' library}}>Delete</button>
        </div>
      </div>
    </div>
  {{/each}}
</div>
# app/styles/app.scss
@import 'bootstrap';

body {
  padding-top: 20px;
}

html {
  overflow-y: scroll;
}

.library-item {
  min-height: 150px;
}

If you try to launch the app now, you’ll get an error message, because we haven’t implemented the libraries.edit route or the deleteLibrary action. Let’s implement these.

Duplicate some code, create edit.js and edit.hbs

Manually add the new edit route to router.js. We’ll set up a unique path: in the second parameter of this.route(). Because there is a : sign before the library_id, that part of the url will be copied as a variable, available as a param in our routes. For example, if the url is http://example.com/libraries/1234/edit, then 1234 will be passed as a param to the route so we can use it to fetch that specific model.

// app/router.js
import EmberRouter from '@ember/routing/router';
import config from './config/environment';

const Router = EmberRouter.extend({
  location: config.locationType,
  rootURL: config.rootURL
});

Router.map(function() {

  this.route('about');
  this.route('contact');

  this.route('admin', function() {
    this.route('invitations');
    this.route('contacts');
  });

  this.route('libraries', function() {
    this.route('new');
    this.route('edit', { path: '/:library_id/edit' });
  });
});

export default Router;

After inserting this extra line in our router, it’s time to create our app/routes/libraries/edit.js. You can use Ember CLI or you can create it manually. The code should look like the following. I’ll explain more below.

// app/routes/libraries/edit.js
import Route from '@ember/routing/route';

export default Route.extend({

  model(params) {
    return this.store.findRecord('library', params.library_id);
  },

  actions: {

    saveLibrary(library) {
      library.save().then(() => this.transitionTo('libraries'));
    },

    willTransition(transition) {

      let model = this.controller.get('model');

      if (model.get('hasDirtyAttributes')) {
        let confirmation = confirm("Your changes haven't saved yet. Would you like to leave this form?");

        if (confirmation) {
          model.rollbackAttributes();
        } else {
          transition.abort();
        }
      }
    }
  }
});

A lot of things happening here.

First of all, in the model function, we have a params parameter. params will contain that id from the url. We can access it with params.library_id. The this.store.findRecord('library', params.library_id); line downloads the single record from the server with the given id. The id comes from the url.

We added two actions as well. The first will save the changes and then redirect the user to the main libraries page.

The second event-action will be called when we are trying to leave the page, because we were redirected in the saveLibrary action or the user clicked on a link on the website. In the first case, the changes are already saved, but in the second case, the user may have modified something in the form but not saved it. This is known as “dirty checking”. We can read the model from the controller, and use Ember Model’s hasDirtyAttributes computed property to check whether something was changed in the model. If so, we popup an ugly confirmation window. If the user would like to leave the page, we just rollback the changes with model.rollbackAttributes(). If the user would like to stay on the page, we abort the transition with transition.abort(). You can see that we use the transition variable, which is initiated as a param in the willTransition function. Ember.js automatically provides this for us.

Our template is still missing. Let’s use our new.hbs and duplicate the code in edit.hbs, with a few changes. We will fix this problem later with reusable “components”, because code duplication is not elegant.

<h2>Edit Library</h2>

<div class="form-horizontal">
  <div class="form-group">
    <label class="col-sm-2 control-label">Name</label>
    <div class="col-sm-10">
      {{input type="text" value=model.name class="form-control" placeholder="The name of the Library"}}
    </div>
  </div>
  <div class="form-group">
    <label class="col-sm-2 control-label">Address</label>
    <div class="col-sm-10">
      {{input type="text" value=model.address class="form-control" placeholder="The address of the Library"}}
    </div>
  </div>
  <div class="form-group">
    <label class="col-sm-2 control-label">Phone</label>
    <div class="col-sm-10">
      {{input type="text" value=model.phone class="form-control" placeholder="The phone number of the Library"}}
    </div>
  </div>
  <div class="form-group">
    <div class="col-sm-offset-2 col-sm-10">
      <button type="submit" class="btn btn-default" {{action 'saveLibrary' model}}>Save changes</button>
    </div>
  </div>
</div>

If you launch your app, it should work; you are able to edit the information from a library. You can see the “dirty checking” if you modify the data in the form, and then click on a link somewhere (ex. a link on the menu) without saving the form data.

Add delete action

The delete action is still missing. Let’s update app/routes/libraries/index.js

// app/routes/libraries/index.js
import Route from '@ember/routing/route';

export default Route.extend({

  model() {
    return this.store.findAll('library');
  },

  actions: {

    deleteLibrary(library) {
      let confirmation = confirm('Are you sure?');

      if (confirmation) {
        library.destroyRecord();
      }
    }
  }

})

Homework

You can add delete buttons to the lists on your Admin pages, so you can delete invitations and contact messages. Another nice improvement would be to clean up app/controllers/index.js and add a createRecord in the model method of app/routes/index.js. It would be similar to the libraries/new.js route.

Lesson 5

Cleaning up our templates with components

First of all, please read more about Components in the Ember.js Guide: http://guides.emberjs.com/v3.19.0/components/defining-a-component/

We can generate a component with ember g component. Let’s create two components. One for the library panel, and one for forms.

$ ember g component library-item
$ ember g component library-item-form

Each command generates a javascript file and a handlebars file. The javascript part sits in the app/components/ folder, the template in app/templates/components/.

We can insert the following code into our library-item template.

<!-- app/templates/components/library-item.hbs -->
<div class="panel panel-default library-item">
    <div class="panel-heading">
        <h3 class="panel-title">{{item.name}}</h3>
    </div>
    <div class="panel-body">
        <p>Address: {{item.address}}</p>
        <p>Phone: {{item.phone}}</p>
    </div>
    <div class="panel-footer text-right">
      {{yield}}
    </div>
</div>

You can see that this code is quite similar to what we have in app/templates/libraries/index.hbs, however instead of model we use item.

The most important concept in terms of components is that they are totally independent from the context. They don’t know “other things”, other variables. They only know what they originally have and what’s passed inside with attributes.

We have a {{yield}} which means that we can use this component as a block component. The code that this component wraps around will be injected inside the {{yield}}.

For example:

{{#library-item item=model}}
  Closed
{{/library-item}}

In this case the Closed text will appear in the panel footer.

Please note that we can use Angle Bracket components from Ember v3.4. The classic handlebar components are not deprecated, so it is up to you which one would you prefer. However Angle Bracket components help to make your template more readable if you have a bigger project. We still use the classic handlebars in this tutorial, because they are compatible with the earlier version of Ember. Don’t hesitate to check out this announcment and learn more about Angle Bracket components.

Let’s add html to our library-item-form component as well.

<!-- app/templates/components/library-item-form.hbs -->
<div class="form-horizontal">
    <div class="form-group has-feedback {{if item.isValid 'has-success'}}">
        <label class="col-sm-2 control-label">Name*</label>
        <div class="col-sm-10">
          {{input type="text" value=item.name class="form-control" placeholder="The name of the Library"}}
          {{#if item.isValid}}<span class="glyphicon glyphicon-ok form-control-feedback"></span>{{/if}}
        </div>
    </div>
    <div class="form-group">
        <label class="col-sm-2 control-label">Address</label>
        <div class="col-sm-10">
          {{input type="text" value=item.address class="form-control" placeholder="The address of the Library"}}
        </div>
    </div>
    <div class="form-group">
        <label class="col-sm-2 control-label">Phone</label>
        <div class="col-sm-10">
          {{input type="text" value=item.phone class="form-control" placeholder="The phone number of the Library"}}
        </div>
    </div>
    <div class="form-group">
        <div class="col-sm-offset-2 col-sm-10">
            <button type="submit" class="btn btn-default" {{action 'buttonClicked' item}} disabled={{unless item.isValid true}}>{{buttonLabel}}</button>
        </div>
    </div>
</div>

This code is almost identical to the code we used multiple times in libraries/new.hbs and libraries/edit.hbs.

A tiny improvement is to add a little validation to our library model. Please update app/models/library.js to include this basic validation, where we check that the name is not empty. (Don’t forget to import Ember on the top of the file.)

// app/models/library.js
import DS from 'ember-data';
import { notEmpty } from '@ember/object/computed';

export default DS.Model.extend({

  name: DS.attr('string'),
  address: DS.attr('string'),
  phone: DS.attr('string'),

  isValid: notEmpty('name')
});

Time to clean up our templates using these new components.

Using the library-item component in app/templates/libraries/index.hbs reduces the amount of code and makes our template cleaner.

<h2>List</h2>
<div class="row">
  {{#each model as |library|}}
    <div class="col-md-4">
      {{#library-item item=library}}
        {{#link-to 'libraries.edit' library.id class='btn btn-success btn-xs'}}Edit{{/link-to}}
        <button class="btn btn-danger btn-xs" {{action 'deleteLibrary' library}}>Delete</button>
      {{/library-item}}
    </div>
  {{/each}}
</div>

We iterate through each library in our model, and then pass that library local variable as item to the library-item component. The name of the component’s variable is always on the left side of the assignment.

Because this component is a block component, we can add some extra content to the library item footer. In this case, we add Edit and Delete buttons.

If you check your app in a browser, the Libraries list page should look the same as before, however, the code is cleaner and we have a component that we can reuse elsewhere.

Update our app/templates/libraries/new.hbs.

<!-- app/templates/libraries/new.hbs -->
<h2>Add a new local Library</h2>

<div class="row">

  <div class="col-md-6">
    {{library-item-form item=model buttonLabel='Add to library list' action='saveLibrary'}}
  </div>

  <div class="col-md-4">
    {{#library-item item=model}}
      <br/>
    {{/library-item}}
  </div>

</div>

Let’s update app/templates/libraries/edit.hbs.

<h2>Edit Library</h2>

<div class="row">

  <div class="col-md-6">
    {{library-item-form item=model buttonLabel='Save changes' action='saveLibrary'}}
  </div>

  <div class="col-md-4">
    {{#library-item item=model}}
      <br/>
    {{/library-item}}
  </div>

</div>

Add the buttonClicked action to library-item-form.js.

import Component from '@ember/component';

export default Component.extend({
  buttonLabel: 'Save',

  actions: {

    buttonClicked(param) {
      this.sendAction('action', param);
    }

  }
});

Merge edit.hbs and new.hbs into form.hbs and use renderTemplate() and setupController()

edit.hbs and new.hbs are almost the same, so we can use the same template in both routes.

Let’s create a form.hbs which will be our common template in Edit and in New page.

<!-- /app/templates/libraries/form.hbs -->
<h2>{{title}}</h2>

<div class="row">

  <div class="col-md-6">
    {{library-item-form item=model buttonLabel=buttonLabel action='saveLibrary'}}
  </div>

  <div class="col-md-4">
    {{#library-item item=model}}
      <br/>
    {{/library-item}}
  </div>

</div>

To use the common template above, we have to do two things. First, we have to set the title and buttonLabel params in our controllers. Second, we have to let Ember know not to look for the default template based on the current route (e.x. in the route libraries/new, by default Ember looks for the template located at templates/libraries/new.hbs). To set controller params in a Route, we can use the setupController hook. For setting a non-default template location, we can use the renderTemplate hook.

With these two new hooks, our app/routes/libraries/new.js should look like this:

// app/routes/libraries/new.js
import Route from '@ember/routing/route';

export default Route.extend({

  model() {
    return this.store.createRecord('library');
  },

  setupController(controller, model) {
    this._super(controller, model);

    controller.set('title', 'Create a new library');
    controller.set('buttonLabel', 'Create');
  },

  renderTemplate() {
    this.render('libraries/form');
  },

  actions: {

    saveLibrary(newLibrary) {
      newLibrary.save().then(() => this.transitionTo('libraries'));
    },

    willTransition() {
      let model = this.controller.get('model');

      if (model.get('isNew')) {
        model.destroyRecord();
      }
    }
  }
});

And our edit.js

// app/routes/libraries/edit.js
import Route from '@ember/routing/route';

export default Route.extend({

  model(params) {
    return this.store.findRecord('library', params.library_id);
  },

  setupController(controller, model) {
    this._super(controller, model);

    controller.set('title', 'Edit library');
    controller.set('buttonLabel', 'Save changes');
  },

  renderTemplate() {
    this.render('libraries/form');
  },

  actions: {

    saveLibrary(library) {
      library.save().then(() => this.transitionTo('libraries'));
    },

    willTransition(transition) {
      let model = this.controller.get('model');

      if (model.get('hasDirtyAttributes')) {
        let confirmation = confirm("Your changes haven't saved yet. Would you like to leave this form?");

        if (confirmation) {
          model.rollbackAttributes();
        } else {
          transition.abort();
        }
      }
    }
  }
});

You can delete edit.hbs and new.hbs from the app/templates/libraries/ folder. We don’t need them anymore.

More information about setupController: http://emberjs.com/api/classes/Ember.Route.html#method_setupController

More information about renderTemplate: http://emberjs.com/api/classes/Ember.Route.html#method_renderTemplate

Time to clean up our application template. We’ll create a nice component that properly manages bootstrap navbar links.

Open your terminal and generate a new component with Ember CLI.

$ ember g component nav-link-to

Because we would like to just slightly modify the built-in LinkComponent, we should just extend that class. We can utilize the tagName property, which determines the main tag of a component.

Update app/components/nav-link-to.js:

// app/components/nav-link-to.js
import LinkComponent from '@ember/routing/link-component';

export default LinkComponent.extend({
  tagName: 'li'
});

Note: don’t forget to change the import statement from import Component from '@ember/component' to import LinkComponent from '@ember/routing/link-component' and Component.extend to LinkComponent.extend.

The corresponding nav-link-to template will be the following:

<!-- app/templates/components/nav-link-to.hbs -->
<a href="">{{yield}}</a>

Now we are ready to use our component in our navigation bar.

<!-- app/templates/application.hbs -->
<nav class="navbar navbar-inverse">
  <div class="container-fluid">
    <div class="navbar-header">
      <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#main-navbar">
        <span class="sr-only">Toggle navigation</span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
      </button>
      {{#link-to 'index' class="navbar-brand"}}Library App{{/link-to}}
    </div>

    <div class="collapse navbar-collapse" id="main-navbar">
      <ul class="nav navbar-nav">
        {{#nav-link-to 'index'}}Home{{/nav-link-to}}
        {{#nav-link-to 'libraries'}}Libraries{{/nav-link-to}}
        {{#nav-link-to 'about'}}About{{/nav-link-to}}
        {{#nav-link-to 'contact'}}Contact{{/nav-link-to}}
      </ul>

      <ul class="nav navbar-nav navbar-right">
        <li class="dropdown">
          <a class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
            Admin<span class="caret"></span>
          </a>
          <ul class="dropdown-menu">
            {{#nav-link-to 'admin.invitations'}}Invitations{{/nav-link-to}}
            {{#nav-link-to 'admin.contacts'}}Contacts{{/nav-link-to}}
          </ul>
        </li>
      </ul>
    </div><!-- /.navbar-collapse -->
  </div><!-- /.container-fluid -->
</nav>

Sidenote: If you have to solve this problem in a real application, I published an Ember Addon, which automatically adds this component to your project, it is more complex, please use that one. You can check the source code on Ember Bootstrap Nav Link repository.

Homework

You can try to extract the whole navigation bar segment to a separated component, so the application.hbs will be much much smaler.

Lesson 6

In this lesson, we’ll add the models book and author, and set up relations between models. We’ll also create a new page where we can generate dummy data with an external javascript library to fill up our database automatically.

Creating some new models and setting up relations

In our simple World, we have Libraries, and we have Authors (who can have a few Books). A Book can only be in one Library, and each Book has only one Author.

To generate the new models, we’ll use Ember CLI.

Run the following in your terminal.

$ ember g model book title:string releaseYear:date library:belongsTo author:belongsTo
$ ember g model author name:string books:hasMany

Now add a hasMany relation to the library model manually.

// app/models/library.js
import DS from 'ember-data';
import { notEmpty } from '@ember/object/computed';

export default DS.Model.extend({

  name: DS.attr('string'),
  address: DS.attr('string'),
  phone: DS.attr('string'),

  books: DS.hasMany('book'),

  isValid: notEmpty('name'),
});

Create a new Admin page ‘Seeder’ and retrieve multiple models in the same route.

Create a new page using Ember CLI in your terminal:

$ ember g route admin/seeder

Check router.js. A new route should be there which points to seeder.

// app/router.js
import EmberRouter from '@ember/routing/router';
import config from './config/environment';

const Router = EmberRouter.extend({
  location: config.locationType,
  rootURL: config.rootURL
});

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

  this.route('admin', function() {
    this.route('invitations');
    this.route('contacts');
    this.route('seeder');
  });
  this.route('libraries', function() {
    this.route('new');
    this.route('edit', { path: '/:library_id/edit' });
  });
});

export default Router;

Extend your application.hbs with the new page.

<!-- app/templates/application.hbs -->
<nav class="navbar navbar-inverse">
  <div class="container-fluid">
    <div class="navbar-header">
      <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#main-navbar">
        <span class="sr-only">Toggle navigation</span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
      </button>
      {{#link-to 'index' class="navbar-brand"}}Library App{{/link-to}}
    </div>

    <div class="collapse navbar-collapse" id="main-navbar">
      <ul class="nav navbar-nav">
        {{#nav-link-to 'index'}}Home{{/nav-link-to}}
        {{#nav-link-to 'libraries'}}Libraries{{/nav-link-to}}
        {{#nav-link-to 'about'}}About{{/nav-link-to}}
        {{#nav-link-to 'contact'}}Contact{{/nav-link-to}}
      </ul>

      <ul class="nav navbar-nav navbar-right">
        <li class="dropdown">
          <a class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
            Admin<span class="caret"></span>
          </a>
          <ul class="dropdown-menu">
            {{#nav-link-to 'admin.invitations'}}Invitations{{/nav-link-to}}
            {{#nav-link-to 'admin.contacts'}}Contacts{{/nav-link-to}}
            {{#nav-link-to 'admin.seeder'}}Seeder{{/nav-link-to}}
          </ul>
        </li>
      </ul>
    </div><!-- /.navbar-collapse -->
  </div><!-- /.container-fluid -->
</nav>

Using Ember.RSVP.hash() to retrieve multiple models in the same route

For downloading multiple models in the same route we have to use the Ember.RSVP.hash() function in the model hook.

RSVP.hash wraps multiple promises and returns a nicely structured hashed object. More information: http://emberjs.com/api/classes/RSVP.html#method_hash

We can import hash directly from rsvp package.

// app/routes/admin/seeder.js
import { hash } from 'rsvp';
import Route from '@ember/routing/route';

export default Route.extend({

  model() {
    return hash({
      libraries: this.store.findAll('library'),
      books: this.store.findAll('book'),
      authors: this.store.findAll('author')
    })
  },

  setupController(controller, model) {
    controller.set('libraries', model.libraries);
    controller.set('books', model.books);
    controller.set('authors', model.authors);
    
    this._super(controller, model);
  }
});

Ember.RSVP.hash tries to download all three requested models, and only returns with a fulfilled state if all are retrieved successfully.

In the setupController hook, we split up the models into their own controller property. (As a good practice, we finally call the this._super(controller, model) also.)

(Of course there are alternative solutions. One of them is omitting the setupController and in this case we would use model.libraries, model.books properties in our controller and template directly. Other option, if you have a controller, you can make an alias there and assign model.libraries to libraries, so other developers can immediately see where those property data come from with checking the Controller file only. Ex.: libraries: Ember.computed.alias('model.libraries').)

Short summary of route hooks

You’ve already used a couple of hooks in routes, like model and setupController. Route hooks are called in specific sequence.

You can use the following snippet to experiment with them in one of your routes.

import Route from '@ember/routing/route';

export default Route.extend({

  init() {
    debugger;
  },

  beforeModel(transition) {
    debugger;
  },

  model(params, transition) {
    debugger;
  },

  afterModel(model, transition) {
    debugger;
  },

  activate() {
    debugger;
  },

  setupController(controller, model) {
    debugger;
  },

  renderTemplate(controller, model) {
    debugger;
  }
});

If you visit the route where you inserted the code above and open your web browser’s inspector, the code will stop for debugging in each hook and you can see the order of the hooks. Here’s a list of the hooks in the order they’re called, and more information about each:

  1. init() http://emberjs.com/api/classes/Ember.Route.html#method_init
  2. beforeModel(transition) http://emberjs.com/api/classes/Ember.Route.html#method_beforeModel
  3. model(params, transition) http://emberjs.com/api/classes/Ember.Route.html#method_model
  4. afterModel(model, transition) http://emberjs.com/api/classes/Ember.Route.html#method_afterModel
  5. activate() http://emberjs.com/api/classes/Ember.Route.html#method_activate
  6. setupController(controller, model) http://emberjs.com/api/classes/Ember.Route.html#method_setupController
  7. renderTemplate(controller, model) http://emberjs.com/api/classes/Ember.Route.html#method_renderTemplate

Create a “number box” component to use on the admin/seeder page to display how much data we have in our database

We’ll make a number-box component to visualize numbers on our page. Let’s create our fancy component.

$ ember g component number-box

Setup css class names in the component controller.

// app/components/number-box.js
import Component from '@ember/component';

export default Component.extend({

  classNames: ['panel', 'panel-warning']

});

And a little html in our component template:

<!-- app/templates/components/number-box.hbs -->
<div class="panel-heading">
  <h3 class="text-center">{{title}}</h3>
  <h1 class="text-center">{{if number number '...'}}</h1>
</div>

As you can see, we can pass our component two attributes: title and number. If we are passed a value for number, we show that number, otherwise we show three dots.

Our component template is ready. We can use it in app/templates/admin/seeder.hbs.

<!-- app/templates/admin/seeder.hbs -->
<h1>Seeder, our Data Center</h1>

<div class="row">
  <div class="col-md-4">{{number-box title="Libraries" number=libraries.length}}</div>
  <div class="col-md-4">{{number-box title="Authors" number=authors.length}}</div>
  <div class="col-md-4">{{number-box title="Books" number=books.length}}</div>
</div>

If you open your browser now, you will see three boxes with numbers or three dots. Remember, we set up the libraries, authors, and books properties in our setupController hook. If our model hook downloaded our data from the server, those variables will not be empty. The .length method will return the size of that array. (As I mentioned above, you can skip remapping in setupController hook, in this case, you should use model.libraries.length, model.authors.length, etc. or as a third option you can add aliases in the controller.)

Building forms to generate dummy data

We have to generate two other components that we’re gonna use for the seeder page. Actually we’ll only use one component in the admin/seeder.hbs template, but inside that component we will use another component. This part is a little bit advanced; I don’t have a detailed explanation here, but you can copy paste the code and try out. I suggest playing around with the code to try to understand it, as well as checking out how it works in the demo. However, don’t forget, if you have any question, don’t hesitate to ping me on Slack, on Twitter or in the comment section below.

Run these Ember CLI commands in your terminal.

$ ember g component seeder-block
$ ember g component fader-label

Insert the following codes in your component javascript files and templates.

// app/components/seeder-block.js
import { lte, not, or } from '@ember/object/computed';
import Component from '@ember/component';

const MAX_VALUE = 100;

export default Component.extend({

  counter: null,

  isCounterValid: lte('counter', MAX_VALUE),
  isCounterNotValid: not('isCounterValid'),
  placeholder: `Max ${MAX_VALUE}`,

  generateReady: false,
  deleteReady: false,

  generateInProgress: false,
  deleteInProgress: false,

  generateIsDisabled: or('isCounterNotValid', 'generateInProgress', 'deleteInProgress'),
  deleteIsDisabled: or('generateInProgress', 'deleteInProgress'),

  actions: {

    generateAction() {
      if (this.get('isCounterValid')) {

        // Action up to Seeder Controller with the requested amount
        this.sendAction('generateAction', this.get('counter'));
      }
    },

    deleteAction() {
      this.sendAction('deleteAction');
    }

  }
});


<!-- app/templates/components/seeder-block.hbs -->
<div class="well well-sm extra-padding-bottom">
  <h3>{{sectionTitle}}</h3>
  
  <div class="form-inline">
  
   <div class="form-group has-feedback {{unless isCounterValid 'has-error'}}">
     <label class="control-label">Number of new records:</label>
     {{input value=counter class='form-control' placeholder=placeholder}}
   </div>
  
   <button class="btn btn-primary" {{action 'generateAction'}} disabled={{generateIsDisabled}}>
     {{#if generateInProgress}}
       <span class="glyphicon glyphicon-refresh spinning"></span> Generating...
     {{else}}
       Generate {{sectionTitle}}
     {{/if}}
   </button>
   {{#fader-label isShowing=generateReady}}Created!{{/fader-label}}
  
   <button class="btn btn-danger" {{action 'deleteAction'}} disabled={{deleteIsDisabled}}>
     {{#if deleteInProgress}}
       <span class="glyphicon glyphicon-refresh spinning"></span> Deleting...
     {{else}}
       Delete All {{sectionTitle}}
     {{/if}}
   </button>
   {{#fader-label isShowing=deleteReady}}Deleted!{{/fader-label}}
  </div>
</div>
// app/components/fader-label.js
import { later, cancel } from '@ember/runloop';
import { observer } from '@ember/object';
import Component from '@ember/component';

export default Component.extend({
  tagName: 'span',

  classNames: ['label label-success label-fade'],
  classNameBindings: ['isShowing:label-show'],

  isShowing: false,

  isShowingChanged: observer('isShowing', function() {

    // User can navigate away from this page in less than 3 seconds, so this component will be destroyed,
    // however our "setTimeout" task try to run.
    // We save this task in a local variable, so we can clean up during the destroy process.
    // Otherwise you will see a "calling set on destroyed object" error.
    this._runLater = later(() => this.set('isShowing', false), 3000);
  }),

  resetRunLater() {
    this.set('isShowing', false);
    cancel(this._runLater);
  },

  willDestroy() {
    this.resetRunLater();
    this._super(...arguments);
  }
});
<!-- app/templates/components/fader-label.hbs -->
{{yield}}

We need also a little scss snippet.

// app/styles/app.scss
@import 'bootstrap';

body {
  padding-top: 20px;
}

html {
  overflow-y: scroll;
}

.library-item {
  min-height: 150px;
}

.label-fade {
  margin: 10px;
  opacity: 0;
  @include transition(all 0.5s);
  &.label-show {
    opacity: 1;
  }
}

.extra-padding-bottom {
  padding-bottom: 20px;
}

// Spinner in a button
.glyphicon.spinning {
  animation: spin 1s infinite linear;
  -webkit-animation: spin2 1s infinite linear;
}

@keyframes spin {
  from { transform: scale(1) rotate(0deg); }
  to { transform: scale(1) rotate(360deg); }
}

@-webkit-keyframes spin2 {
  from { -webkit-transform: rotate(0deg); }
  to { -webkit-transform: rotate(360deg); }
}

We have our components, let’s insert them in seeder.hbs

<!-- app/templates/admin/seeder.hbs -->
<h1>Seeder, our Data Center</h1>

<div class="row">
 <div class="col-md-4">{{number-box title="Libraries" number=libraries.length}}</div>
 <div class="col-md-4">{{number-box title="Authors" number=authors.length}}</div>
 <div class="col-md-4">{{number-box title="Books" number=books.length}}</div>
</div>

{{seeder-block
 sectionTitle='Libraries'
 generateAction='generateLibraries'
 deleteAction='deleteLibraries'
 generateReady=libDone
 deleteReady=libDelDone
 generateInProgress=generateLibrariesInProgress
 deleteInProgress=deleteLibrariesInProgress}}

{{seeder-block
 sectionTitle='Authors with Books'
 generateAction='generateBooksAndAuthors'
 deleteAction='deleteBooksAndAuthors'
 generateReady=authDone
 deleteReady=authDelDone
 generateInProgress=generateBooksInProgress
 deleteInProgress=deleteBooksInProgress}}

Install faker.js for dummy data

To generate dummy data we have to install faker.js. https://github.com/johnotander/ember-faker

$ ember install ember-faker

We import faker in our models, where we extend each of our models with a randomize() function for generating dummy data.

Update your models with the followings.

// app/models/library.js
import { notEmpty } from '@ember/object/computed';
import DS from 'ember-data';
import Faker from 'faker';

export default DS.Model.extend({

  name: DS.attr('string'),
  address: DS.attr('string'),
  phone: DS.attr('string'),

  books: DS.hasMany('book', { inverse: 'library', async: true }),

  isValid: notEmpty('name'),

  randomize() {
    this.set('name', Faker.company.companyName() + ' Library');
    this.set('address', this._fullAddress());
    this.set('phone', Faker.phone.phoneNumber());

    // If you would like to use in chain.
    return this;
  },

  _fullAddress() {
    return `${Faker.address.streetAddress()}, ${Faker.address.city()}`;
  }
});

// app/models/book.js
import DS from 'ember-data';
import Faker from 'faker';

export default DS.Model.extend({

  title: DS.attr('string'),
  releaseYear: DS.attr('date'),

  author: DS.belongsTo('author', { inverse: 'books', async: true }),
  library: DS.belongsTo('library', { inverse: 'books', async: true }),

  randomize(author, library) {
    this.set('title', this._bookTitle());
    this.set('author', author);
    this.set('releaseYear', this._randomYear());
    this.set('library', library);

    return this;
  },

  _bookTitle() {
    return `${Faker.commerce.productName()} Cookbook`;
  },

  _randomYear() {
    return new Date(this._getRandomArbitrary(1900, 2015).toPrecision(4));
  },

  _getRandomArbitrary(min, max) {
    return Math.random() * (max - min) + min;
  }
});

// app/models/author.js
import { empty } from '@ember/object/computed';
import DS from 'ember-data';
import Faker from 'faker';

export default DS.Model.extend({

  name: DS.attr('string'),
  books: DS.hasMany('book', { inverse: 'author', async: true }),

  isNotValid: empty('name'),

  randomize() {
    this.set('name', Faker.name.findName());

    // With returning the author instance, the function can be chainable,
    // for example `this.store.createRecord('author').randomize().save()`,
    // check in Seeder Controller.
    return this;
  }
});

Add the following code in your environment.js file if you plan to deploy the app to firebase again. Without it your pages won’t load.

//config/environment.js
if (environment === 'production') {
  ENV['ember-faker'] = {
    enabled: true
  };
}

We will implement our actions in our controller.

$ ember generate controller admin/seeder
// app/controllers/admin/seeder.js
import { all } from 'rsvp';
import Controller from '@ember/controller';
import Faker from "faker";

export default Controller.extend({

  actions: {

    generateLibraries(volume) {

      // Progress flag, data-down to seeder-block where our lovely button will show a spinner...
      this.set('generateLibrariesInProgress', true);

      const counter = parseInt(volume);
      let savedLibraries = [];

      for (let i = 0; i < counter; i++) {

        // Collect all Promise in an array
        savedLibraries.push(this._saveRandomLibrary());
      }

      // Wait for all Promise to fulfill so we can show our label and turn off the spinner.
      all(savedLibraries)
        .then(() => {
          this.set('generateLibrariesInProgress', false);
          this.set('libDone', true)
        });
    },

    deleteLibraries() {

      // Progress flag, data-down to seeder-block button spinner.
      this.set('deleteLibrariesInProgress', true);

      // Our local _destroyAll return a promise, we change the label when all records destroyed.
      this._destroyAll(this.get('libraries'))

        // Data down via seeder-block to fader-label that we ready to show the label.
        // Change the progress indicator also, so the spinner can be turned off.
        .then(() => {
          this.set('libDelDone', true);
          this.set('deleteLibrariesInProgress', false);
        });
    },

    generateBooksAndAuthors(volume) {

      // Progress flag, data-down to seeder-block button spinner.
      this.set('generateBooksInProgress', true);

      const counter = parseInt(volume);
      let booksWithAuthors = [];

      for (let i = 0; i < counter; i++) {

        // Collect Promises in an array.
        const books = this._saveRandomAuthor().then(newAuthor => this._generateSomeBooks(newAuthor));
        booksWithAuthors.push(books);
      }

      // Let's wait until all async save resolved, show a label and turn off the spinner.
      all(booksWithAuthors)

        // Data down via seeder-block to fader-label that we ready to show the label
        // Change the progress flag also, so the spinner can be turned off.
        .then(() => {
          this.set('authDone', true);
          this.set('generateBooksInProgress', false);
        });
    },

    deleteBooksAndAuthors() {

      // Progress flag, data-down to seeder-block button to show spinner.
      this.set('deleteBooksInProgress', true);

      const authors = this.get('authors');
      const books = this.get('books');

      // Remove authors first and books later, finally show the label.
      this._destroyAll(authors)
        .then(() => this._destroyAll(books))

        // Data down via seeder-block to fader-label that we ready to show the label
        // Delete is finished, we can turn off the spinner in seeder-block button.
        .then(() => {
          this.set('authDelDone', true);
          this.set('deleteBooksInProgress', false);
        });
    }
  },

  // Private methods

  // Create a new library record and uses the randomizator, which is in our model and generates some fake data in
  // the new record. After we save it, which is a promise, so this returns a promise.
  _saveRandomLibrary() {
    return this.store.createRecord('library').randomize().save();
  },

  _saveRandomAuthor() {
    return this.store.createRecord('author').randomize().save();
  },

  _generateSomeBooks(author) {
    const bookCounter = Faker.random.number(10);
    let books = [];

    for (let j = 0; j < bookCounter; j++) {
      const library = this._selectRandomLibrary();

      // Creating and saving book, saving the related records also are take while, they are all a Promise.
      const bookPromise =
        this.store.createRecord('book')
          .randomize(author, library)
          .save()
          .then(() => author.save())

          // guard library in case if we don't have any
          .then(() => library && library.save());
      books.push(bookPromise)
    }

    // Return a Promise, so we can manage the whole process on time
    return all(books);
  },

  _selectRandomLibrary() {

    // Please note libraries are records from store, which means this is a DS.RecordArray object, it is extended from
    // Ember.ArrayProxy. If you need an element from this list, you cannot just use libraries[3], we have to use
    // libraries.objectAt(3)
    const libraries = this.get('libraries');
    const size = libraries.get('length');

    // Get a random number between 0 and size-1
    const randomItem = Faker.random.number(size - 1);
    return libraries.objectAt(randomItem);
  },

  _destroyAll(records) {

    // destroyRecord() is a Promise and will be fulfilled when the backend database is confirmed the delete
    // lets collect these Promises in an array
    const recordsAreDestroying = records.map(item => item.destroyRecord());

    // Wrap all Promise in one common Promise, RSVP.all is our best friend in this process. ;)
    return all(recordsAreDestroying);
  }
});

Lesson 7

CRUD interface for Authors and Books, managing model relationship

We are going to create two new pages: Authors and Books, where we list our data and manage them. We implement create, edit and delete functionality, search and pagination also. You can learn here, how could you manage relations between models. (Still work in progress, listing and editing author’s data is implemented so far.)

Let’s create our two new pages.

$ ember g route authors
$ ember g route books

These will add two new lines to our router.js.

this.route('authors');
this.route('books');

We have two new template files also: authors.hbs and books.hbs

You can find two new files in app/routes folder: authors.js and books.js

Let’s extend our navigation bar. Just add the following two lines to your application.hbs. I just inserted next to the Libraries menu point. (You can move the About and Contact menu point next to the Admin also.)

{{#nav-link-to 'authors'}}Authors{{/nav-link-to}}
{{#nav-link-to 'books'}}Books{{/nav-link-to}}

We are going to focus here on Authors page, we build it up together, after as a practice you can build up the Books section yourself. ;)

Check the Author model

First, check app/models/author.js file. If you’ve followed the Lesson 6 and added Faker, maybe you have some extra lines in your model, like randomize() method, however for us the most important is the Author model fields:

  name: DS.attr('string'),
  books: DS.hasMany('book', { inverse: 'author' }),

So we have a name field and a books field, which is related to the Book model. The inverse property is not really necessary, because we follow strictly the conventions, but I just leave there, so you can learn more about it in the official guide: https://guides.emberjs.com/v3.19.0/models/relationships/#toc_explicit-inverses

Download data from the server

We would like to list all authors, when the user visits Authors page, we have to download the data from the server. Let’s implement the model() hook in Authors’ route handler.

// app/routes/authors.js
import Route from '@ember/routing/route';

export default Route.extend({

  model() {
    return this.store.findAll('author');
  }
});

Now we can update the authors template file to list all data.

 <!-- app/templates/authors.hbs -->
 <h1>Authors</h1>

 <table class="table table-bordered table-striped">
   <thead>
     <tr>
       <th>Name</th>
       <th>Books</th>
     </tr>
   </thead>
   <tbody>
     {{#each model as |author|}}
       <tr>
         <td>{{author.name}}</td>
         <td>
           <ul>
             {{#each author.books as |book|}}
               <li>{{book.title}}</li>
             {{/each}}
           </ul>
         </td>
       </tr>
     {{/each}}
   </tbody>
 </table>

It’s a simple striped table with two columns. Because model array contains all the record about authors (we returned those in model() hook in the route handler and Ember automatically passes forward those records to controller and add to model property), so we can list them with an each block. Showing the author.name is simple.

The interesting part is using the author.books. Ember Data asynchronously downloads the related model if we use that information. We previously setup in our model/author.js file, that books field connect to Book model with a hasMany relationship. Ember Data fills that field with an array of records. We can use an each block to iterate them and list in our table. Check out in your app. (If you use Faker for generating book titles, you will see a few funny data there. :) )

Click on a name to edit Author’s name

Let’s improve further our template. At this stage we will add functionality to the main authors.hbs template, however when you feel that a template is getting too big, you can clean it up extracting functionality to components. We can do it later.

It would be cool if we would be able to edit an author’s name with clicking on it. It means, that an author would have two states: it is in editing mode and it is not. We can store this state in the model itself. It doesn’t have to be a “database” field, it is just a state in the memory. Let’s call it isEditing. In our each loop we can check author.isEditing true or false. When it is true we can show a form, else only the name.

When we show only the name, we wrap it in a simple <span> with an action which manages the click event. We implement the editAuthor action in our route handler.

When we edit the name, we have to manage saveAuthor action and cancelAuthorEdit. Additionally submit should be disabled if the new data is not valid.

Update your authors.hbs template:

<!-- app/templates/authors.hbs -->
<h1>Authors</h1>

<table class="table table-bordered table-striped">
  <thead>
  <tr>
    <th>
      Name
      <br><small class="small not-bold">(Click on name for editing)</small>
    </th>
    <th class="vtop">Books</th>
  </tr>
  </thead>
  <tbody>
  {{#each model as |author|}}
    <tr>
      <td>
        {{#if author.isEditing}}
          <form {{action 'saveAuthor' author on='submit'}} class="form-inline">
            <div class="input-group">
              {{input value=author.name class="form-control"}}
              <div class="input-group-btn">
                <button type="submit" class="btn btn-success" disabled={{author.isNotValid}}>Save</button>
                <button class="btn btn-danger" {{action 'cancelAuthorEdit' author}}>Cancel</button>
              </div>
            </div>
          </form>
        {{else}}
          <span {{action 'editAuthor' author}}>{{author.name}}</span>
        {{/if}}
      </td>
      <td>
        <ul>
          {{#each author.books as |book|}}
            <li>{{book.title}}</li>
          {{/each}}
        </ul>
      </td>
    </tr>
  {{/each}}
  </tbody>
</table> 

Implement isNotValid method in your model with adding an import and a computed property to app/models/author.js:

import { empty } from '@ember/object/computed';
...

isNotValid: empty('name'),

And add actions to app/routes/authors.js:

// app/routes/authors.js
import Route from '@ember/routing/route';

export default Route.extend({

  model() {
    return this.store.findAll('author');
  },

  actions: {

    editAuthor(author) {
      author.set('isEditing', true);
    },

    cancelAuthorEdit(author) {
      author.set('isEditing', false);
      author.rollbackAttributes();
    },

    saveAuthor(author) {

      if (author.get('isNotValid')) {
        return;
      }

      author.set('isEditing', false);
      author.save();
    }
  }
});

Check out your app. Click on a name, edit it, save or cancel. Hope everything works as expected.

Any time when we invoke an action, it passes the selected author record to that function as a parameter, so we can set isEditing on that record only.

In case of cancellation we revoke changes with rollbackAttributes.

The if - else block in the template helps to manage isEditing state.

It is a good practice wrapping input fields and buttons with form tag, because we can submit data with typing Enter in the input field. However, it works properly only if we add on='submit' to the action.

There are two new classes in our stylesheet also, please extend app/styles/app.scss:

.vtop {
  vertical-align: top!important;
}

.not-bold {
  font-weight: normal!important;
}

Now you can try to create a list about books on Books page, with a simple table where you list book.title and book.author.name. You can use the above logic to update a book title if you click on it. Good luck! :)

Under the hood

If you would like to see more information in your browser’s console about what Ember.js is doing under the hood, you can turn on a few debugging options in your configuration file.

You can find a list of debugging options in ./config/environment.js file. Remove the comment signs for the code to read as follows:

//..
if (environment === 'development') {
  // ENV.APP.LOG_RESOLVER = true;
  ENV.APP.LOG_ACTIVE_GENERATION = true;
  ENV.APP.LOG_TRANSITIONS = true;
  ENV.APP.LOG_TRANSITIONS_INTERNAL = true;
  ENV.APP.LOG_VIEW_LOOKUPS = true;
}
//..

Check your app and open the Console in Chrome/Firefox. You will see some extra information about what Ember.js actually does under the hood. Don’t worry if you don’t understand these debug messages at this stage. As you spend more and more time with Ember.js development, these lines are going to be clearer. (If you prefer to keep your development console clear, just comment out these debugging options. You can turn on and off them, whenever you like.)

TBC

Well done you folks who made it this far. Keep up the good work.

If you would like to know about new blog updates, please follow me on twitter.

Check out my new tutorial in the following GitHub repository’s README, work in progress: Product App

Contributors

Thanks for your help to improve further the most popular Ember tutorial:

Other tutorials, examples


The Best Way to Install Node.js with Yarn

There are a few ways to install Node.js, but it looks like only one way gives you the best experience for long term. Please find a few tips below how could you setup Yarn as well. On Mac The best...

How To Update Ember.js

You can keep your Ember application up-to-date with a few simple steps. Use ember-cli-update Update dependencies: $ cd your-project-directory $ npx ember-cli-update $ npm install Test your application, if you are happy commit these changes. Let’s run the “codemods”, so...