Alright, fellow fullstack developers. In the last few weeks I had the chance to dive into Ember.js - and I would like to give you a complete example of a blog application with Ember CLI for the frontend and Rails as backend server.

This article contains lots of code. I will not explain all of it in detail. I’ll just reference the sources that helped me to understand the aspects shown here. You will need to have basic experience in Rails and JavaScript to walk through this.

Rails Backend

Let’s get the started. First we create our Rails backend server. We use the rails-api gem to generate the server.

1
2
3
4
mkdir my-blog
cd my-blog
rails-api new blog-backend
cd blog-backend

Here, we generate the scaffold for posts and comments.

1
2
3
rails-api generate scaffold Post title:string body:text
rails-api generate scaffold Comment author:string body:text post:references
bundle exec rake db:migrate

Don’t forget to add the has_many relation to post model.

1
2
3
class Post > ActiveRecord::Base
  has_many :comments
end

To set up CORS we use a gem called rack-cors. It makes configuring CORS in a Rails project as easy as writing an initializer. So add this to your gem file:

1
gem 'rack-cors'

Run the bundler to install the new gem.

1
bundle

Add here is the initializer:

1
2
3
4
5
6
7
8
9
10
11
# config/initializer/cors.rb

# Be sure to restart your server when you modify this file.

# Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin AJAX requests

Rails.application.config.middleware.insert_before 0, "Rack::Cors" do
  allow do
    origins '*',
    resource '*',
    headers: :any,
    methods: [:get, :post, :put, :patch, :delete, :options, :head]
  end
end

Ember Data expects the transferred JSON data between frontend and backend to be in a certain format. To meet that format we have to update the controller actions in the posts controller and the comments controller. Read this and this to learn more about the JSON format in ember data and check this out as well.

Here’s the code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# posts_controller.rb

def index
  render json: { posts: Post.all, comments: Comment.all }, methods: :comment_ids
end

def show
  render json: { post: @post, comments: @post.comments }, methods: :comment_ids
end

def create
  @post = Post.new(post_params)
  if @post.save
    render json: { post: @post, comments: @post.comments }, methods: :comment_ids, status: :created, localtion: @post
  else
    render json: @post.errors, status: :unprocessable_entity
  end
end

# comments_controller.rb

def index
  render json: { comments: Comment.all }, methods: :post_id
end

def show
  render json: { comment: @comment }, methods: :post_id
end

def create
  @comment = Comment.new(comment_params)
  if @comment.save
    render json: { comment: @comment }, methods: :post_id, status: :created, location: @comment
  else
    render json: @comment.errors, status: :unprocessable_entity
  end
end

To have some test data, just create a post record and a comment record in the Rails console.

1
2
rails console
> Post.create(title: 'First Post', body: 'This is a great post.').comments.create(author: 'Lex Luthor', body: 'Yeah, right!')

And finally run your development server.

1
rails server

That’s it for the backend. The rest of this article will be all about the Ember application.

Ember Frontend

Alright, now let’s get to the really cool stuff. First of all you need to have ember-cli installed and then we’re moving on like this.

1
2
3
cd /path/to/my-blog
ember new blog-frontend
cd blog-frontend

Ok kids, security is a very important issue, but to keep this demo quick and simple we’ll remove the following line from the package.json.

1
"ember-cli-content-security-policy": "0.4.0",

Learn more about the content security policy here and here.

You can configure the URL of your backend inside the application adapter. So run

1
ember generate adapter application

to generate it and make it look like this

1
2
3
4
// app/adapters/application.js

export default DS.ActiveModelAdapter.extend({
  host: 'http://localhost:3000'
});

As you will probably know, this is the URL of your running Rails dev server. ;)

Now lets create models, templates and routes.

1
2
3
4
5
ember generate resource posts
ember generate resource comments
ember generate route post
ember generate route post/new
ember generate route post/comment/new

This will generate a bunch of files. I’ll leave it up to you to learn what is what. Check out the following links: Models, Controllers, Router Request Lifecycle, Routes, Templates.

Add titles to the following templates to see if the routing works correctly later on. Just replace the

1
{{outlet}}

with something like:

1
2
3
4
5
6
7
8
9
10
11
<!-- blog-frontend/app/templates/posts.hbs -->
<h3>Post index</h3>

<!-- blog-frontend/app/templates/post.hbs -->
<h3>Post show</h3>

<!-- blog-frontend/app/templates/post/new.hbs -->
<h3>Post new</h3>

<!-- blog-frontend/app/templates/post/comment/new.hbs -->
<h3>Comment new</h3>

Now let’s update the router. The generators already added some routes, but I learned from Andy Borsz’s blog post that it should be more like this.

1
2
3
4
5
6
7
8
// app/router.js

Router.map(function() {
  this.route('posts');
  this.route('post.new', { path: 'posts/new' });
  this.resource('post', { path: 'posts/:post_id' }, function() {
    this.route('comment.new', { path: 'comments/new' });
  });
});

You can run the development server and check out the generated paths.

1
ember serve

Install the Ember Inspector and visit the generated routes to see what already works.

