The Art of Mastering Ember

5 key strengths

You can follow my slides on

slides.com/rubycalling/ember-chef/live

Hello!

I am Laura Lebovic

@lauralebovic

@rubycalling

I live in Dalston, London

I was born outside London

, in Paris

When I am not coding, I am probably busy boxing

You can find me on several Music Festivals

I love travelling <3

I work at Show My Homework

Let's have a look at my cookbook

1. Organise your own routes

2. The importance of integration tests

3. Mock your ideal payload

4. Focus on the user experience

5. Expect the un-expected

5 keys strength

1. Organise your own routes

Organise your own routes

Organise your own routes


  app
  ├─authenticated
  │  ├─route.js
  │  ├─template.hbs
  │  │
  │  └─recipes
  │     ├─controller.js
  │     ├─route.js
  │     └─template.hbs
  |
  ├─components
  │  └─ingredients-modal
  │     ├─component.js
  │     └─template.hbs

The structure

Organise your own routes


  <ul class="recipes-list">
    {{#each model as |recipe|}}
      <li {{action 'viewIngredients' recipe}}>
        {{recipe.title}}
      </li>
    {{/each}}
  </ul>

  {{#if showIngredients}}
    <div class="ingredients-popup">
      {{ingredients-modal 
        ingredients=ingredients 
        showIngredients=showIngredients}}
    </div>
  {{/if}}

app/authenticated/recipes/template.hbs

Organise your own routes


  ingredients: computed('selectedRecipe', function(){
    return this.get('selectedRecipe.ingredients');
  }),

  actions: {
    viewIngredients(recipe) {
      this.set('selectedRecipe', recipe);
      this.toggleProperty('showIngredients');
    }
  }

app/authenticated/recipes/controller.js

Organise your own routes

Organise your own routes


  actions: {
    close() {
      this.toggleProperty('showIngredients');
    }
  }

app/components/ingredients-modal/component.js

Organise your own routes

What happens when we want to share our ingredients list?


  app
  ├─authenticated
  │  ├─route.js
  │  ├─template.hbs
  │  │
  │  └─recipes
  │     ├─controller.js
  │     ├─route.js
  │     ├─template.hbs
  │     │
  │     └─ingredients
  │        ├─controller.js
  │        ├─route.js
  │        └─template.hbs
  │
  ├─components
  │  └─ingredients-modal
  │     ├─component.js
  │     └─template.hbs

Let's make it work

Organise your own routes


  <ul class="recipes-list">
    {{#each model as |recipe|}}
      <li>
        <a href="{{href-to 'authenticated.recipes.ingredients' recipe.id}}">
          {{recipe.title}}
        </a>
      </li>
    {{/each}}
  </ul>

  {{outlet}}

Organise your own routes

  app/authenticated/recipes/template.hbs


  <div class="ingredients-popup">
    {{ingredients-modal ingredients=ingredients}}
  </div>

Organise your own routes

  app/authenticated/recipes/ingredients/template.hbs


  <!DOCTYPE html>
  <html>
    <head>...</head>
    <body>
      <nav>...</nav>
      <ul class="recipes-list">
        <li><a href="/recipes/1/ingredients">title</a></li>
        <li><a href="/recipes/2/ingredients">title</a></li>
        <li><a href="/recipes/2/ingredients">title</a></li>
      </ul>
      <div class="ingredients-popup">
        <div class"ember-view ingredients-modal">...</div>
      </div>
      <div class="pagination">...</div>
      <footer>...</footer>
    </body>
  </html>

Organise your own routes


  app
  ├─authenticated
  │  ├─route.js
  │  ├─template.hbs
  │  │
  │  └─recipes
  │    ├─controller.js
  │    ├─route.js
  │    ├─template.hbs
  │    │
  │    └─ingredients
  │      ├─controller.js
  │      ├─route.js
  │      └─template.hbs
  │ 
  ├-components
  │   └─ingredients-modal
  │     ├─component.js
  │     └─template.hbs
  │ 
  ├-templates
  │   └─application.hbs

Organise your own routes

Let's master it!

Organise your own routes


  {{outlet}}
  {{liquid-outlet 'modal'}}

  app/application.hbs

Organise your own routes


  renderTemplate() {
    this.render({into: 'application', outlet: 'modal'});
  }

  app/authenticated/recipes/ingredients/route.js

1. Organise your own routes

2. The importance of integration tests

3. Mock your ideal payload

4. Focus on the user experience

5. Expect the un-expected

5 keys strength

2. The importance of integration tests

The importance of integration tests

2 unit tests, 0 integration tests!

The importance of integration tests

The importance of integration tests

app/authenticated/recipes/index/template.hbs


  <div class="recipes-list">
    {{#each model as |recipe|}}
      {{recipe-entry recipe=recipe}}
    {{/each}}
  </div>

app/authenticated/recipes/desserts/template.hbs


  <div class="desserts-list">
    {{#each model as |dessert|}}
      {{recipe-entry recipe=dessert}}
    {{/each}}
  </div>

The importance of integration tests

app/authenticated/components/recipe-entry/template.hbs


  <img src="{{recipe.photo}}" class="photo">
  <h1>{{recipe.title}}</h1>
  <p class="chef">{{recipe.author}}</p>
  <p class="date">{{recipe.date}}</p>
  <div class="rating">{{recipe.rating}}</div>

The importance of integration tests

tests/acceptance/recipes/index-test.js


  test('visiting /recipes', assert => {
    let entries = server.createList('recipe-entry', 5);
    let firstEntry = find('.recipes-list .recipe-entry:eq(0)');
    visit('/recipes');

    andThen(() => {
      assert.equal(currentURL(), '/recipes');
      assert.equal(find('.recipes-list .recipe-entry').length,
        5, '5 recipes rendered');
      assert.equal(firstEntry.find('.recipe-title').text().trim(),
        entries[0].title, 'Correct title rendered');
      assert.equal(firstEntry.find('.description').text().trim(),
        entries[0].description, 'Correct description rendered');
      assert.equal(firstEntry.find('.rating .full-star').length,
        5, 'correct rating stars rendered');
      ...
    });
  });

tests/acceptance/recipes/dessert-test.js


  test('visiting /desserts', assert => {
    let entries = server.createList('recipe-entry', 5);
    let firstEntry = find('.desserts-list .recipe-entry:eq(0)');
    visit('/desserts');

    andThen(() => {
      assert.equal(currentURL(), '/desserts');
      assert.equal(find('.desserts-list .recipe-entry').length,
        5, '5 recipes rendered');
      assert.equal(firstEntry.find('.recipe-title').text().trim(),
        entries[0].title, 'Correct title rendered');
      assert.equal(firstEntry.find('.description').text().trim(),
        entries[0].description, 'Correct description rendered');
      assert.equal(firstEntry.find('.rating .full-star').length,
        5, 'correct rating stars rendered');
      ...
    });
  });

The importance of integration tests

The importance of integration tests

Lets master it!

tests/acceptance/recipes/dessert-test.js


  test('visiting /recipes', assert => {
    let entries = server.createList('recipe-entry', 5);
    visit('/recipes');

    andThen(() => {
      assert.equal(currentURL(), '/recipes');
      assert.equal(find('.recipes-list .recipe-entry').length,
        5, '5 recipes rendered');
      ...
    });
  });

  test('visiting /desserts', assert => {
    let entries = server.createList('recipe-entry', 5);
    visit('/desserts');

    andThen(() => {
      assert.equal(currentURL(), '/desserts');
      assert.equal(find('.desserts-list .recipe-entry').length,
        5, '5 recipes rendered');
      ...
    });
  });

tests/acceptance/recipes/index-test.js

The importance of integration tests

tests/integration/components/recipe-entry/component-test.js


  test('The component renderes all elements correctly', function(assert) {
    let recipe = {
      photo: 'http://url-to-photo.com/recipe.jpg',
      title: 'My first recipe',
      author: 'Brigitte Lebovic',
      date: moment().format('dd mm, YYY'),
      rating: '4'
    };
    this.set('recipe', recipe);
    this.render(hbs`{{recipe-entry recipe=recipe}}`);

    assert.equal(this.$('.photo').attr('src'), recipe.photo,
      'The recipe photo URL is renderd');
    assert.equal(this.$('.title').text().trim(), recipe.title,
      'The recipe title is renderd');
    assert.equal(this.$('.chef').text().trim(), recipe.author,
      'The recipe author is renderd');
    assert.equal(this.$('.date').text().trim(), recipe.date,
      'The recipe date renderd');
    assert.equal(this.$('.rating').text().trim(), recipe.rating,
      'The recipe rating is renderd');
  });

Lets master it!

The importance of integration tests

Acceptance tests are much more expensive than component tests.

Summary

Integration tests make the component much more reusable.

Integration tests makes it possible to do TDD.

The importance of integration tests

A good test tells a story

1. Organise your own routes

2. The importance of integration tests

3. Mock your ideal payload

4. Focus on the user experience

5. Expect the un-expected

5 keys strength

3. Mock your ideal payload

Mock your ideal payload

We settle with something that is not perfect, because the feedback loop is annoying!

 

If your team is distributed in different time zone it can lead to...

Mock your ideal payload

It will do!

Mock your ideal payload


  {
    recipes: [
      {
        id: 21,
        title: 'Chocolate Cake With Green Tea Cream',
        ...
        author: 'Sam De Maeyer'
      },
      {
        id: 22,
        title: 'Crema Catalagna',
        ...
        author: 'Miguel Camba'
      },
      {
        id: 23,
        title: 'New York Vanilla Cheesecake',
        ...
        author: 'Jamie White'
      },
      ...
    ],
    metadata: {
      total-count: 126,
      limit: 10,
      offset: 20
    }
  }

In this example we will have a payload that contains the recipes, and the information about the pagination.

Mock your ideal payload

  
  totalPages = Math.ceil(total-count/limit); # 13 pages
  currentPage = Math.ceil(total_pages - (total-count - offset) / limit); # page 3

Pagination Calculation

Mock your ideal payload

Pagination Calculation

Mock your ideal payload

Let's make it work!


  {
    recipes: {
      id: 21,
      title: 'Chocolate Cake With Green Tea Cream',
      ...
      author: 'Sam De Maeyer'
    },
    {
      id: 22,
      title: 'Crema Catalana',
      ...
      author: 'Miguel Camba'
    },
    {
      id: 23,
      title: 'New York Vanilla Cheesecake',
      ...
      author: 'Jamie White'
    },
    metadata: {
      total-pages: 25,
      page-number: 3
    }
  }
  • Reduce the feedback loop with the API
  • It's easier for the BE to adapt to the FE
  • Include the calculated values in the payload

Mock your ideal payload

  {
    meta: { 
        total-pages: 25,
        page-number: 3
    },
    data: [
      {
        type: recipes,
        id: 26,
        attributes: {
          title: 'A very chocolatey mousse',
          ...
          author: 'Brigitte Lebovic'
        }
      },
      {
        type: recipes,
        id: 27,
        attributes: {
          title: 'Sweet Chilli and Lime Chicken Wings',
          ...
          author: 'Yehuda Katz'
        }
      }
    ],
    links: {
      'self': 'api.emberchef.com/recipes?page[number]=3&page[size]=10',
      'first': 'api.emberchef.com/recipes?page[number]=1&page[size]=10',
      'prev': 'api.emberchef.com/recipes?page[number]=2&page[size]=10',
      'next': 'api.emberchef.com/recipes?page[number]=4&page[size]=10',
      'last': 'api.emberchef.com/recipes?page[number]=25&page[size]=10'
    }
  }

Use JSON API

The let's master it!

1. Organise your own routes

2. The importance of integration tests

3. Mock your ideal payload

4. Focus on the user experience

5. Expect the un-expected

5 keys strength

4. Focus on the user experience

Focus on the user experience

Example

Focus on the user experience

Lets make it work!

app/authenticated/recipes/delete/route.js


  model({ recipe_id }) {
    this.get('store').findRecord('recipe', recipe_id);
  },

  redirect(model, redirect) {
    if (model.get('isDeleted')) { 
      this.transitionTo('authenticated');
    }
  }

Focus on the user experience

Lets master it!

app/authenticated/recipes/delete/route.js


  model({ recipe_id }) {
    this.get('store').findRecord('recipe', recipe_id);
  },

  actions: {
    deleteRecipe(recipe) {
      recipe.destroyRecord()
        .then(() => this.replaceWith('authenticated.recipes'))
        .catch(e => console.warn(e));
    }
  }

Focus on the user experience

Search field add characters to the url (?search=choco).

Lets say we do debounce, but for a slow types, 

the characters are added one by one in the url.

?search=c

Example

?search=cho

?search=chocol

?search=chocolat

?search=chocolate

Focus on the user experience

When I hit the back button of the browser,

the page does not change, only the url.

The user has no idea what is going on!

Example

Focus on the user experience

Lets master it!

app/authenticated/index/route.js


  model({ search }) {
    return this.get('store').query('recipe', { search });
  },

  queryParams: {
    search: {
      refreshModel: true,
      replace: true
    }
  }

1. Organise your own routes

2. The importance of integration tests

3. Mock your ideal payload

4. Focus on the user experience

5. Expect the un-expected

5 keys strength

5. Expect the un-expected

Expect the un-expected

Expect the un-expected

The ember framework makes it very easy to display

 a loading page and an error page.

So use it!

When we try to access a route with a slow model, ember will try to display a loading page.

Ember will start searching the following hierarchy:

 

  • authenticated.recipes-loading
  • loading or application-loading
  • authenticated.loading or authenticated-loading
  • authenticated.recipes.loading

Expect the un-expected

In our case, we can use:

app/authenticated/recipes/loading.hbs


  app
  ├─authenticated
  │  ├─route.js
  │  ├─template.hbs
  │  │
  │  └─recipes
  │     ├─controller.js
  │     ├─route.js
  │     ├─template.hbs
  |     └─loading.hbs
 

Expect the un-expected

In our case, we can use:

  
  <div class="recipes-list">
    Recipes are loading...
  </div>

app/authenticated/recipes/loading.hbs

Expect the un-expected

Lets talk about error pages

Expect the un-expected

Catch-all (404 - Not found)


  this.route('catchAll', { path: '/*wildcard' });

app/router.js


  <h2 class"generic-404">404 - Not found</h2>
  <h4>we can seem to find the page you are looking for.</h4>
  <p>
    Please 
    <a href="{{href-to 'authenticated'}}">click here</a>
    to go back to the main page.
  </p>

app/catchall/template.hbs

Expect the un-expected

Generic error page


  <h2>Whooopsie ... someting went wrong</h2>
  <h4>It looks like something went wrong.</h4>
  <p>Not to worrie, please refresh your page, or</p>
  <p>
    <a href="{{href-to 'authenticated'}}">click here</a>
    to go back to the main page.
  </p>

app/templates/error.hbs

Expect the un-expected

Let's master it!


  setupController(controller, error) {
    this.get('bugsnag').report(this, error);
    controller.set('errorMessage', error.message);
    this._super(...arguments);
  }

app/authenticated/recipes/error/route.js

Expect the un-expected

Let's master it!


  <h2>Whooops ...</h2>
  <h4>
    The recipe cannot be displayed because something went wrong
  </h4>
  <p class="server-error-message">{{errorMessage}}</p>
  <p>Not to worry, please refresh your page, or</p>
  <p>
    <a href="{{href-to 'authenticated'}}">click here</a>
    to go back to the main page.
  </p>

app/authenticated/recipes/error/template.hbs

Expect the un-expected

As a master we should anticipate one more thing!


  <form class="create-recipe" {{action 'submitRecipe'}}>
    ...
    <button class="btn-danger" disabled={{onAir}}>
      {{if onAir 'Saving...' 'Save'}}
    </button>
  </form>

app/authenticated/recipes/create/template.hbs

Expect the un-expected

Let's make it work.


  actions: {
    submitRecipe() {
      this.set('onAir', true);
      this.get('model').save()
        .catch(e => console.warn(e))
        .finally(() => this.set('onAir', false));
    }
  }

app/authenticated/recipes/create/controller.js

Expect the un-expected

Let's master it.


  import { task } from 'ember-concurrency';

  // tasks
  submitRecipeTask: task(function*() {
    try {
      yield this.get('model').save();
    } catch (e) {
      console.warn(e)
    }
  })

app/authenticated/recipes/create/controller.js

Expect the un-expected

Let's master it.


  <form class="create-recipe" {{perform 'submitRecipeTask'}}>
    ...
    <button class="btn-danger" disabled={{submitRecipeTask.isRunning}}>
      {{if submitRecipeTask.isRunning 'Saving...' 'Save'}}
    </button>
  </form>

app/authenticated/recipes/create/template.hbs

1. Organise your own routes

2. The importance of integration tests

3. Mock your ideal payload

4. Focus on the user experience

5. Expect the un-expected

5 keys strength

1. Organise your own routes

2. The importance of integration tests

3. Mock your ideal payload

4. Focus on the user experience

5. Expect the un-expected

So what's next?

Be a master

Get involved in the community

  • Meet other Emberinos via Github, Slack, Ember meetups
  • A lot of open source project are broken so if you find something that doesn't work you can probably help

Be a master

Read the official documentation

  • Keeping up with changes to the framework.
  • Read rfc and comment on them - community effort
  • Give your voice so the framework can improve

Be a master

Create an addon and share it!

  • The worth thing that will happen is that you are the only one using it

Thank you!

@lauralebovic

rubycalling

art-of-mastering-ember

By Laura Lebovic

art-of-mastering-ember

  • 1,988