Heading image for post: Building Ember CLI Addons Simply

Ember

Building Ember CLI Addons Simply

Profile picture of Jonathan Jackson

Before we begin, a backstory. Ember is a framework for building ambitious web applications. Ember CLI is a project that enables you to get your ambitious web applications into the browser with a minimum amount of fuss. It makes a lot of decisions for you, that allow you to get up and running quickly. Build tools, ES6 module transpilation, and QUnit just work. Which allows you to work on the important stuff. It has a thrumming community that really cares. Anyways, it's amazing. But, I'll stop gushing on Ember CLI now and get on with what I want to talk about. Addons.

edit: rjackson the Ember core team member who submitted the PR that brought addons to Ember CLI has just written a post about the topic. Go read it here for some more awesome information

One of the things that most excited me about Ember CLI was the promise of an easy way to share sections of reusable code between ember apps. Recently, addons made it in. (in 0.0.35) I had a component that I'd been working on within an Ember CLI app that look like a perfect fit. It was integration / unit tested, and the method of sharing was copy and pasting the relevant sections (not ideal). So I turned it into an addon. Let's take a look at it!

Roadmap

So the skeleton of things we have to do to prepare an Ember app to become an Ember addon is pretty straightforward:

  • Setup package.json
  • Write ember-addon-main file to include files
  • Setup folder structure

Setup package.json

Ember CLI looks for the ember-addon keyword within the package.json to identify your package as an addon and kick things off.

"keywords": [
  "ember-addon"
],
"ember-addon-main": "lib/ember-addon-main.js"

When Ember CLI sees ember-addon it looks for an ember-addon-main property (defaulting to main if not found). Now we get to the fun stuff. Ember CLI is going to read in lib/ember-addon-main.js and use the constructor it exports to create an instance with two methods; treeFor and included.

Write ember-addon-main.js

I'm going to be using the ember-addon-main.js file from ember-cli-super-number. If you'd like to follow along with the full file you can here.

EmberCLISuperNumber.prototype.treeFor = function treeFor(name) {
  var treePath =  path.join('node_modules', 'ember-cli-super-number', name + '-addon');

  if (fs.existsSync(treePath)) {
    return unwatchedTree(treePath);
  }
};

EmberCLISuperNumber.prototype.included = function included(app) {
  this.app = app;

  this.app.import('vendor/ember-cli-super-number/styles/style.css');
};

The treeFor method is called once for app, once for styles, and once for vendor. The return from this should be a tree that can be merged into the including application's app, styles, or vendor folder.

If you are curious about where the magic is happening (from the original PR):

In our case, we are looking inside the addon (from within node_modules) for ember-cli-super-number/app-addon. which is returned on line 39. The unwatchedTree function ensures that the watcher doesn't monitor changes on included files.

You might be wondering why ember-cli-super-number suffixes '-addon' to the app directory within treeFor.

Understanding the '-addon' suffix

I made this section because I need to make a few caveats. Ember CLI addons are moving fast. As such, the conventions surrounding how these addons are being implemented are still being established. While moving ember-cli-super-number to an addon we wanted to be able to keep the integration/unit testing as is. And we didn't want to bring any non-essential files used for testing (and the like) into the including application. So, we decided to have an app-addon directory. Locally to ember-cli-super-number, we'd merge app and app-addon before new EmberApp was called in our Brocfile.

This would allow us to return only the relevant files to treeFor by constructing the tree (inside ember-addon-main script) by appending -addon, and still keep the application running as before locally.

This provides two major benefits. We only include files that are relevant to the including application (as mentioned above). And, we are able to act as if the addon is simply another Ember CLI app locally. This makes testing quite simple and easy, in fact, the tests for ember-cli-super-number didn't have to change as a result of turning it into an addon.

Merging these trees together is made quite simple with Broccoli:

var mergeTrees = require('broccoli-merge-trees');

var appTree    = mergeTrees(['app', 'app-addon'], { overwrite: true });
var vendorTree = mergeTrees(['vendor', 'vendor-addon']);

var EmberApp = require('ember-cli/lib/broccoli/ember-app');

var app = new EmberApp({
  trees: {
    app: appTree,
    vendor: vendorTree
  }
});

I've done the same thing for vendor-addon, and would for styles if that was necessary. The above simply merges app and app-addon before including newing up the EmberApp.

I really want to stress that these conventions are still in flux. Another option is to merge an app directory and tests/dummy. In that scenario we'd end up with the same usability, but app would then contain only addon files, and tests/dummy would contain the rest of the Ember CLI app. (this idea is influenced by the way Rails engines are tested)

The included hook

Our ember-cli-super-number component also needs to apply css. In order to accomplish this, we need to use the included hook. This hook is called by the EmberApp constructor and given access to this as app. This is the perfect place to vendor our css.

You can see where it is called here (from the original PR)

You'll remember from earlier that we had a vendor-addon folder. This is where we'll place our css. Then in our included hook we'll use our access to app to import into vendor. We'll do this here, because if we add it here it will automatically be concatted to the assets/vendor css link in the default app/index.html file (of the including application).

This seemed like a sensible default for the super-number component because it requires some css to be functional, but leaving it out could lead to great flexibility for the including app as it would force the including application to implement their own css (or copy it). This is a decision left to each individual use case.

Publishing

Once you've done the above you have to remove the "private": true flag from package.json and call npm publish. And now you can install your addon with npm install ember-cli-<your-addon-name-here> --save-dev and use your included stuff.

You should probably add an .npmignore file to ignore the app, vendor, and tests so that they aren't shipped with the package (making for quicker downloads)

In our example, you can now call:

npm install ember-cli-ember-super-number --save-dev

And within your tempates you can call {{super-number}} to get the super number component.

Fin

Personally, my conclusion is that this is really awesome stuff. The people who are pushing the addon story through are trying to make this stuff easy and approachable while at the same time establish conventions that will work for a very wide range of people. For this reason, I'd like to extend some gratitude to them. Thanks!

Hopefully, I didn't overcomplicate this topic. It is actually quite simple to setup. I wanted to go into a little more detail because documentation is so sparse on addons due how new it is. Anyways, I hope this helps.

Cheers

photo credit Robert S. Donovan on Flickr

More posts about Ember Development

  • Adobe logo
  • Barnes and noble logo
  • Aetna logo
  • Vanderbilt university logo
  • Ericsson logo

We're proud to have launched hundreds of products for clients such as LensRentals.com, Engine Yard, Verisign, ParkWhiz, and Regions Bank, to name a few.

Let's talk about your project