Ember.js Tutorial

Building a complex web application with the latest Ember.js 3

Welcome! This is an Ember.js tutorial from the absolute beginner level. (At the end of the course we touch some advanced topic.) This tutorial is up-to-date in 2019 as well and compatible with Ember v2 and Ember v3. Please note, we use Ember v3.4, which is the latest long term support version. (I would recommend to play with this version at the moment, because we will use Firebase in this course and the latest version of Firebase supports only Ember v3.4 or earlier.)

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

Lesson 1

This tutorial uses the latest long term supported Ember CLI tool (v3.4).

Install Ember CLI

The following npm command installs Ember CLI latest stable version in the global namespace. It generates app with the latest Ember.js and Ember Data. (If you have an earlier version of Ember CLI, the following command automatically updates it to the latest.)

$ npm install -g ember-cli@3.4

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

$ ember -v

You should see something similar:

version: 3.4
node: 10.15.1
os: darwin x64

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

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.

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 for us add-ons and useful packages. These add-ons simplify our development process, because we don’t have to reinvent the wheel, we get more out of the box. You can find various packages, add-ons on these websites: http://www.emberaddons.com or http://www.emberobserver.com

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;}

Create a new About page and add the link to the menu bar

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:

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.

Requirements

“Invite me” button should be inactive when the content in the input box is not a valid email address.

Show a response message after clicking on “Invite me” button.

Clear the input box after invitation has sent.

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.”

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.

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.

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.

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.

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.

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.

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.

In this contact form will be two fields; one field for an email address and another field for a text message.

There will be a “Send message” button.

This button should be active only if the email address field isn’t empty and is valid, and there is some message in the text box.

After clicking on the “Send message” button, an alert should appear with the email address and the message.

When you close the alert message, the form should be cleared and a success message should appear on the page in a green box. This message could be something like, “We got your message and we’ll get in touch soon”.

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.

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.

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

Create an account on Firebase website.

You can learn more about EmberFire addon, which connects your Ember App to the Firebase service here:

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.

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)

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','');})

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.)

