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
- 2,116