Ember
Building Ember CLI Addons Simply
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