Building an Ember Bookstore App

This is part 2 of our Ember and Rails 5 bookstore app (Part 1)!

This guide is not Rails-specific and anyone can follow most of it. You’ll need, however, a running backend and data integration equivalent to that accomplished in part 1.

We have no user interface thus far. We set up the data models in both our client and API projects: Authors who publish Books through a Publisher and sell on our Bookstore. We connected client and API and interacted with data on the console.

In this episode we will “dress up” our data skeleton with a user interface, including some Ember Data knobs to help us improve the user experience.

Topics we will cover include basic routing, templates, components and even controllers! As well as fine-tuning caching, handling metadata, computed properties, query parameters and more.

I encourage you to understand these essential Ember concepts before we carry on.

Start engines!

This guide is Ember 2.x compatible. If you need to update, follow this guide.

$ cd bookstore-client
$ ember server --proxy http://localhost:3000
$ cd bookstore-api
$ bin/rails server --binding 0.0.0.0

Revisiting our Router

Ember is URL-driven. The Router object defines the URL structure and as such is an application’s main entry point.

If we open app/router.js we will notice that Ember CLI has appended routes for the resources we ember generated in part 1:

Router.map(function() {
  this.route('books');
  this.route('authors');
  this.route('publishing-houses');
});

One of Ember tenets is convention over configuration, so it will map the books URL to a BooksRoute. Ember will auto-generate an instance of the resource if it doesn’t exist.

Let’s use the path option to tell the Router that we want to list books at / and have an author page at /author/:author_id:

Router.map(function() {
  this.route('books', { path: '/' });
  this.route('author', { path: '/author/:author_id' });
});

Let’s inspect how our routes (highlighted) are laid out using the Ember Inspector:

As evidenced in the snapshot:

  • Every URL has an associated route
  • Every route has an associated controller
  • Every template has an associated controller

(If any of these does not exist, Ember will provide an auto-generated in-memory default version.)

Every property of a template is, literally, owned by its controller. Just like components!

In fact, since components can do that (and more) controllers are slowly being replaced by components. We still need controllers because we can’t send a model() from a route directly to a component. One day, a feature called routable components will be available in Ember. Which will mean the end of controllers.

For further clarification about these concepts, see Should we use controllers in Ember 2.0? and 5 Essential Ember 2.0 Concepts You Must Understand.

Let’s start selling!

Edit the books template to list the books:

{{!-- app/templates/books.hbs --}}

