« Back to home

EmberJS: How to Use Ember within a Rails page

I have been working extensively with EmberJS recently and loving every bit of it. Ember can make things happen out of the box, very well suited for single-page applications. However, a lot of times I find myself writing semi-single page websites, i.e. only some portions of the site require Ember, the rest are handled by Rails. This guide shows my current approach of embedding Ember code inside a Rails application.

Use ember-rails

First of all, I highly recommend using the ember-rails gem to manage Ember assets. The gem precompiles Handlebars template and add Ember assets to assets pipeline. It also provides some useful Rails generators that generate code templates for controller, model, store...To use ember-rails, just add to your Gemfile:

gem "ember-rails"

Also, don't forget to set up Ember variant in Rails initializer in order for it to work:

config.ember.variant = :develop # or :production    

Shared templates

This is the tricky part. Ember's approach is to render everything with Handlebars templating system. However, for some cases you may want to add Rails related content. The best way to do it is to share templates between Rails and Handlebars. For example in your Rails view:

<!--example_user_show.html.rb-->
<head> 
  <title>Example shared template</title>
  <script src="example_user_show.js"></script>
</head>

<body>
  <%= current_user.name %>
  <script type="text/x-handlebars" data-template-name="application">
    {{user.email}}
  </script>
</body>

Where {{user.email}} comes from the user model stored in ApplicationController:

// example_user_show.js
MyApp = Ember.Application.create();

MyApp.User = DS.Model.extend({
  email: DS.attr('string'),
  name: DS.attr('string')
})

MyApp.ApplicationController = Ember.Controller.extend({
  user: Ember.K

  init: function() {
    this.set('user', MyApp.User.find(userID));
  }
})

Notice that MyApp.User.find(userID) fires an Ajax request to obtain the user data. It results in data duplication when the same user model is loaded twice in Rails and Ember. We can solve this problem by preloading Ember data which will be discussed in next section.

Also note that an Ember application is associated to only one root element. To use Ember for different parts of the page, you can create multiple Ember applications. For example in Rails view:

<!--example.html.erb-->
<head> 
  <title>Example shared template</title>
  <script src="example_calendar.js"></script>
  <script src="example_map.js"></script>
</head>

<body>
  <div id="calendar">
    <!--Display a dynamic calendar with a lot of interactions. Perfect use case for Ember-->
    <script type="text/x-handlebars" data-template-name="calendar"></script>
  </div>

  <div id="map">
    <!--Display a map with a lot of interactions. We can also use Ember for that-->
    <script type="text/x-handlebars" data-template-name="map"></script>
  </div>

  <div id="other-stuff">
    <!--Some other stuff that should be handled by Rails-->
  </div>   
</body>

JS:

// example_calendar.js
Calendar = Ember.Application.create({
  rootElement: '#calendar'
})

Calendar.ApplicationView = Ember.View.extend({
  templateName: 'calendar'
})

Calendar.ApplicationController = Ember.Controller.extend({
  init: function() {
    // Do stuff
  }
})

// example_map.js
Map = Ember.Application.create({
  rootElement: '#map'
})

Map.ApplicationView = Ember.View.extend({
  templateName: 'map'
})

Map.ApplicationController = Ember.Controller.extend({
  init: function() {
    // Do stuff
 }
})

Since we're using Ember for only one page of the Rails application, Ember router becomes unnecessary. Therefore I prefer doing setup (getting model, setting up bindings...) in the init function of the ApplicationController. In theory, one Ember application is enough for one page (make the root element body or parent container), but in many cases I find myself using Ember for only a small portion of the site. Therefore, it doesn't make much sense to wrap the entire template in Handlebars just for a small Ember code to work. Plus, separating different portions into different applications makes the code more modular and easier to maintain.

Preload Ember data

As mentioned above, there is a data duplication issue when the same record being fetched twice by both Rails and Ember. To avoid this, we can preload Ember data using a preload store. The PreloadStore used for Discourse is a great example of a simple and powerful preload store. So now in the Rails template we can do:

<!--example_user_show.html.erb-->
<head> 
  <title>Example shared template</title>
  <script src="preload_store.js"><script>
  <script src="example_user_show.js"></script>
</head>

<body>
  <script>
    PreloadStore.store('currentUser', JSON.parse(<%= current_user.as_json() %>)); 
  </script>

  <%= current_user.name %>
  <script type="text/x-handlebars" data-template-name="application">
    {{user.email}}
  </script>
</body>

and JS:

// example_user_show.js
MyApp = EmberApplication.create();

MyApp.User = DS.Model.extend({
  email: DS.attr('string'),
  name: DS.attr('string')
})

MyApp.ApplicationController = Ember.Controller.extend({
  user: Ember.K

  init: function() {
    // Retrieve current user data from PreloadStore
    // the record will be automatically deleted from the store after retrieval
    var currentUser = PreloadStore.getAndRemove('currentUser');  
    if (currentUser) {
      this.set('user', currentUser);
    } else {
      // Cannot retrieve from preload store, fallback to Ajax request
      this.set('user', MyApp.User.find(userID)); 
    }
  }
})

This is a very clean way of injecting static data into Ember without setting global variables and sending unnecessary Ajax requests. You can also take a look at Discourse to see how they use PreloadStore consistently throughout their site.

Conclusion

Ember is probably the most powerful MVC framework out there for single-page application. However, using Ember for font-end rendering means that much of the Rails magic becomes useless. Incorporating Ember in Rails template is a good choice if you only want Ember to handle some portions of the web page. The above is my preferred approach and not a standard guideline from the Ember community. It's been working very well for me. If you have any comments or suggestions, feel free to drop me a note.

Comments

comments powered by Disqus