workshop
Nick Schot
- EmberConf speaker
- Groot Star Wars & LEGO fan
- Fulltime Ember zzp'er
- Super cool
Setup
git clone git@github.com:csvalpha/ember-training.git
cd ember-training
npm i
ember s
Now go to: http://localhost:4200
1. Routing & Models
- Routing: the URL
- Renders the template
- Sets up (partial) application state
Routing inside out
Route example
GET /authors/218
application
- index
- authors
- index
- author
// app/router.js
Router.map(function() {
this.route('authors', function() {
this.route('author', {
path: ':author_id'
});
});
});
Actual available routes
// app/routes/authors/author.js
import Route from '@ember/routing/route';
export default Route.extend({
model(params){
return this.store.findRecord('author', params.author_id);
}
});
Several other hooks:
- beforeModel
- afterModel
- ...
Execution is "paused" when returning a Promise in these hooks
Route
Retrieving Data
{{!-- model hook is NOT triggered --}}
{{#link-to "authors.author" author}}
{{author.name}}
{{/link-to}}
{{!-- model hook is triggered --}}
{{#link-to "authors.author" author.id}}
{{author.name}}
{{/link-to}}
From a template
Switching routes
this.transitionTo('authors.index');
From a route
this.transitionToRoute('authors.index');
From a controller
Model
// app/models/author.js
import Model from 'ember-data/model';
import attr from 'ember-data/attr';
import { hasMany } from 'ember-data/relationships';
export default Model.extend({
name: attr(),
birthplace: attr(),
dateOfBirth: attr(),
dateOfDeath: attr(),
books: hasMany('book')
});
Assignment 1
- app/routes/authors/index.js
- app/routes/authors/author.js
Implement:
2. Displaying Data
- Controllers are required for
- Route specific actions
- Query Parameter state
Controllers and Templates
- Controllers are singleton
<!-- templates/authors/index.hbs -->
{{#each model as |author|}}
{{#link-to "authors.author" author}}
{{author.name}}
{{/link-to}}
{{else}}
<i>No authors found</i>
{{/each}}
// controlers/authors/index.js
import Controller from '@ember/controller';
export default Controller.extend({
// empty controller, so the
// file is not even neccessary
});
Template example
- Controllers are not required
- Model is set by Route
Templates contain...
- Control flow
- Data
- Components
- HTML
{{#if isHappy}} :D {{else}} D: {{/if}}
{{model.name}} {{formattedAuthorName}}
<AuthorForm @model={{model}} onSubmit={{action "save"}}>
<div class="card">Yay!</div>
<!-- templates/authors/index.hbs -->
{{#each model.books as |book|}}
{{book.title}}
{{/each}}
Displaying Relationships
- Relationships are async
- Data may not yet be available when template renders
// controller.js
import { computed } from '@ember/object';
import { equal } from '@ember/object/computed';
...
firstName: "John",
lastName: "Doe",
fullName: computed('firstName', 'lastName', function(){
return `${this.firstName} ${this.lastName}`;
}),
isJohnDoe: equal('fullName', 'John Doe')
Computed Properties
- Function as a property
- Ember keeps it up-to-date (lazily)
- Can be defined almost everywhere...
{{!-- template.hbs --}}
{{fullName}}
{{#if isJohnDoe}}It's John Doe!{{/if}}
Assignment 2
- app/templates/authors/index.hbs
- app/templates/authors/author.hbs
Implement:
Display a list of authors which link to the specific author
Display the authors information & books
3. Components
- (Reusable) building bricks of the application
- components.js and template file
- Pass data in
- Trigger actions
app/components & app/templates/components
Basic component
{{!-- template.hbs --}}
{{firstName}} {{lastName}}
// component.js
import Component from '@ember/component';
export default Component.extend({
firstName: 'John',
lastName: 'Doe',
});
<div id="ember-218" class="ember-view">
John Doe
</div>
Using a component...
Two styles:
- "Old" style handlebars invocation
- Angle bracket invocation
<MyComponent @value={{value}}/>
{{my-component value=value}}
Passing in data
<Input @value={{value}}/>
value: null,
onChange(value){
this.set('value', value);
}
Two way binding
Data down
- Arguments
- Attributes
<MyComponent @value={{value}} title="Component Title" />
Actions up
Closure actions
<!-- app/templates/something.hbs -->
<MyComponent @value={{value}} @onClick={{action "myAction"}} />
// app/components/my-component.js
export default Component.extend({
// hooks
onValueChange(){},
actions: {
updateValue(newValue) {
this.onValueChange(newValue);
}
}
});
<!-- app/templates/components/my-component.hbs -->
{{value}}
<button {{action "updateValue" "I changed!"}}>
Click me!
</button>
Block components
<!-- app/templates/something.hbs -->
<MyComponent label="Click me!" />
<!-- app/templates/components/my-component.hbs -->
<button {{action "click"}}>
{{#if hasBlock}}
{{yield}}
{{else}}
{{label}}
{{/if}}
</button>
<!-- app/templates/something.hbs -->
<MyComponent>
Click Me!
</MyComponent>
Sharing component data
with wrapped content
<!-- app/templates/components/my-component.hbs -->
<button {{action "click"}}>
{{yield
(component "icon-component")
}}
</button>
<!-- app/templates/something.hbs -->
<MyComponent as |IconComponent|>
<IconComponent icon="check"> Click Me!
</MyComponent>
<!-- app/templates/components/my-component.hbs -->
<button {{action "click"}}>
{{yield
(hash
IconComponent=(component "icon-component")
actions = (hash
doSomething=(action "doSomething")
)
)
}}
</button>
<!-- app/templates/something.hbs -->
<MyComponent as |mc|>
<mc.IconComponent icon="check"> Click Me!
</MyComponent>
Assignment 3
- app/components/search-input.js
- app/templates/components/search-input.hbs
Implement:
Takes value as argument
Triggers onChange hook on edit
Tip: this is a good spot to test the differences between two-way binding & pure DDAU
ember g component search-input
4. Query Parameters
- Query Parameters are used for
- Saving search/pagination state in the URL
url?paramater=value¶meter2=value2
// app/routes/books/index.js
export default Route.extend({
queryParams: {
title: { refreshModel: true }
},
model(params){
console.log('Parameter', params.title);
// -> Parameter 'genesis'
...
}
});
// app/controllers/books/index.js
export default Controller.extend({
queryParams: ['title'],
title: '',
});
http://localhost:4200/books?title=genesis
Assignment 4
- app/routes/books/index.js
- app/controllers/books/index.js
Prepare for filtering the books index page
Use your search-input component
Implement query parameters so they are ready to use
5. Filtering with JSON:API
api/books?filter[title]=genesi
// app/routes/books/index.js
import Route from '@ember/routing/route';
import { isEmpty } from '@ember/utils';
export default Route.extend({
queryParams: {
title: { refreshModel: true }
},
model(params){
const filter = {};
if(!isEmpty(params.title)){
filter.title = params.title
}
return this.store.query('book', { filter });
}
});
That's it!
Assignment 5
- app/routes/books/index.js
Implement:
Use your query parameter to trigger the proper API request to finalize the filter implementation
6. Debounced Filtering
With ember-concurrency
import { task, timeout } from 'ember-concurrency';
...
updateValue: task(function * (e){
yield timeout(250);
this.onChange(e.target.value);
}).restartable(),
Ember Concurrency
Javascript Generator Functions
Generator syntax? Difficult stuff...
ember-concurrency
<input oninput={{perform updateValue}}>
{{#if updateValue.isRunning}}
loading...
{{/if}}
Assignment 6
Make your search-input component debounced
This lessens the amount of requests dramatically
7. Form Handling & Validation
{{!-- component.hbs --}}
<BsForm @model={{model}} @onSubmit={{action "save"}} as |form|>
<div class="card-body">
<form.element @controlType="text" @property="name" @label="Name" />
<form.element @controlType="text" @property="address" @label="Address" />
</div>
<div class="card-footer text-right">
<BsButton @type="default" @onClick={{action "cancel"}}>Cancel</BsButton>
<BsButton @type="primary" @buttonType="submit">Save</BsButton>
</div>
</BsForm>
// component.js
...
actions: {
cancel(){ ... },
async save(model){
try {
await model.save();
} catch (e) {
// oh no!
console.error(e);
}
}
}
Validation
// app/models/store.js
import { validator, buildValidations } from 'ember-cp-validations';
// ember-cp-validations
const Validations = buildValidations(
{
name: {
description: 'Name',
validators: [
validator('presence', true),
validator('length', {
min: 3,
max: 16
})
]
},
address: {
description: 'Address',
validators: [
validator('presence', true),
]
}
}
);
export default Model.extend(Validations, {
name: attr(),
address: attr(),
books: hasMany('book')
});
Creating a new resource
routes/store/add.js
// app/routes/stores/add.js
export default Route.extend({
model(){
return this.store.createRecord('store');
},
actions: {
willTransition(transition){
if(this.controller.model.isNew){
if (confirm('Are you sure you want to leave?')){
this.controller.model.deleteRecord();
} else {
transition.abort();
}
}
}
}
});
Editing an existing resource
// app/routes/stores/store/edit.js
import Route from '@ember/routing/route';
export default Route.extend({
model(params){
return this.store.findRecord('my-model', params.my_model_id);
},
actions: {
willTransition(transition){
if(this.controller.model.hasDirtyAttributes){
if (confirm('Are you sure you want to leave?')){
this.controller.model.rollbackAttributes();
} else {
transition.abort();
}
}
}
}
});
Assignment 7
Implement:
- store-form component
- /stores/store/add page
- /stores/store/edit page
Bored?
Congratulations.
You're making your own Pagination component!
or
Experiment with Services!
Ember beginners workshop
By Nick Schot
Ember beginners workshop
- 423