This is a walkthrough tutorial on how to get started with building a blog website using Ember-CLI. As we go along, you'll be getting your hands dirty with a real-world scenario of building a multi-page website for the fictitious travel company named Iconic Locations. This article is intended to be less high-level and more hands-on as it is meant to be followed in conjunction with the Ember.js Guides.
Prerequisites: I'm assuming you have already installed Ember-CLI on your machine along with the Chrome Ember Inspector.
If you get stuck at any point, feel free to browse my copy of the code at GitHub. For your convenience, I made a branch for each section of this article so you can jump in at any point.
Start at the Finish Line
Before we begin, let's look at a mockup of what we'll be building:
Index page:
Post page:
Here's a JSFiddle with the template HTML and CSS we'll be starting with:
What we have here is a blog page template made responsive by Bootstrap. We have an app bar, a header, an article section, and a footer that will have some dynamic controls on it. The process I have in mind here is that we'll start with this mockup and iteratively make parts of it work until we have a fully-working site. This way, we'll always have something to preview to make sure things are still working along the way.
Creating a project in Ember
Open up a terminal with node.js installed and type the following:
$ ember new iconic-locations
This instructs Ember-CLI to install a basic set of files you'll need to start your project. The part we will be doing most of the work is in the app
folder. Once it's done, go to the root directory of your application.
$ cd iconic-locations
From here you can start the app.
$ ember serve
It will output something like this:
version: 0.1.12 Livereload server on port 35729 Serving on http://0.0.0.0:4200/
Okay, it told us it's serving at http://0.0.0.0:4200
. Load that in your browser and you'll see:
It looks pretty plain, doesn't it? Let's add in our template next because we always want the project to look like the finished product. This will allow us to be able to turn stuff into the components, views and templates we need with minimal effort.
Add the Prototype Layout
Open app/templates/application.hbs
. This is a handlebars file that will contain our template. Let's start by replacing everything in this file with the contents of the HTML pane of the JSFiddle editor. (Don't worry about the {{outlet}}
tag just yet, we'll add it back once we need it.) Since we're running Ember-CLI at the moment, if you save the file the browser will automatically update with your changes. We've still got a few missing styles, so let's add them next.
Open the file app/styles/app.css
and copy the remaining template styles from the CSS pane of the JSFiddle editor. After the browser refreshes, it should now look like the template.
As a matter of tidying things up a bit, go back to the application template and move the script tags to just below the other script tags in app/index.html
. Now we're ready to start converting it to an Ember app.
Adding Routes and Templates
Let's add an index route template, which will be viewable from the starting directory and will list all the recent posts. Open up a second terminal window and change its current directory to the root iconic-locations
path. Now type the following:
$ ember generate route index
Now simply move the bottom section of the template from the app/templates/application.hbs
into app/templates/index.hbs
, replacing file's contents. The entire file should now look like this:
<div class="container foreground"> ... </div>
And at the bottom of the application.hbs
file, add an {{outlet}}
tag, so the template you just moved will continue to show when you are at your website's root URL.
At this point, your browser should still look like the starting template when you reload the page.
Now let's add a route for viewing an individual post. Ember.js generated a default route for the index page, so we haven't needed to add one until now. Open app/router.js
and add a resource inside the router's map method, like so:
... Router.map(function() { this.resource('post', { path: '/:post_id' }); }); ...
This adds a route named post, which will be accessible near the application's root directory at '/:post_id'
. If we didn't specify a path, it would be located at /post/:post_id
instead by default.
Since we don't want to display the full article text and header on every page, we should move those blocks into a separate template. In your terminal, type the following:
$ ember generate route post
This created a few files, one of them located at app/templates/post
. Go back to app/templates/application.hbs
and replace everything below the <div id="navbar">
section with an {{outlet}}
tag, while replacing the contents of the app/templates/post.hbs
file with the contents you removed.
To recap, your application template should now look like this:
<div id="navbar" class="navbar navbar-default navbar-page-title"> ... </div> {{outlet}}And the post template should look like this:
<header id="page-title" class="jumbotron background"> ... </header> <div class="container foreground"> ... </div> <footer> ... </footer>At this point, your web browser should look like this after it refreshes:
Configuring the Content Security Policy
In your browser, open up the developer tools. You may notice in the console that you're getting several red-colored
[Report Only]
warning messages that look like this:
This is a security feature built into modern web browsers to prevent cross site scripting (XSS) attacks, and Ember-CLI comes preinstalled with support for it. It generally works by having Ember tell your browser to get "permission" to use these particular files, URLs, or CSS styles, and to consider them as unsafe until told otherwise.
Open up
config/environment.js
and modify it as follows:... var ENV = { ... }; ENV.contentSecurityPolicy = { 'default-src': "'none'", 'script-src': "'self' https://code.jquery.com https://maxcdn.bootstrapcdn.com https://cdnjs.cloudflare.com", // Allow scripts from https://code.jquery.com, https://maxcdn.bootstrapcdn.com, and https://cdnjs.cloudflare.com 'font-src': "'self' https://maxcdn.bootstrapcdn.com https://cdnjs.cloudflare.com https://fonts.gstatic.com", // Allow fonts from https://maxcdn.bootstrapcdn.com, https://cdnjs.cloudflare.com and https://fonts.gstatic.com 'connect-src': "'self'", 'img-src': "'self'", 'style-src': "'self' 'unsafe-inline' https://maxcdn.bootstrapcdn.com https://cdnjs.cloudflare.com https://fonts.gstatic.com https://fonts.googleapis.com", // Allow styles from https://maxcdn.bootstrapcdn.com, https://cdnjs.cloudflare.com and https://fonts.googleapis.com 'media-src': "'self'" } if (environment === 'development') { ... ENV.contentSecurityPolicy['img-src'] += " http://lorempixel.com http://s3.amazonaws.com"; // Allow images from http://lorempixel.com and https://s3.amazonaws.com (Used for UIFaces) } ...This will configure subdomain-level permissions for different setting types. Note that I'm allowing the prototype profile photos from UIFaces only when the site is in development. When it goes live, we'll want to display something else.
NOTE: The bootstrap library requires the use of inline scripts and styles. This is why I'm allowing the
'unsafe-inline'
attribute on thestyle-src
property, and the'unsafe-eval'
attribute on thescript-src
property.If you reload your browser, the content security policy reports should all be gone from the console at this time.
Creating Models with Fixture Data
In your terminal, type the following:
$ ember generate model post title:string subtitle:string image:string content:string date-published:string author:belongs-to tags:has-manyThis created a model file at
app/models/post.js
that contains both its properties and relationships. However, we're going to be accessing authors and tags through their posts, so we'll want to modify the post model's relationships to load asynchronously:export default DS.Model.extend({ title: DS.attr('string'), subtitle: DS.attr('string'), image: DS.attr('string'), content: DS.attr('string'), datePublished: DS.attr('date'), author: DS.belongsTo('author', { async: true }), tags: DS.hasMany('tag', { async: true }) });Now let's add the fixture data. We'll be using the
reopenClass
method to handle this, so by the time we're done it should now look like the following:import DS from 'ember-data'; export default DS.Model.extend({ ... }).reopenClass({ FIXTURES: [ { id: 1, title: 'Snapper Rocks Surfing', subtitle: 'Surfing Away on Pennies a Day', content: '<p>HTML-formatted article text</p>', image: 'http://lorempixel.com/1000/570/sports/4/', datePublished: new Date(Date.parse("2015-02-12T13:15:00Z")), author: 1, tags: [1, 2, 3] }, { id: 2, title: 'The Best Sushi in St. Louis', subtitle: '', image: 'http://lorempixel.com/1000/570/food/8/', content: '<p>HTML-formatted article text</p>', datePublished: new Date(Date.parse("2015-02-07T16:21:00Z")), author: 2, tags: [1] }, ... ] });Note that the model says it
belongsTo
an author, and ithasMany
tags, let's create those models as well so we can load those in. Type the following:$ ember generate model author name:string email-address:string profile-image-url:string posts:has-manyNext, set up the file to look like the following:
import DS from 'ember-data'; export default DS.Model.extend({ name: DS.attr('string'), emailAddress: DS.attr('string'), profileImageUrl: DS.attr('string'), posts: DS.hasMany('post') }).reopenClass({ FIXTURES: [ { id: 1, name: 'Brian Barrett', emailAddress: 'BrainSBarrett@jourrapide.com', profileImageUrl: 'http://s3.amazonaws.com/uifaces/faces/twitter/cacique/73.jpg', posts: [1] }, { id: 2, name: 'Janice Collins', emailAddress: 'JaniceRCollins@dayrep.com', profileImageUrl: 'http://s3.amazonaws.com/uifaces/faces/twitter/visionarty/73.jpg', posts: [2] }, ... ] });Once again, enter this into the console to create a tags model:
$ ember generate model tag name:string posts:hasManyNow open the
app/models/tag.js
file and set it up to resemble the following:import DS from 'ember-data'; export default DS.Model.extend({ name: DS.attr('string'), posts: DS.hasMany('post') }).reopenClass({ FIXTURES: [ { id: 1, name: 'Travel', posts: [1,2,3,4,5] }, { id: 2, name: 'Surfing', posts: [1] }, ... ] });If all went well, the website should still load.
Loading the Fixture Data
Since we're going to be using fixtures for our data, we need to create a special application adapter so our website knows how to load them. In your console, type the following:
$ ember generate adapter applicationThen, open up the file at
app/adapters/application
. Note that Ember generated aDS.RESTAdapter
object for you. By default, adapters are primed for making Web requests to an external API. All we need to do here is change the type fromDS.RESTAdapter
toDS.FixtureAdapter
.Now that we have some fixtures available, let's start using them to dynamically display content in our templates. Since we'll be needing a list of recent posts in most pages (According to our mockup, it will be on both on our index page and on the footer on the post page), we'll just load that once from the application route. In your console, type the following:
$ ember generate route applicationIt will then ask you whether you want to overwrite the application template:
[?] Overwrite /Users/mattt/iconic-locations/app/templates/application.hbs? (Yndh)Just type
n
, which means no, since we want to keep our changes so far. This will create a file atapp/routes/application.js
which will be loaded into memory before anything else. Open the file and modify it as follows:import Ember from 'ember'; export default Ember.Route.extend({ model: function() { return this.get('store').find('post').then(function(posts) { return posts.sortBy('datePublished').reverseObjects(); }); } });Now we have access to a list of posts ordered from newest to oldest from anywhere in our application. We want our index route to be able to use it, so let's add that at
app/routes/index.js
:import Ember from 'ember'; export default Ember.Route.extend({ model: function() { return this.modelFor('application'); } });The post route will be more complicated since it needs a list of recent posts as well as an individual post to display. In cases like these we make use of RSVP.hash, which returns only when all of its contained promises are fulfilled. Modify
app/routes/post.js
as follows:import Ember from 'ember'; export default Ember.Route.extend({ model: function(params) { return this.get('store').find('post', params.post_id); } });Please take a moment to reload the website and make sure the index page still loads normally. We can also now go to
http://localhost:4200/1
, which will load our post template:
Rendering the Fixture Data
Now it's time to start making our templates show some fixture data. Let's start with the index page. Modify it as follows:
<div class="container foreground"> {{#each post in model}} <article class="article-preview"> <div class="row"> <div class="col-sm-2"> <h2 class="publish-day">12</h2> <div class="publish-month">February</div> </div> <div class="col-sm-10"> <h2 class="post-title">{{#link-to "post" post}}{{post.title}}{{/link-to}}</h2> <h3 class="post-subtitle">{{post.subtitle}}</h3> <div class="post-preview"><p>Are sentiments apartments decisively the especially alteration. Thrown shy denote ten ladies though ask saw. Or by to he going think order event music. Incommode so intention defective at convinced. Led income months itself and houses you. After nor you leave might share court balls.</p></div> <div class="author"> <a href="#"> <img {{bind-attr src="post.author.profileImageUrl"}} class="author-image"><span class="author-name">{{post.author.name}}</span> </a> </div> </div> </div> </article> {{/each}} </div>Here we loop through each article, print whatever we can about them, and link to the post route by passing in the Id of the one we want.
We've still got a couple more things yet to do here. We don't have a way to format the publish date yet, and we don't want to write the entire post in the preview, maybe just the first paragraph or so. Since that will take a bit of additional work, and I want to focus on just getting the templates to render what we have available in the models, so let's ignore that for now. We'll build that in the Building Custom Helpers section.
Open up the
app/templates/post.hbs
file and modify the header section:<header id="page-title" class="jumbotron background"> <div id="title-image" class="bg"> <img {{bind-attr src=model.image}} alt=""> </div> <div class="container"> <div class="horizontal-center vertical-center"> <h1 class="article-title">{{model.title}}</h1> {{#if model.subtitle}} <h2 class="article-subtitle">{{model.subtitle}}</h2> {{/if}} </div> </div> </header>Next, we can load the article:
<article> {{{model.content}}} <div class="horizontal-center"> <div class="author"> <a href="#"> {{#if model.author.profileImageUrl}}<img {{bind-attr src="model.author.profileImageUrl"}} class="author-image">{{/if}}<span class="author-name">{{model.author.name}}</span> </a> </div> <div class="date-published"> February 12, 2015 </div> </div> </article>Note that the
{{{model.content}}}
tag is wrapped with a triple bar syntax. This tells Ember that it's safe to render HTML tags. Also note that we're not converting the dates just yet. If we did, they'll just look likeThu Feb 12 2015 07:15:00 GMT-0600 (CST)
instead of the much prettierFebruary 12, 2015
that we want. We'll take care of that shortly.In the footer, our template doesn't yet have access to the list of recent posts because it's not being provided in the model. The solution for that is to load them through the controller, which we'll get to later. The only section left to consider that we need to modify is the tags list. Let's do that now:
<div class="tag-cont"> {{#each tag in model.tags}} <button type="submit" class="btn">{{tag.name}}</button> {{/each}} </div>Adding a Controller
Controllers are a great way to add business logic to our application. We have the "Older" and "Newer" buttons on our post page, that likely won't be used anywhere else on the site, so this could be considered worthy of a controller implementation.
In the console, enter the following:
$ ember generate controller postThis generates a controller at
app/controllers/posts.js
. Open the controller file and modify it as follows:import Ember from 'ember'; export default Ember.ObjectController.extend({ needs: ['application'], recentPosts: Ember.computed.alias('controllers.application.model'), previousPost: Ember.computed('model', 'recentPosts.@each', function() { var recentPosts, index; recentPosts = this.get('recentPosts'); index = recentPosts.indexOf(this.get('model')); return recentPosts.objectAt(index - 1); }), nextPost: Ember.computed('model', 'recentPosts.@each', function() { var recentPosts, index; recentPosts = this.get('recentPosts'); index = recentPosts.indexOf(this.get('model')); return recentPosts.objectAt(index + 1); }) });Note that we converted it into an
Ember.ObjectController
. Another type we have available is anEmber.ArrayController
. Now we have properties we can use to wire up the pager buttons in the post template:<ul class="pager"> {{#if previousPost}} <li class="previous">{{#link-to "post" previousPost}}← Older{{/link-to}}</li> {{else}} <li class="previous disabled"><a>← Older</a></li> {{/if}} {{#if nextPost}} <li class="next">{{#link-to "post" nextPost}}Newer →{{/link-to}}</li> {{else}} <li class="next disabled"><a>Newer →</a></li> {{/if}} </ul>Additionally, since we have a list of recentPosts given to us now, we can modify the recent posts section:
<div class="recent-posts-cont"> <ul class="image-list"> {{#each post in recentPosts}} <li class="table"> {{#link-to "post" post classNames="table-row"}} <div class="vertical-center"> <img {{bind-attr src="post.author.profileImageUrl"}} class="author-image"> </div> <div class="vertical-center article-title"> {{post.title}} </div> {{/link-to}} </li> {{/each}} </ul> </div>Here we loop through each one and link to the post page again by passing in the model of the target post.
Building Custom Helpers
Helpers are a tool for converting logic into compiled HTML or text for your output. They work best when turned into something generic and reusable, such as a date formatter. As it stands, there's a JavaScript library called Moment.js that's very close to what we need. In your console, type the following:
$ bower install moment --saveThis will install the moment.js library as a bower dependency. We can reference this in our app from the
Brocfile.js
file. Open it up and add the following import statement near the end of the file:... app.import('bower_components/moment/moment.js'); module.exports = app.toTree();NOTE: You'll need to restart the ember server that's been running in your first console window because it doesn't watch for changes in your dependencies. You may stop it simply with the
Ctrl + C
shortcut, and then rerunning the commandember serve
.Next we'll create the helper:
$ ember generate helper date-formatterNow modify the file
app/helpers/date-formatter.js
as follows:/* globals moment */ import Ember from 'ember'; export function dateFormatter(format, date) { if (typeof(format) !== 'string') { format = 'MMMM DD, YYYY'; } if (date == null) { date = Date.now(); } return moment(date).format(format); } export default Ember.Handlebars.makeBoundHelper(dateFormatter);This method takes an optional date format string, and a date value. We can now modify our index template to use it, like so:
... <h2 class="publish-day">{{date-formatter 'DD' post.datePublished}}</h2> <div class="publish-month">{{date-formatter 'MMMM' post.datePublished}}</div> ...Now if you reload the index page at
http://localhost:4200
the dates should be loading correctly from your fixtures.We still need to modify the date on the post page. Open the post template and modify the article date:
<div class="date-published"> {{date-formatter "MMMM DD, YYYY" model.datePublished}} </div>We now have all the dates formatted as they are in the mockups. Let's create another helper for the article previews on the index page:
$ ember generate helper text-previewOpen
app/helpers/text-preview.js
:import Ember from 'ember'; export function textPreview(text, minCutoff, maxCutoff) { var preview, cutoff; // Strip HTML from the article preview = Ember.$(text).text(); if (preview.length > maxCutoff) { // Cut off text near the end of a word cutoff = preview.lastIndexOf(' ', maxCutoff); if (cutoff < minCutoff) { cutoff = maxCutoff; } preview = preview.substr(0, cutoff) + '...'; } return preview; } export default Ember.Handlebars.makeBoundHelper(textPreview);This attempts to find a decent spot in the article to cut off a sentence, rather than mid-word, if possible. It takes a text string, and a cutoff range. In our
app/templates/index.hbs
file, we can now write:<div class="post-preview">{{text-preview post.content 200 250}}</div>If your text preview helper is working, it should look something like this:
I hope that by now you've got a good grasp on how to convert a web page prototype layout into a functional Ember-CLI website. I know there is a lot to do yet on the website, but after having gone through the Ember.js Guides and this walkthrough, you should be able to take it from here. Give yourself a pat on the back, and thanks for making it this far!