Using Ember.js with Spree

Matteo Latini

22 Jan 2014 Ruby On Rails, Development

Matteo Latini

17 mins
Emberjs + spreecommerce

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:

  1. choosing the right development tools for your app
  2. how to setup your Spree application to serve an Ember application
  3. a quick way to authenticate a Spree user to use the Ember application
  4. understanding the differences between Spree's and Ember Data's APIs
  5. 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:

A basic Spree demo store

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:

A sad Hello World from Ember.js

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:

  1. We're adding a namespace to actually reach the API which is within /api as the API docs says;
  2. 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:

{{ "{{" }}! Inside lib/assets/javascripts/spree-ember-example/templates/products.hbs }}
<div id="products">
  <h2>Products</h2>

  <ul>
    {{ "{{" }}#each product in controller}}
      <li>{{ "{{" }} product.name }}</li>
    {{ "{{" }}/each}}
  </ul>
</div>

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.

A basic product list

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.

{{ "{{" }}! Inside lib/assets/javascripts/spree-ember-example/templates/products.hbs }}
<aside id="sidebar" class="columns five">
  <div id="products">
    <h2>Products</h2>

    <ul>
      {{ "{{" }}#each product in controller}}
        <li class="clearfix">
          <img {{ "{{" }}bind-attr src="product.mainImage.productUrl" alt="product.mainImage.alt" class=":one :column :alpha"}}>
          {{ "{{" }} product.name }}
        </li>
      {{ "{{" }}/each}}
    </ul>
  </div>
</aside>

We now have a pretty list of products! Take a look:

A decent list of products

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:

{{ "{{" }}! Inside lib/assets/javascripts/spree-ember-example/templates/products.hbs }}
<aside id="sidebar" class="columns five">
  <div id="products">
    <h2>Products</h2>

    <ul>
      {{ "{{" }}#each product in controller}}
        <li class="clearfix">
          {{ "{{" }}#link-to "product" product classNames="button columns five"}}
            <img {{ "{{" }}bind-attr src="product.mainImage.productUrl" alt="product.mainImage.alt" class=":one :column :alpha"}}>
            {{ "{{" }} product.name }}
          {{ "{{" }}/link-to}}
        </li>
      {{ "{{" }}/each}}
    </ul>
  </div>
</aside>

<div id="content" class="columns eleven">
  {{ "{{" }} outlet }}
</div>

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:

{{ "{{" }}! Inside lib/assets/javascripts/spree-ember-example/templates/product.hbs }}
<div class="product">
  <h3>{{ "{{" }}name}} ${{ "{{" }}price}}</h3>

  <p>
    {{ "{{" }}description}}<br/>

    <p>
    {{ "{{" }}#each image in this.images}}
      <img {{ "{{" }}bind-attr src="image.productUrl" alt="image.alt"}}>
    {{ "{{" }}/each}}
  </p>
</div>

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:

Product list with product details

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:

{{ "{{" }}! Inside lib/assets/javascripts/spree-ember-example/templates/products/index.hbs }}
<h2>Please select a product!</h2>

That will make sure we have a placeholder that informs users:

The finishing touch, product details placeholder

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.

You may also like

Let’s redefine
eCommerce together.