Let’s move on. Now we add the model attributes according to the backend models.

1
2
3
4
5
6
7
8
9
10
11
12
13
// app/models/post.js

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

// app/models/comment.js

export default DS.Model.extend({
  author: DS.attr('string'),
  body: DS.attr('string'),
  post: DS.belongsTo('post')
});

Here comes first bit of functionality that actually reads data from the backend. Let’s implement the model function in the posts route. This will define what should be rendered in the post.hbs template. This and this will help you understand what happens here.

1
2
3
4
5
6
7
8
9
10
11
12
// app/routes/posts.js

export default Ember.Route.extend({
  model() {
    return this.get('store').findAll('post');
  },
  actions: {
    delete(post) {
      post.deleteRecord();
      post.save();
    }
  }
});

In the posts.hbs template we loop over the posts and render a simple li tag with the title and a link for deleting. We add a link to the ‘Post New’ page as well.

1
2
3
4
5
6
7
8
9
10
11
12
<h3>Posts Index</h3>
<ul>
  {{#each model as |post|}}
    <li>
      {{#link-to 'post' post}}
        {{post.title}}
      {{/link-to}}
      <button {{action 'delete' post}}>Delete</button>
    </li>
  {{/each}}
</ul>
{{#link-to 'post.new'}}New Post{{/link-to}}

Check out the index page in the browser.

1
http://localhost:4200/posts

You should already see the first post we created in the rails console. The delete button should work as well.

Now, let’s create the the detail page for one post. Just update post.hbs to this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{{#link-to 'posts'}}Back to the posts list{{/link-to}}
<h3>Post Show</h3>
<h2>{{model.title}}</h2>
<p>{{model.body}}</p>
<div>
  <strong>Comments( {{model.comments.length}} ):</strong>
  {{#each model.comments as |comment|}}
    <div>
      <strong>{{comment.author}} said:</strong>
      <p>{{comment.body}}</p>
      <button {{action 'deleteComment' comment}}>Delete Comment</button>
    </div>
  {{/each}}
</div>
<div>{{#link-to 'post.comment.new'}}Add comment{{/link-to}}</div>

Go to /posts/1 and see if it works!

And now let’s make the delete button work. Here is the post.js route.

1
2
3
4
5
6
7
8
9
// app/routes/post.js

export default Ember.Route.extend({
  actions: {
    deleteComment(comment) {
      comment.deleteRecord();
      comment.save();
    }
  }
});

Next we create a form to create a new post. This is the post/new.hbs template.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<h3>Post New</h3>
<form>
  <div>
    <label>Title:</label>
    {{input type="text" value=model.title}}
  </div>
  <div>
    <label>Body:</label>
    {{textarea rows="5" value=model.body}}
  </div>
  <div>
    <button {{action 'save'}}>Speichern</button>
    <button {{action 'cancel'}}>Abbrechen</button>
  </div>
</form>

To implement the action handlers and save the form data to the backend, we need to update the post/new.js route to this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// app/routes/post/new.js

export default Ember.Route.extend({
  model() {
    return {};
  },
  actions: {
    save() {
      const newPost = this.get('store').createRecord('post', this.currentModel);
      newPost.save().then((post) => {
        this.transitionTo('post', post);
      });
    },
    cancel() {
      this.transitionTo('posts');
    }
  }
});

Creating posts should work now. Go to /posts/new and try it out. Also, check the Rails logs to make sure the data is being saved correctly.

So far, so good. Are you still with me? We’re almost done. Moving on to the comments.

Here’s the template for the new comment form post/comment/new.hbs.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<h3>Comment New</h3>
<form>
  <div>
    <label>Author:</label>
    {{input type="text" value=model.author}}
  </div>
  <div>
    <label>Body:</label>
    {{textarea rows="5" value=model.body}}
  </div>

  <div>
    <button {{action 'save'}}>Speichern</button>
    <button {{action 'cancel'}}>Abbrechen</button>
  </div>
</form>

Now we have to implement the /post/comment/new.js route. It defines the model and handles the actions triggered in the comment form.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// app/routes/post/comment/new.js

export default Ember.Route.extend({
  model() {
    return {};
  },
  renderTemplate() {
    this.render('post.comment.new', { into: 'application' });
  },
  actions: {
    save() {
      const post = this.modelFor('post');
      const newComment = this.get('store').createRecord('comment', this.currentModel);
      newComment.set('post', post);
      newComment.save().then(() => {
        this.transitionTo('post', post);
      });
    },
    cancel() {
      this.transitionTo('post', this.modelFor('post'));
    }
  }
});

Read this to understand why we need the renderTemplate() function here.

You made it, you reached the end of this article. Creating posts and adding comments should work now. Yay! \o/

One last Note

I found it really exciting how fast and simple it has become to build a frontend application along with the backend server. In my opinion, Ember.js and Ember CLI in particular are indeed great tools to build ambitious web applications. You don’t have to put a puzzle together before you can start getting productive. On the other hand you spend quite some time trying to understand the Ember magic and why your code actually works. I hope this article helped you with your learning curve. ;)