Blog post model

For our test application, we're going to create a blog. Let's start off by using a generator to create a model for the blogPost. We'll give it a couple of basic fields and take a look at what happens.

$ ember generate model blog-post title:string body:string
installing model
  create app/models/blog-post.js
installing model-test
  create tests/unit/models/blog-post-test.js

OK, Ember-CLI has just created for us both a model file in app/models and a test file in tests/unit/models. Let's take a look at the model and see what it contains:

import Model from 'ember-data/model';
import attr from 'ember-data/attr';

export default Model.extend({
  title: attr('string'),
  body: attr('string')
});

What is that funky syntax? import Model from 'ember-data/model and export default Model.extend()? Welcome to the world of tomorrow!

Those import and export statements use ECMAScript 6 module syntax. Thanks to the magic of transpilers, we can already use them today even though no browsers support ES6 yet. This should look familiar if you have used Node.js or AMD modules, there's just slightly different syntax. We're importing a module from 'ember-data/model' and calling it Model. Then we're extending the Model class and using that as our module export.

Our model specifies every field it should have, in this case title and body. If we later decide we want another field (perhaps a published date) we just need to add it to our model:

import Model from 'ember-data/model';
import attr from 'ember-data/attr';

export default Model.extend({
  title: attr('string'),
  body: attr('string'),
  publishedDate: attr('date')
});

Test our blog post model

Testing can seem daunting if you put it off for too long so lets get right to it and write a test for that model we just created. Ember-CLI has us covered. When we generated our blog post model Ember-CLI also generated a test module for our model:

$ ls tests/unit/models
blog-post-test.js

Pretty cool, huh? Let's open up tests/unit/models/blog-post-test.js and see what Ember-CLI generated for us:

import { moduleForModel, test } from 'ember-qunit';

moduleForModel('blog-post', 'Unit | Model | blog post', {
  // Specify the other units that are required for this test.
  needs: []
});

test('it exists', function(assert) {
  let model = this.subject();
  // let store = this.store();
  assert.ok(!!model);
});

That looks like a lot! First is the import statement. This imports the moduleForModel and test helpers we will need from ember-qunit for writing our test.

The first section you see, moduleForModel, is where any necessary loading for the model testing will be done. Each unit test is self-contained, so any dependencies (for example if one model depends on another) must be defined here. We don't need to worry about this for our simple blog post model yet.

The next section, test, shows how we define an individual test. One test can have many assertions but should test only one thing. The generator created a default test which asserts that our model exists.

Since we have about as much as we can test in here already for our small model, let's make sure the tests pass by visiting http://localhost:4200/tests in your browser.

Adding blog posts to the homepage

Create an index route

If we want to see blog posts on our website, we need to render them into our HTML. Ember's view layer places routes and their associated URLs front and center. The way to show something is to create a route and associated template.

Let's start once again from a generator, this time for our index page route:

$ ember generate route index
installing route
  create app/routes/index.js
  create app/templates/index.hbs
installing route-test
  create tests/unit/routes/index-test.js

ProTip™ If you ever need to know what generators are available, just type ember help generate and enjoy a deliciously long list of generating goodness.

This creates a few files for our index route and template file.

Looking at app/routes/index.js we see:

import Ember from 'ember';

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

Update an index template

Let's take a look at the template file that was generated for us in app/templates/index.hbs:

{{outlet}}

Just this funky thing called {{outlet}}. Ember.js uses handlebars for templating, and the outlet variable is a special variable that Ember uses to say "insert any subtemplates here". If you've done anything with Ruby on Rails, think yield and you'll be awfully close. We're not adding any subtemplates to our index template so let's remove the {{outlet}} and add a sample post:

<article>
  <header class="page-header">
    <h2>My Blog Post</h2>
  </header>
  <p>This is a test post.</p>
</article>

Go look at the website in your browser again. Our 'My Blog Post' header should appear nicely beneath our big site header.

Putting our posts on the page

So far we have only put some HTML on our page. Let's use the API to show our actual blog posts.

First let's add a model to our route in app/routes/index.js. One of the jobs of routes is to provide a model to their template. Our model should be a list of blog posts retrieved from our API.

We could manually provide list of blog posts as our model:

import Ember from 'ember';

export default Ember.Route.extend({
  model: function() {
    return [{
      title: "First post",
      body: "This is the post body."
    }];
  }
});

Instead let's use the data store to retrieve all of our blog posts:

import Ember from 'ember';

export default Ember.Route.extend({
  model: function() {
    return this.store.findAll('blog-post');
  }
});

Now we should update our index template to loop over each of our blog posts and render it:

{{#each model as |post|}}
  <article>
    <header class="page-header">
      <h2>{{post.title}}</h2>
    </header>
    <p>{{post.body}}</p>
  </article>
{{/each}}

The handlebars each helper allows us to enumerate over a list of items. This should print out all of our blog posts to the page. Let's check out our homepage in our browser again and make sure it worked.

Additional Blog post route(s)

What if we want to share a link to one of our blog posts? To do that, we would need a page for each blog post. Let's make those!

Create the route

Let's start by using a generator to make the new files we'll need:

$ ember generate route blog-post --path=/post/:blog_post_id
installing route
  create app/routes/blog-post.js
  create app/templates/blog-post.hbs
updating router
  add route blog-post
installing route-test
  create tests/unit/routes/blog-post-test.js

This creates a few files, and also adds some stuff to your app/router.js:

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

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

Router.map(function() {
  this.route('blog-post', {
    path: '/post/:blog_post_id'
  });
});

export default Router;

Here it has defined a route for us with a dynamic segment in the path, :blog_post_id. This dynamic segment will be extracted from the URL and passed into the model hook on the post route. We can then use this parameter to look up that exact blog-post in the data store. So let's open up app/routes/blog-post.js that was generated for us and do just that.

import Ember from 'ember';

export default Ember.Route.extend({
  model: function(params) {
    return this.store.find('blog-post', params.blog_post_id);
  }
});

Update the template

In order to make sure this is working, let's add some markup to app/templates/blog-post.hbs that will display a post. This file doesn't exist yet, so be sure to add it.

<article>
  <header class="page-header">
    <h1>{{model.title}}</h1>
  </header>
  <p>{{model.body}}</p>
</article>

Since we happen to know there is a blog post with id: 1 on our API server, we can manually visit http://localhost:4200/post/1 in our browser to test with an example blog post.

The magic of Ember-Data

Ember-Data's REST Adapter comes with some freebies to save us time and unnecessary code. The adapter that we are using, JSONAPIAdapter is an extension of the REST Adapter, so we get to take advantage of this automagic if our application follows the URL conventions expected of the REST Adapter.

Based on our route's dynamic URL segments the REST Adapter will make the proper calls to the application's API for the model hook.

Action HTTP Verb URL
Find GET /post/:blog_post_id
Find All GET /post
Update PUT /post/:blog_post_id
Create POST /post
Delete DELETE /post/:blog_post_id

ProTip™ The store action determines the model name based on the defined dynamic segment. In our example :blog_post_id contains the proper snake-case name for our model with the suffix _id appended.

To confirm that this works, delete the app/routes/blog-post.js file and verify that our blog post page (http://localhost:4200/post/1) still works properly after reload.

Ember-CLI created a route test file automatically as well. To make sure your tests continue to pass, if you delete app/routes/blog-post.js you should also delete tests/unit/routes/blog-post-test.js

Handlebars link-to helper

Now that we have unique URLs for each blog post, we can link to these URLs from our index route.

To add these links open up the app/templates/index.hbs file and add a link-to Handlebars helper around our blog title:

{{#each model as |post|}}
  <article>
    <header class="page-header">
      <h2>
        {{#link-to 'blog-post' post}} {{post.title}} {{/link-to}}
      </h2>
    </header>
    <p>{{post.body}}</p>
  </article>
{{/each}}

Now take a look at http://localhost:4200 and title links should appear. Click it! And now you're at the page for our blog post.

Acceptance testing

With some user interaction added to our application we can now create an acceptance test. The user flow for this test will be:

  1. Visit /
  2. Click the first blog link
  3. Verify that the URL now matches /post/:blog_post_id

First we will have to generate our acceptance test.

$ ember generate acceptance-test blog-post-show
installing acceptance-test
  create tests/acceptance/blog-post-show-test.js

Open the created file tests/acceptance/blog-post-show-test.js and see what is there:

import { test } from 'qunit';
import moduleForAcceptance from 'workshop/tests/helpers/module-for-acceptance';

moduleForAcceptance('Acceptance | blog post show');

test('visiting /blog-post-show', function(assert) {
  visit('/blog-post-show');

  andThen(function() {
    assert.equal(currentURL(), '/blog-post-show');
  });
});

Let's first rename this test to something more applicable and remove the stuff inside.

test('visit blog post from index', function(assert) {

});

There are a few helpers available to us that we will use a lot when writing acceptance tests.

  • visit(route): Visits the given route
  • click(selector or element): Clicks the element and triggers any actions triggered by that element's click event
  • andThen(callback): Waits for any preceding promises to continue

Since visit and click are both asynchronous helpers we need to wrap subsequent logic in andThen to make sure actions complete before continuing onto the next step.

Now we will code out the steps listed above to test that we can link to a blog post from index.

test('visit blog post from index', function(assert) {
  visit('/');
  let blogSelector = 'article:first-of-type a';

  andThen(function() {
    click(blogSelector);
  });

  andThen(function() {
    assert.equal(currentURL(), '/post/1');
  });
});

Verify the tests are passing by visiting http://localhost:4200/tests in the browser.