Ember addons are a great way to share reusable code between applications. At their most basic level, an addon is just an Ember specific NPM package.

Today we will be creating an Ember addon that will display a list of daily drip episodes for any topic that the consumer of the addon chooses. This should help expose you to the basics of creating an Ember addon and hopefully, you will be able to use your new knowledge to create a bunch of really useful addons.

Creating an addon is a lot like creating an new Ember application, but instead of typing ember new ${name} we use ember addon ${name} for our generator. So, let's start.

ember addon ember-daily-drip-feed

This will generate a new addon shell for us. If you cd into your new addon you will notice that it looks pretty much like an app but it has an addon directory. While we are in here, let's go ahead and run our tests.

ember t

As you can see, tests run just like any ember app. Also, just like any Ember app, there is an app directory. As you build your addon you should keep in mind that everything you put in your addon's app directory will be merged with the app directory of the consuming Ember application. However, unlike normal Ember apps, most of your code in your addon will go in your addon directory not your app directory. You will then import any objects from your addon directory into the app directory. This will become more obvious as we start building our addon and as always, Ember CLI will help us a lot along the way.

Let's get started by generating our primary component.

ember g component daily-drip-feed

Now, we have something that looks noticeably different than a normal app. This generate command created a component in app/components and in addon/components and it put the component's template in addon/templates/components. So, let's run our tests again.

ember t

And they no longer pass. But, because Ember CLI is awesome we know exactly why it doesn't pass and we can easily fix it. Because of the way the template and layout work for the addon's component we need to include "ember-cli-htmlbars" in our dependencies rather than our devDependencies. Just open the addon's package.json file and move that dependency. While we are in here, we can also add a few relevant keywords so that when we publish our addon it can easily be found. For our addon, I am going to add: "daily drip", "learning", and "education".

Now, let's start making our component do what we want it to. We will worry about fetching data later, for now, let's just get it looking like we want. Let's open our component's integration test and add some assertions that will fail. Since we are going to use this as an inline component and we can get rid of the block level/yield test.

First, let's set up what we our feed might look like. I think we will return an object that will have a title property and a feed property. The title will contain a generic title for the whole feed, and the feed will contain an array with the actual feed items. So, let's assert that we have an h3 that contains the results.title value and an unordered list that has the same number of li elements as the results.feed's length.

tests/integration/components/daily-drip-feed.js

import { moduleForComponent, test } from 'ember-qunit';
import hbs from 'htmlbars-inline-precompile';

moduleForComponent('daily-drip-feed', 'Integration | Component | daily drip feed', {
  integration: true
});

test('it renders the title and a list with the appropriate number of items.', function(assert) {
  let results = { title: 'title', feed: [1, 2, 3]};
  this.set('results', results);
  // Handle any actions with this.on('myAction', function(val) { ... });

  this.render(hbs`{{daily-drip-feed results=results}}`);

  assert.equal(this.$('[data-feed-header]').text().trim(), 'title');
  assert.equal(this.$('[data-feed-list] > li').length, 3);
});

This test will fail, let's make it pass. In our template (addon/templates/components/daily-drip-feed.hbs) lets add our header with the data attribute we are using as a selector in our test and pass in our results title attribute. And let's add a ul with our data attr and use an each helper to iterate over the results' feed array. Finally add list items.

<h3 data-feed-header>
  {{results.title}}
</h3>
<ul data-feed-list>
  {{#each results.feed as |item|}}
    <li>
      {{item}}
    </li>
  {{/each}}
</ul>

Now, that works. Let's make one more change today. The feed wont be just a list of numbers or strings, it will be a list of objects each with a link and title property. Back in our test, we'll set it up for that.

import { moduleForComponent, test } from 'ember-qunit';
import hbs from 'htmlbars-inline-precompile';

moduleForComponent('daily-drip-feed', 'Integration | Component | daily drip feed', {
  integration: true
});

test('it renders the title and a list with the appropriate number of items.', function(assert) {
  let obj1 = {
    title: '[Elm 021.1] Editing Users and a Week of Refactoring',
    link: 'https://www.dailydrip.com/topics/elm/drips/editing-users-and-a-week-of-refactoring'
  };
  let obj2 = {
    title: '[Elm 020.5] Elm Weekly Drip #20 and Exercise: Edit Users',
    link: 'https://www.dailydrip.com/topics/elm/drips/elm-weekly-drip-20-and-exercise-edit-users'
  };
  let obj3 = {
    title: '[Elm 020.4] Finishing a CRUD Resource with a Sortable Table in elm-mdl',
    link: 'https://www.dailydrip.com/topics/elm/drips/finishing-a-crud-resource-with-a-sortable-table-in-elm-mdl'
  };

  let results = { title: 'title', feed: [obj1, obj2, obj3]};
  this.set('results', results);

  this.render(hbs`{{daily-drip-feed results=results}}`);

  assert.equal(this.$('[data-feed-header]').text().trim(), 'title');
  assert.equal(this.$('[data-feed-list] > li').length, 3);
  assert.equal(this.$('[data-feed-list] > li:first-child a').text().trim(), obj1.title);
  assert.equal(this.$('[data-feed-list] > li:first-child a').attr('href'), obj1.link);
});

Now, let's make that pass.

<h3 data-feed-header>
  {{results.title}}
</h3>
<ul data-feed-list>
  {{#each results.feed as |item|}}
    <li>
      <a href={{item.link}}>{{item.title}}</a>
    </li>
  {{/each}}
</ul>

So, that get's up where we need to be for now. We have an addon that gives any consuming application access to a component that displays a title, and a list. The list items have links and their own titles. That's it for today, tomorrow we will look a little more at addon tests and at the dummy app that is included with your addon. See you then.