importEmberfrom'ember';exportdefaultEmber.Controller.extend({headerMessage:'Coming Soon',responseMessage:'',emailAddress:'',isValid:Ember.computed.match('emailAddress',/^.+@.+\..+$/),isDisabled:Ember.computed.not('isValid'),actions:{saveInvitation:function(){var_that=this;varemail=this.get('emailAddress');varnewInvitation=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.)

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

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.

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><divclass="form-horizontal"><divclass="form-group"><labelclass="col-sm-2 control-label">Name</label><divclass="col-sm-10">{{inputtype="text"value=model.nameclass="form-control"placeholder="The name of the Library"}}</div></div><divclass="form-group"><labelclass="col-sm-2 control-label">Address</label><divclass="col-sm-10">{{inputtype="text"value=model.addressclass="form-control"placeholder="The address of the Library"}}</div></div><divclass="form-group"><labelclass="col-sm-2 control-label">Phone</label><divclass="col-sm-10">{{inputtype="text"value=model.phoneclass="form-control"placeholder="The phone number of the Library"}}</div></div><divclass="form-group"><divclass="col-sm-offset-2 col-sm-10"><buttontype="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 the route above, we retrieve all the library records from the server.

// app/routes/libraries/new.jsimportRoutefrom'@ember/routing/route';exportdefaultRoute.extend({model(){returnthis.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().

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.

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.

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.

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.jsimportRoutefrom'@ember/routing/route';exportdefaultRoute.extend({model(params){returnthis.store.findRecord('library',params.library_id);},actions:{saveLibrary(library){library.save().then(()=>this.transitionTo('libraries'));},willTransition(transition){letmodel=this.controller.get('model');if(model.get('hasDirtyAttributes')){letconfirmation=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><divclass="form-horizontal"><divclass="form-group"><labelclass="col-sm-2 control-label">Name</label><divclass="col-sm-10">{{inputtype="text"value=model.nameclass="form-control"placeholder="The name of the Library"}}</div></div><divclass="form-group"><labelclass="col-sm-2 control-label">Address</label><divclass="col-sm-10">{{inputtype="text"value=model.addressclass="form-control"placeholder="The address of the Library"}}</div></div><divclass="form-group"><labelclass="col-sm-2 control-label">Phone</label><divclass="col-sm-10">{{inputtype="text"value=model.phoneclass="form-control"placeholder="The phone number of the Library"}}</div></div><divclass="form-group"><divclass="col-sm-offset-2 col-sm-10"><buttontype="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

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.

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-itemitem=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 --><divclass="form-horizontal"><divclass="form-group has-feedback {{ifitem.isValid'has-success'}}"><labelclass="col-sm-2 control-label">Name*</label><divclass="col-sm-10">{{inputtype="text"value=item.nameclass="form-control"placeholder="The name of the Library"}}{{#ifitem.isValid}}<spanclass="glyphicon glyphicon-ok form-control-feedback"></span>{{/if}}</div></div><divclass="form-group"><labelclass="col-sm-2 control-label">Address</label><divclass="col-sm-10">{{inputtype="text"value=item.addressclass="form-control"placeholder="The address of the Library"}}</div></div><divclass="form-group"><labelclass="col-sm-2 control-label">Phone</label><divclass="col-sm-10">{{inputtype="text"value=item.phoneclass="form-control"placeholder="The phone number of the Library"}}</div></div><divclass="form-group"><divclass="col-sm-offset-2 col-sm-10"><buttontype="submit"class="btn btn-default"{{action'buttonClicked'item}}disabled={{unlessitem.isValidtrue}}>{{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.)

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><divclass="row"><divclass="col-md-6">{{library-item-formitem=modelbuttonLabel='Add to library list'action='saveLibrary'}}</div><divclass="col-md-4">{{#library-itemitem=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.jsimportRoutefrom'@ember/routing/route';exportdefaultRoute.extend({model(){returnthis.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(){letmodel=this.controller.get('model');if(model.get('isNew')){model.destroyRecord();}}}});

And our edit.js

// app/routes/libraries/edit.jsimportRoutefrom'@ember/routing/route';exportdefaultRoute.extend({model(params){returnthis.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){letmodel=this.controller.get('model');if(model.get('hasDirtyAttributes')){letconfirmation=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.

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.

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.

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.

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:

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.jsimport{lte,not,or}from'@ember/object/computed';importComponentfrom'@ember/component';constMAX_VALUE=100;exportdefaultComponent.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 amountthis.sendAction('generateAction',this.get('counter'));}},deleteAction(){this.sendAction('deleteAction');}}});

// app/components/fader-label.jsimport{later,cancel}from'@ember/runloop';import{observer}from'@ember/object';importComponentfrom'@ember/component';exportdefaultComponent.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;@includetransition(all0.5s);&.label-show{opacity:1;}}.extra-padding-bottom{padding-bottom:20px;}// Spinner in a button.glyphicon.spinning{animation:spin1sinfinitelinear;-webkit-animation:spin21sinfinitelinear;}@keyframesspin{from{transform:scale(1)rotate(0deg);}to{transform:scale(1)rotate(360deg);}}@-webkit-keyframesspin2{from{-webkit-transform:rotate(0deg);}to{-webkit-transform:rotate(360deg);}}

Install faker.js for dummy data

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.jsimport{notEmpty}from'@ember/object/computed';importDSfrom'ember-data';importFakerfrom'faker';exportdefaultDS.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.returnthis;},_fullAddress(){return`${Faker.address.streetAddress()}, ${Faker.address.city()}`;}});

// app/models/author.jsimport{empty}from'@ember/object/computed';importDSfrom'ember-data';importFakerfrom'faker';exportdefaultDS.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.returnthis;}});

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.

// app/controllers/admin/seeder.jsimport{all}from'rsvp';importControllerfrom'@ember/controller';importFakerfrom"faker";exportdefaultController.extend({actions:{generateLibraries(volume){// Progress flag, data-down to seeder-block where our lovely button will show a spinner...this.set('generateLibrariesInProgress',true);constcounter=parseInt(volume);letsavedLibraries=[];for(leti=0;i<counter;i++){// Collect all Promise in an arraysavedLibraries.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);constcounter=parseInt(volume);letbooksWithAuthors=[];for(leti=0;i<counter;i++){// Collect Promises in an array.constbooks=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);constauthors=this.get('authors');constbooks=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(){returnthis.store.createRecord('library').randomize().save();},_saveRandomAuthor(){returnthis.store.createRecord('author').randomize().save();},_generateSomeBooks(author){constbookCounter=Faker.random.number(10);letbooks=[];for(letj=0;j<bookCounter;j++){constlibrary=this._selectRandomLibrary();// Creating and saving book, saving the related records also are take while, they are all a Promise.constbookPromise=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 timereturnall(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)constlibraries=this.get('libraries');constsize=libraries.get('length');// Get a random number between 0 and size-1constrandomItem=Faker.random.number(size-1);returnlibraries.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 arrayconstrecordsAreDestroying=records.map(item=>item.destroyRecord());// Wrap all Promise in one common Promise, RSVP.all is our best friend in this process. ;)returnall(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.)

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:

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.

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:

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:

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.)

There are a few ways to install Node.js, but it looks like only one way gives you the best experience for long term. On Mac The best way to install Node.js on Mac is nvm. https://github.com/creationix/nvm You have to have...