Externally Embedding Ember

We’ve been playing around with Ember since before it was extracted from SproutCore, and it wasn’t until recently that we got this unusual request from one of our clients: “Can you embed an Ember app in an external page like you would Google Analytics or Google Maps?”

Our immediate answer was “Probably…” quickly followed by “Would you like us to try?”

After receiving approval to explore the problem, here’s how it turned out:

The Desired Use Case

We first determined what exactly our client wanted as an interface. It looked something like this:

<script src="//example.com/widget.js"></script>
<script>
  initializeWidget({
    rootElement: '#widget',
    serverName: 'api.example.com',
    initialQuery: 'timey wimey'
  });
</script>

With a specification like this, we identified three major puzzles:

  1. Determine where a customization hook can be inserted
  2. Write a hook that provides the available customization
  3. Wrap up the app, including dependencies and hook, in a single file

Finding Where to Customize

Since we want to be able to configure the application before it does its magic, we need to understand how its boot sequence works:

  1. You call something like MyApp = Em.Application.create({});
  2. You configure MyApp with models, controllers, routes, etc.
  3. After all your JavaScript on the page runs, the browser executes the next tick of its run loop, which is where your application actually boots up

We can see that the spot where we can configure our application is between steps 2 and 3 of this sequence.

Writing the Hook

We identified three types of customization that we wanted to perform:

  • Ember configuration
  • Domain-specific configuration
  • Controller initialization

There could be more, but we’ll focus on these three.

We also want to manually defer booting the app until we’re ready. We could rely on the browser’s JavaScript run loop, but Ember provides us with something less fragile: deferReadiness() and advanceReadiness().

We’ll defer the readiness of our application and write a global initialization function that looks like the following:

MyApp.deferReadiness();
window.initializeWidget = function(opts) {
  // place configuration handling here
  MyApp.advanceReadiness();
};

Ember Configuration

The most common type of Ember configuration we would expect in our embedding scenario is to set the rootElement. This tells Ember to take over only a portion of the page rather than the entire body. Inside the initializeWidget function, this would look like:

window.initializeWidget = function(opts) {
  var rootEl = opts.rootElement;

  if (typeof rootEl != 'undefined' && rootEl !== null) {
    MyApp.reopen({rootElement: rootEl});
  }
};

Domain-specific Configuration

Let’s say your widget can run with different, but similarly acting, backend servers. One example might be running against a staging or production server. Another example might be switching from one RSS feed to one on a different domain. This one is a little easier:

window.initializeWidget = function(opts) {
  var serverName = opts.serverName;

  // insert null-checks as necessary...
  MyApp.serverName = serverName;
};

Controller Initialization

Initializing any of the runtime objects in your Ember app gets a little trickier. We’ve already identified that our Ember application isn’t yet running, therefore the objects we’d like to initialize don’t yet exist. We get around this by using Application.then() as illustrated here:

window.initializeWidget = function(opts) {
  var initialQuery = opts.initialQuery;

  // insert null-checks as necessary...
  MyApp.then(function() {
    MyApp.
      __container__.
      lookup('controller:search').
      set('query', initialQuery);
  });
};

By using MyApp.then(), we defer the execution of the callback until MyApp is resolved, which occurs in the didBecomeReady function of Ember.Application.

Wrap the App

This is a problem with different solutions, depending on which JavaScript concatenation workflow you use. Suffice to say, you’ll need to concatenate Ember, Handlebars, Ember Data (if you use it), your app, and your customization hook into one file. This isn’t strictly necessary—you can require your users to insert multiple remote script tags—but it certainly improves the developer’s experience.

Conclusion & Caveats

I haven’t tried this approach with multiple Ember apps on the same page. I suspect there would be undesirable run loop interactions in play.

Suggestions are welcome; my Twitter handle is @listrophy, and I’d love to discuss this solution and any ideas you have.


Category: Development
Tags: Ember, Tutorial