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&parameter2=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