Using Ember.js with Spree

For some time we’ve been wondering what would it be like to integrate Ember.js with a Spree Ecommerce application. In this post we’ll see how to create a simple Ember.js application that serves data from a Spree API endpoint.
In this post we’ll take a look at:
- choosing the right development tools for your app
- how to setup your Spree application to serve an Ember application
- a quick way to authenticate a Spree user to use the Ember application
- understanding the differences between Spree’s and Ember Data’s APIs
- coding a basic Ember application to show products
You can follow what’s happening in the blogbpost by playing around with the fully working example: spree-ember-example. It’s built with Spree 2.1 but both 2.0 and 1.3 should work flawlessly.
WARNING: this post is quite long as we’ll have to cover code for each of the steps needed. If you don’t like long posts, refer to the demo application linked above to get a full picture faster.
Choose your tools
If you’re coming from the Rails world you’ll feel out of place when you discover that there is no powerful environment out of the box to write code without worrying about trivial stuff.
By default you’ll have no development server, no template generators, no shared best practices for placing files or naming directories. Many tools are trying to overcome the lack of a complete development environment like the one Rails has. Expect to spend some time choosing what is right for you.
The big decision you have to make upfront is wether you want to run the Ember application as part of a Rails application or completely standalone. There are some pros and cons for each approach obviously.
What you choose between the two approaches is really up to your requirements and your personal preference. That said, even though it can be tempting to reinvent the wheel, there are a couple of projects that do that for you.
Integrated with Rails vs. Standalone
The tools we’re going to refer are ember-rails and Brunch/Ember App Kit; the first stands for the “in Rails” style while the other two are javascript development environments where Ember will sit on its own.
These are the main features and pain points:
-
Inside Rails (ember-rails):
- all the Rails features we love
- asset pipeline (with Ruby templating support)
- seeds/fixtures/factories
- many cool gems available
- testing can be done with Ruby
- huge mess of files/directories
- painless token based authentication
- effortless deploy
- messy if Rails also serves a “classic app”
-
Standalone (Brunch/Ember App Kit):
- setup in no time
- more room for choices
- testing js as intended (with js)
- easier multi-browser testing
- decoupled api design
- have to learn new stuff if you’ve never done js
- a little harder to deploy
- the cleanest solution for a big js app
In this article we’ll talk about ember-rails beacuse it makes sense to use the fastest way to get up and running as a Rails developer. That said, you should really check out Brunch and Ember App Kit because they’re really great tools and you’ll have the chance to take a peek at what the javascript community is doing. Our personal advice is: start with ember-rails then move to a standalone solution once the Ember application grows bigger.
Prepare your Spree application
If you’ve done your homework you should know by now how to create a Spree store. If you don’t read about how to get started.
You should have the basic Spree demo store, like this:
Once you have a full store up and running add to your Gemfile
:
#
# It will use Ember's latest stable version
#
gem 'ember-rails'
#
# We'll use Ember Data's latest beta version
#
gem 'ember-data-source', '1.0.0.beta.5'
and run bundle install
to install the gems.
You should also add which variant to use in each Rails environment and the paths we’ll be using to store the Ember application:
#
# Inside config/environments/development.rb
#
config.ember.variant = :development
config.ember.ember_path = 'lib/assets/javascripts/spree-ember-example'
config.handlebars.templates_root = 'spree-ember-example/templates'
#
# Inside config/environments/production.rb
#
config.ember.variant = :production
config.ember.ember_path = 'lib/assets/javascripts/spree-ember-example'
config.handlebars.templates_root = 'spree-ember-example/templates'
This way ember-rails will serve a non-minified-full-verbosity version of Ember in development and a minified-zero-verbosity version of Ember in production.
We’ll host the Ember application inside lib/assets/javascripts
so that
we don’t mess with Spree javascript assets. This requires some extra
configuration but in the end we’ll come up with a pretty clean solution.
Now we can bootstrap some files and directories:
# Bootstrap all the things!
rails generate ember:bootstrap
We’ll also need to edit the application.js.coffee
file generated to incude jQuery and support our
custom Ember application path:
#
# Inside lib/assets/javascripts/spree-ember-example/application.js.coffee
#
#= require jquery
#= require handlebars
#= require ember
#= require ember-data
#= require_self
#= require ./spree_ember_example
# for more details see: http://emberjs.com/guides/application/
window.SpreeEmberExample = Ember.Application.create()
Serve the Ember application
Now that we have our Ember application ready, we’ll have to serve it. To do this we’ll add a controller to manage static pages:
#
# Inside app/controllers/static_controller.rb
#
class StaticController < ApplicationController
layout false
def ember_shop
end
end
The ember_shop
action will serve our html page with the initial code
to serve our Ember application. The layout false
directive allows us
to serve full html pages. To add that:
<!--
# Inside app/views/static/ember_shop.html.erb
-->
<!DOCTYPE html>
<html>
<head>
<title>SpreeEmberExample</title>
<%= stylesheet_link_tag "application", :media => "all" %>
<%= javascript_include_tag "spree-ember-example/application" %>
<%= csrf_meta_tags %>
</head>
<body>
<h1>Hello Ember!</h1>
</body>
</html>
Now we need to add a route for getting there safely:
#
# Inside config/routes.rb
#
match '/ember-shop' => 'static#ember_shop', :via => :get
If you now visit http://localhost:3000/ember-shop
you should see a pretty
sad hello world example:
Ok, last thing, if you look at the javascript_include_tag
above, you’ll see
we have a new manifest and we have to tell Rails we want it precompiled
when it’s time otherwise it won’t be available in the production environment:
#
# Inside config/environments/production.rb
#
config.assets.precompile += %w( spree-ember-example/application.js )
Now we’re done! We can actually start writing some Ember code! We’ll start with managing user authentication.
Authenticate users
To authenticate a user in our Ember application, we’ll use a little trick
we can use since we’re serving the application inside Spree. We’ll add a
meta
tag inside our html page carrying some basic information about
our user. This way we’ll pass around the spree_api_key
which is necessary
to use Spree’s APIs.
First we’ll need to generate the spree_api_key
. For that we can use the
generate_spree_api_key!
user method. I like to do that inside db/seeds.rb
so I don’t have to run it each time from the console:
#
# Inside db/seeds.rb
#
admin_user = Spree::User.find_by_login("[email protected]")
admin_user.generate_spree_api_key! if admin_user
So that running rake db:setup
or rake db:seed
will provide an api key to
[email protected]
Since we’re using a user attribute to access the API, we should only let authenticated users access the Ember application:
#
# Inside app/controllers/static_controller.rb
#
class StaticController < ApplicationController
layout false
def ember_shop
if spree_current_user.nil?
flash[:error] = "You should be authenticated before using the Ember application!"
redirect_to spree.login_path
end
end
end
Now we’ll need to set the meta
tag for Ember to use:
<!--
# Inside app/views/static/ember_shop.html.erb
-->
<!DOCTYPE html>
<html>
<head>
<title>SpreeEmberExample</title>
<%= stylesheet_link_tag "application", :media => "all" %>
<%= javascript_include_tag "spree-ember-example/application" %>
<%= csrf_meta_tags %>
<meta name="api-key" content="<%= spree_current_user.spree_api_key %>" />
</head>
<body>
<h1>Hello Ember!</h1>
</body>
</html>
To retrieve and use that api key we can use jQuery’s $.ajaxPrefilter
:
#
# Inside lib/assets/javascripts/spree-ember-example/application.js.coffee
#
#= require jquery
#= require handlebars
#= require ember
#= require ember-data
#= require_self
#= require ./spree_ember_example
# for more details see: http://emberjs.com/guides/application/
window.SpreeEmberExample = Ember.Application.create
ready: ->
apiKey = ($ 'meta[name="api-key"]').attr('content')
Ember.Logger.debug("Spree API Key: " + apiKey)
#
# We need a Spree API Key to get resources from the API.
# Here we pass it via X-Spree-Token header.
#
$.ajaxPrefilter (options, originalOptions, xhr) ->
xhr.setRequestHeader('X-Spree-Token', apiKey)
By doing this we use the ready
function that is called as
soon as Ember has initialized all its components. Inside this function
we use jQuery to read that meta
tag carrying the api key and set it
as a default for ajax calls to always add a X-Spree-Token
header
carrying the api key, just like Spree suggests.
NOTE: watch out how you pass that api key around! Always make sure you either use SSL or provide api keys for non-admin users who will only have access to public/safe data.
Even if this is a pretty basic behavior and we’ll use it throughout the blogpost, it can be extended easily. For example, you could pass JSON data instead of just a string (you’ll need some extra work to make the render work with spree_auth_devise):
<meta name="current-user" content="<%= render(:template => 'spree/api/users/show.v1') %>" />
and on the Ember part:
user = JSON.parse(($ 'meta[name="current-user"]').attr('content'))
$.ajaxPrefilter (options, originalOptions, xhr) ->
xhr.setRequestHeader('X-Spree-Token', user.spree_api_key)
Add some Ember code
Now that everything is good, we can actually start coding with Ember. We’ll build a classic “Desktop style” interface, with a list of products in a column on the left and a section on the right where product details will be shown.
First thing first, let’s replace Hello Ember!
with something that has more sense.
We’ll add Ember’s main template, the application template:
<!--
# Inside app/views/static/ember_shop.html.erb
-->
<!DOCTYPE html>
<html>
<head>
<title>SpreeEmberExample</title>
<%= stylesheet_link_tag "application", :media => "all" %>
<%= javascript_include_tag "spree-ember-example/application" %>
<%= csrf_meta_tags %>
<meta name="api-key" content="<%= spree_current_user.spree_api_key %>" />
</head>
<body>
<script type="text/x-handlebars" data-template-name="application">
<div class="container">
<h1>My Little Ember Shop</h1>
{{ outlet }}
</div>
</script>
</body>
</html>
We should be able to see the change as soon as we visit the /ember-shop
path.
The Handlerbars’ {{ outlet }}
directive is used by Ember to render other things
that need to be rendered, mostly like Rails’ <%= yield %>
inside layouts.
Since we now have an {{ outlet }}
, we can start rendering products. To do that,
we have to retrieve the list of products via Spree’s API which means we need
an Ember model:
#
# Inside lib/assets/javascripts/spree-ember-example/models/product.js.coffee
#
SpreeEmberExample.Product = DS.Model.extend
name: DS.attr('string')
description: DS.attr('string')
price: DS.attr('number')
Before adding more stuff, let’s check if we can retrieve products from the browser console. Open up your favourite browser console and type:
SpreeEmberExample.__container__.lookup('store:main').find('product').then(function(products) { console.log(products.toArray()) })
By doing this you’re asking Ember to call the API and return the list of
products. The toArray()
method is there to force the HTTP call otherwise
Ember will delay it until you ask for the actual products data.
You will find the console either returns an empty array or errors out. We’re
still missing some configurations to the RESTAdapter
to actually make Ember
and Spree API talk:
#
# Inside lib/assets/javascripts/spree-ember-example/store.js.coffee
#
SpreeEmberExample.ApplicationSerializer = DS.ActiveModelSerializer.extend
extractArray: (store, type, payload) ->
delete payload.count
delete payload.pages
delete payload.per_page
delete payload.total_count
delete payload.current_page
@_super(store, type, payload)
SpreeEmberExample.Store = DS.Store.extend
adapter: DS.RESTAdapter.extend
namespace: 'api'
Here are happening two things:
- We’re adding a
namespace
to actually reach the API which is within/api
as the API docs says; - we’re removing Spree API’s sideloaded metadata so that Ember won’t be bothered by it.
Now if you run the find()
again in the console, you should see your array of products!
API differences
The biggest effort in making Spree and Ember interact is making Ember understand Spree API’s data. What Ember expects as server data is somewhat different from what Spree offers.
Spree has a robust and well documented API interface, custom built with RABL. Ember has strong conventions which are spread through active_model_serializers. If you spend some time reading through both implementations, you’ll soon discover that some aspects are completely different. If you start coding without knowing what differs, you’ll hurt your productivty and make a little Ember hamster die.
One of the biggest differences is what we saw right now; Spree promotes embedded resources with sideloaded and unnamespaced metadata while Ember promotes sideloaded data (both resources and metadata) with namespaced metadata.
Here is an example. We’ll look at a stripped down version of Spree API’s products index payload and the same data if we were to serve it in an Ember compatible way (via ActiveModel::Serializers).
Spree API:
{
"products": [
{
"id": 1,
"name": "Example product",
"description": "Description",
"price": "15.99",
"variants": [
{
"id": 1,
"name": "Ruby on Rails Tote"
}
]
}
],
"count": 25,
"pages": 5,
"current_page": 1
}
ActiveModel::Serializers API:
{
"products": [
{
"id": 1,
"name": "Example product",
"description": "Description",
"price": "15.99",
"variants": [ 1 ]
}
],
"variants": [
{
"id": 1,
"name": "Ruby on Rails Tote"
}
],
"meta": {
"count": 25,
"pages": 5,
"current_page": 1
}
}
Even if these two formats does not seem very much alike, Ember supports embedded resrouces too (it just needs some extra configuration) and it’s exactly what we’ll do since we must avoid changing Spree API’s behavior whenever we can. This is why we need to disable sideloading: Ember won’t accept Spree API’s json data otherwise.
Render the products list
Now that we have a list of products, we need to render it. We’ll need to define a new route:
#
# Inside lib/assets/javascripts/spree-ember-example/router.js.coffee
#
SpreeEmberExample.Router.map ->
@resource 'products'
#
# Inside lib/assets/javascripts/spree-ember-example/routes/index_route.js.coffee
#
SpreeEmberExample.IndexRoute = Ember.Route.extend
redirect: ->
@transitionTo('products')
#
# Inside lib/assets/javascripts/spree-ember-example/products_route.js.coffee
#
SpreeEmberExample.ProductsRoute = Ember.Route.extend
model: ->
@store.find('product')
and an Handlebars template:
This will basically define a products resource and redirect to it whenever we visit the /ember-shop
path; as
soon as we get to /ember-shop#/products
Ember will load the products hitting the Spree API and render it via
the template we’ve added, like in the screenshot below.
Now that we have the basic list, we should work on making it prettier. We want the UI to be like that of Mac OS X Finder, with a column list on the left and the details in the center; by using this structure we’ll guarantee Ember will give its best because that is what Ember is for, desktop-like applications.
To add the images we need to do something a little more elaborate. Since, according to Spree’s API,
images are embededd inside variants which are embeded in products, we need to add two models,
one for the variants and one for the images and add proper hasMany
relations so that we can access
images from products.
We also need to tell Ember Data which resources will be embedded (since Ember doesn’t support
embedded resources by default).
So we should change what is inside store.js.coffee
to look something like this:
#
# Inside lib/assets/javascripts/spree-ember-example/store.js.coffee
#
SpreeEmberExample.ApplicationSerializer = DS.ActiveModelSerializer.extend(DS.EmbeddedRecordsMixin,
attrs: {
images: { embedded: 'always' }
variants: { embedded: 'always' }
}
extractArray: (store, type, payload) ->
delete payload.count
delete payload.pages
delete payload.per_page
delete payload.total_count
delete payload.current_page
@_super(store, type, payload)
)
SpreeEmberExample.Store = DS.Store.extend
adapter: DS.RESTAdapter.extend
namespace: 'api'
Here we added a mixin called DS.EmbeddedRecordsMixin
to make sure we enable support for embedded
objects. Then, with the attrs
parameter, we added which resources we have embedded in our json
objects. This should be enough to guarantee that Ember will read images and variants correctly.
Then we have to add our two new models as well:
#
# Inside lib/assets/javascripts/spree-ember-example/models/image.js.coffee
#
SpreeEmberExample.Image = DS.Model.extend
productUrl: DS.attr('string')
alt: DS.attr('string')
#
# Inside lib/assets/javascripts/spree-ember-example/models/variant.js.coffee
#
SpreeEmberExample.Variant = DS.Model.extend
images: DS.hasMany('image')
#
# Inside lib/assets/javascripts/spree-ember-example/models/product.js.coffee
#
SpreeEmberExample.Product = DS.Model.extend
name: DS.attr('string')
description: DS.attr('string')
price: DS.attr('number')
variants: DS.hasMany('variant')
images: (->
@get('variants.firstObject.images')
).property('variants.firstObject.images')
mainImage: (->
@get('images.firstObject')
).property('images.firstObject')
To make the Product
model easier to use we also added the images
and mainImage
methods which we can use to retrieve images from the product.
We now need to display the images inside our templates; we’ll also add some some
Skeleton (Spree’s grid system of choice) classes to style it out.
We now have a pretty list of products! Take a look:
The list will get boring as soon as you look at it, we’ll need to add some interactivity to make it better.
Render the product details
To add the products details, as usual, we have to change our routes file first:
#
# Inside lib/assets/javascripts/spree-ember-example/router.js.coffee
#
SpreeEmberExample.Router.map ->
@resource 'products', ->
@resource 'product', { path: ':product_id' }
Then we’ll to change the products template to add links and, since we need to render
a nested resource, an additional {{ outlet }}
directive:
Now we should have a properly working products list; if you visit /ember-shop#/products
click
around, you should see the URL changing, effectively adding the product’s id to the path.
That said, we’re still missing the product template so nothing is shown; let’s fix it:
We should be able to click around and see the products details in the main section of the page. Since we also cycle on the products images, we should see every image for each product so, for example, we should have t-shirts with front and back images.
Now this is a little more interesting, take a look:
Now let’s add a finishing touch! You might have noticed that if you visit /ember-shop#/products
,
you get an ugly empty page which might be a little frightening and counter-intuitive. We can
fix it by adding the proper index template:
That will make sure we have a placeholder that informs users:
Seems like we’re done! We’ll stop here but feel free to go on and play around. Try adding more resources/interactions, you’ll find it’s actually quite easy.
Going further
If you like what you’ve seen and opt for Ember as the weapon of choice, you might encounter some more rough edges when you’re working with Spree. Here are a couple of hints about what has made our life easier while developing with Spree and Ember:
- always compare Spree’s API with ActiveModel::Serializers’s documentation. If you want to know what Ember Data will consume happily in terms of API, ActiveModel::Serializers will tell you;
- you could be tempted to write your own API using ActiveModel::Serializers, DON’T. Spree’s API is not just a bunch of JSON data, it’s much more; it manages authentication and authorization, it has many useful helpers and it has a sweet test suite you can use to test your own additional changes. If you don’t believe it, browse through the source and look at how many wheels you’d need to reinvent;
- the above warning only counts if you’re actually building a store application. If your application is something completely different (and you just need few of the endpoints of the Spree API) consider using ActiveModel::Serializers from the start;
- if you need to override some of the API’s behavior, take note of the api_helpers.rb file. This file defines, for each resource, what attributes are included in the JSON payload. Before doing a complete RABL view override, try decorating this file, it will make your code cleaner and you’ll be changing just what you need instead of the whole view;
- even if you’re just starting out with javascript, don’t be afraid to browse through Ember’s and Ember Data’s source code. Since Ember is changing so fast most times hours spent looking through stackoverflow’s outdated posts equal to minutes when reading Ember’s well written source code.