<ul>
  {{#each model as |book|}}
    {{book-cover book=book}}
  {{/each}}
</ul>

Wait – what exactly is model? It looks like it should be an array of book

Remember we said every template is backed by a controller? Well, model is a property of the controller. Who has set model on the controller you may ask? It’s the route’s model() hook!

Let’s modify it to return all books:

// app/routes/books.js

import Ember from 'ember';

export default Ember.Route.extend({
  model() {
    return this.store.findAll('book');
  }
});

The store.findAll call is asynchronous therefore it returns a Promise. Once the BooksRoute resolves the promise, it sets that resolved collection on the BooksController and the template rendering process begins. Makes sense?

But… what is book-cover?

It’s a component! Let’s create it right away:

$ ember generate component book-cover

Components are highly reusable modules of code that encapsulate UI and logic. The consist of a Handlebars template…

{{!-- app/templates/components/book-cover.hbs --}}

<li>
  <strong>{{book.title}}</strong><br><br>
  <em>by</em>
  {{book.author.name}}
</li>

Where does the book variable come from? Well, when we invoked the component we passed a book property, remember?

Its backing Javascript file (which acts a little bit like a controller) is located at app/components/book-cover.js. We’ll get back to this one later!

Do it in style

Before we scare ourselves off with the non-existent default style, let’s grab our app/styles/app.css from here.

Then modify the application template for a more adequate title and make it a link:

{{!-- app/templates/application.hbs --}}

<h2 id="title">
  {{link-to "Our Awesome Bookstore" 'books'}}
</h2>

{{outlet}}

Visit http://localhost:4200:

Awesome indeed!

But what’s a good bookstore to do? Sell books, of course!

Ember articles like this delivered straight to your inbox? Tell me where:

(A few e-mails per month. No BS. Unsubscribe anytime!)

Creating a modal for purchase

Like many modern checkout systems, we will display a modal so that the customer can purchase without leaving the current page.

We’ll use a practical add-on called ember-modal-dialog! Switch to the terminal and type:

$ ember install ember-modal-dialog

Adding {{#modal-dialog}} to the book-cover component as we’ll need to populate each modal with specific book information.

To open the modal, we define an open action when clicking on the book cover itself (li element). Pressing the Purchase button will close it:

{{!-- app/templates/components/book-cover.hbs --}}

<li {{action "open"}}>
  <strong>{{book.title}}</strong><br><br>
  <em>by</em>
  {{book.author.name}}
</li>

{{#if isShowingModal}}
  {{#modal-dialog close="close" clickOutsideToClose=true}}

    <div class="modal">
      <h3>Purchase confirmation</h3>
      You want to buy <strong>{{book.title}}</strong> by {{book.author.name}}.

      <p>
        <button {{action "close"}}>Purchase for ${{book.price}}!</button>
      </p>

      <p>
        <em>Thank you! We will e-mail you your e-book</em>
      </p>

    </div>

  {{/modal-dialog}}
{{/if}}

However, those actions do nothing. That is, unless we write handlers for them!

// app/components/book-cover.js

import Ember from 'ember';

export default Ember.Component.extend({

  actions: {

    open() {
      this.set('isShowingModal', true);
    },

    close() {
      this.set('isShowingModal', false);
    }
  }

});

Open? Show modal. Close? Hide modal. Pretty self explanatory! And it works great.

Blurring the background

Improving readability is a priority, so we are going to blur the background when the modal is active.

Essentially we need a way to communicate with the application template (the thing we have to blur). We are going to trigger an action on the application route which, in turn, will set a property on the application controller. Going through the code will give us a better idea.

For starters, we’ll install the fantastic ember-route-action-helper:

$ ember install ember-route-action-helper

So we can use it like:

{{!-- app/templates/books.hbs --}}

<ul>
  {{#each model as |book|}}
    {{book-cover book=book blurBackground=(route-action "blurBackground")}}
  {{/each}}
</ul>

This means that the component has a reference to the blurBackground function in the route! Yes, we can now trigger blurring the background from our modal.

// app/components/book-cover.js

import Ember from 'ember';

export default Ember.Component.extend({

  actions: {

    open() {
      this.set('isShowingModal', true);
      this.get('blurBackground')(true);
    },

    close() {
      this.set('isShowingModal', false);
      this.get('blurBackground')(false);
    }
  }

});

While we handle these calls in the route:

// app/routes/books.js

import Ember from 'ember';

export default Ember.Route.extend({

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

  actions: {
    blurBackground(blur) {
      this.controllerFor('application').set('blur', blur);
    }
  }

});

The actual, you know, blurring… it’s just wrapping the content with a CSS class:

{{!-- app/templates/application.hbs --}}

<h2 id="title">
  {{#link-to 'books'}}Our Awesome Bookstore{{/link-to}}
</h2>

<div class="{{if blur 'blur-background' ''}}">
  {{outlet}}
</div>

The background is now blurred and more opaque! Neat, huh?

The freshest price

We are opening a modal in which we show the price, initially loaded with each book. As an online shop, it is imperative to show the latest price to the customer!

Thus, we will make sure we get the latest price when opening the modal by reload()ing the model:

// app/components/book-cover.js

    // ...

    open() {
      this.get('book').reload().then(() => {
        this.set('isShowingModal', true);
        this.get('blurBackground')(true);
      });
    }

    // ...

Just for kicks (this is optional!) we can update the price of our first book “The Great Escape”.

$ cd bookstore-api
$ bin/rails c
Loading development environment (Rails 5.0.0.alpha)
irb(main):001:0> Book.first.update_attributes(price: 29.99)

Make sure you have the console open on the Network panel. When you click on the first book “The Great Escape”, you will see a new request:

It’s a GET /books/1… success! Even though Ember had this book in store, it executed the reload and it’s correctly displaying the price we just updated in the backend.

(Hadn’t we updated the price in Rails, we would’ve simply received an HTTP 304 Not Modified response and kept the same price.)

Thankfully, for cases in which a reload() needs to be more precise than “always reload the record”, Ember Data exposes adapter functions for advanced cache control.

These are shouldReloadAll (which governs the reload policy for findAll) and shouldReloadRecord (reload policy for findRecord).

Similarly, shouldBackgroundReload dictates findAll’s backgroundReload policy, and shouldBackgroundReloadRecord findRecord’s.

For more information about this API have a look at Force Ember Data to reload data from backend API and the docs.

Ember articles like this delivered straight to your inbox? Tell me where:

(A few e-mails per month. No BS. Unsubscribe anytime!)

Author page

Creating a page for an author is very simple. We will use our knowledge from the book listing. Only that now we will have one model (singular), not a collection of models (plural).

For this very reason, we will rename plural app/routes/authors.js to its singular counterpart app/routes/author.js, and app/templates/authors.hbs to app/templates/author.hbs. Ember CLI created the plural versions when we generated the authors resource in part 1.

// app/routes/author.js

import Ember from 'ember';

export default Ember.Route.extend({
  model(params) {
    return this.store.findRecord('author', params.author_id);
  }
});

{{!-- app/templates/author.hbs --}}

<h3>{{model.name}}</h3>

<strong>Biography</strong>: Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

<h4>Published titles:</h4>
<ul>
{{#each model.books as |book|}}
  {{book-cover book=book blurBackground=(route-action "blurBackground")}}
{{/each}}
</ul>

(Notice how we reused the book-cover component?)

Visiting http://localhost:4200/author/1 will show us the author page!

The author page ought to be fast

If you recall, in part 1 our UX team had asked us to optimize the author page. This pushed us to include an author’s books in the author API endpoint.

Following this optimization, and since authors rarely publish new books, we will override shouldReloadRecord and shouldBackgroundReloadRecord to only refresh the model if more than an hour has passed since loading it:

// app/adapters/application.js

import JSONAPIAdapter from 'ember-data/adapters/json-api';
import Ember from 'ember';

export default JSONAPIAdapter.extend({

  pathForType: function(type) {
    return Ember.String.pluralize(Ember.String.underscore(type));
  },

  shouldReloadRecord(store, snapshot) {
    return false;
  },

  shouldBackgroundReloadRecord(store, snapshot) {
    console.log("Calling shouldBackgroundReloadRecord");
    const loadedAt = snapshot.record.get('loadedAt');

    // if it was loaded more than an hour ago
    if (Date.now() - loadedAt > 3600000) {
      return true;
    } else {
      return false;
    }
  }

});

As we want to prevent eager loading of an author, shouldReloadRecord has to always return false!

Two questions you may be asking yourself:

  1. We want to apply this logic to an author – wouldn’t these overrides impact any model?
  2. snapshot.record.get('loadedAt'): what’s that?

About #1 you’re right! What we have to do now is create a specific adapter for author models:

$ ember generate adapter author

and, importantly, cut what we just put in the application adapter and paste it in the author adapter.

Regarding #2, we somehow need to know when this record was loaded. Since Ember Data still doesn’t support per-record metadata, which would be ideal for this case, we are going to use a property on the model that is set upon loading:

// app/models/author.js

import Publisher from './publisher';
import { hasMany } from 'ember-data/relationships';

export default Publisher.extend({
  books: hasMany('book', { async: true }),

  loadedAt: Ember.on('didLoad', function() {
    this.set('loadedAt', new Date());
  })

});

Let’s jump to http://localhost:4200/author/1!

Hmmm… we should see a “Calling shouldBackgroundReloadRecord” in the console, right? I see nothing.

The thing is, these are “reload” APIs which logically are called upon reload – not load. Fair enough.

{{!-- app/templates/components/book-cover.hbs --}}

<li {{action "open"}}>
  <strong>{{book.title}}</strong><br><br>
  <em>by</em>
  {{#link-to 'author' book.author class="author" }}{{book.author.name}}{{/link-to}}
</li>

{{!-- ... --}}

If we navigate to the homepage, we will see the author link! Going back and forth between author pages and books page is easy now. Remember: “Our Awesome Bookstore” is a link.

Fine but… shouldBackgroundReloadRecord is still not being called?!

Because we supplied a model to the link (book.author), Ember is not calling the model() hook and therefore no question arises about reloading data. Our fix is simple enough: pass in book.author.id so that model() gets called.

{{#link-to 'author' book.author.id class="author" }}{{book.author.name}}{{/link-to}}

Voilà, navigate back and forth… you see it now? I do!

Ember Data will issue a background request to refresh the model only after an hour. You can check this in the Network panel of your console. Powerful stuff!

Showing all books via metadata

The classic use case for metadata in the Ember World is pagination.

Our case is similar. We are going to limit the book listing to 5 books – unless a specific amount of books is requested.

As we want to have a “Show All” button below the (limited) book listing, we need to know the total amount of books in advance. The API will have to send our client metadata telling us the total number of books in the database.

Let’s code it. Starting by appending the button and adding its action handler in the route:

{{!-- app/templates/books.hbs --}}
...
<button {{action 'showAll'}}>Show All</button>

// app/routes/books.js

...
export default Ember.Route.extend({

  queryParams: {
    limit: {
      refreshModel: true
    }
  },

  model(params) {
    return this.store.query('book', params);
  },

  actions: {

    showAll() {
      this.transitionTo({ queryParams: { limit: total }}); // total?
    },
...

An action handler that came with extra lines of code! What’s this queryParams business?

It’s Ember’s implementation of query parameters. Query parameters are good fit for passing around UI state.

Are you interested in implementing pagination? Check out Pagination in Ember with a JSON API Backend!

Our idea is to transition to this same route, but include a parameter with the total books. I hope the refreshModel: true option is self-explanatory! Yes, when limit is supplied the model() hook will be called.

Notice that we changed findAll for query! As findAll does not accept query parameters for the XHR request.

Buuut… what about that total variable? It’s not defined anywhere.

Easy, that’s our next step. I hate to say it, but we are creating a controller… (our first one) the BooksController! This dude is required for making queryParams work.

Controllers are being deprecated yet we create one?

As we mentioned earlier, the idea behind controllers is one: to keep the state behind templates. And, at the moment of writing, indispensable to work with query parameters.

When routable component arrive the refactoring will be super easy. Instead of Ember.Controller.extend() we’ll be Ember.Component.extend()‘ing!

Enough purism! Generate the good ol’ controller and edit it:

$ ember generate controller books

// app/controllers/books.js

import Ember from 'ember';
const { computed } = Ember;

export default Ember.Controller.extend({

  queryParams: ['limit'],
  limit: 5,

  total: computed('model.meta', function() {
    return this.get('model.meta').total;
  }),

  showAll: computed('total', 'model', function() {
    return this.get('total') > this.get('model.length');
  })

});

Okay okay… what have we done here?

First, add our queryParams and their state as controller properties. Check.

Second, use computed properties to bind the model’s metadata (via model.meta) to our missing variable total! By the way, let’s update our total-less showAll action:

// app/routes/books.js
...
    showAll() {
      const total = this.controllerFor('books').get('total');
      this.transitionTo({ queryParams: { limit: total }});
    },
...

Lastly, a friendly showAll property that’ll allow us to display the “Show All” button only when it makes sense.

{{!-- app/templates/books.hbs --}}
...
{{#if showAll}}
  <button {{action 'showAll'}}>Show All</button>
{{/if}}

For this to work properly though, Rails needs a tiny update:

# app/controllers/books_controller.rb
...
def index
  @books = Book.limit(params[:limit])
  render json: @books, meta: { total: Book.count }
end

Restart the engines, and hit http://localhost:4200 once more. That’s our app for today… phew!

Wrapping up

I hope you could tag along with ease. Did you hit any roadblocks? Did you manage to get the app fully functional?

Remember that everything is available on Github: Bookstore client and Bookstore API.

Any questions? What part of the guide is the least clear?

Enjoyed this article? Don't miss my next one!

Leave me your e-mail for content that will help you master Ember.js fast:

(A few e-mails per month. No BS. Unsubscribe anytime!)

© 2016 Frank Treacy. All rights reserved.

Do you want to master Ember fast?

Leave me your e-mail for helpful updates delivered straight to your inbox.

(A few e-mails per month. No BS. Unsubscribe anytime!)