$30 off During Our Annual Pro Sale. View Details »

Templates and Logic in Ember

Templates and Logic in Ember

This talk showcases different approaches to handling application logic in templates - keeping it in the template context vs. in the template itself using nested helpers. It shows how to achieve the same result with either alternative and covers tradeoffs and differences to be aware of.

Marco Otte-Witte

October 27, 2016
Tweet

More Decks by Marco Otte-Witte

Other Decks in Technology

Transcript

  1. Templates and Logic

    View Slide

  2. Marco Otte-Witte
    @marcoow

    View Slide

  3. simplabs.com
    @simplabs

    View Slide

  4. View Slide

  5. https://ember-workshops.simplabs.com

    View Slide

  6. https://elixir-phoenix-workshops.simplabs.com

    View Slide

  7. Templates and Logic

    View Slide

  8. https://babeljs.io
    http://handlebarsjs.com
    http://blog.yhat.com/static/img/handlebars-logo.png

    View Slide

  9. "Handlebars.js is an extension to the
    Mustache templating language created by
    Chris Wanstrath. Handlebars.js and
    Mustache are both logicless templating
    languages that keep the view and the code
    separated like we all know they should be."
    https://github.com/wycats/handlebars.js/

    View Slide


  10. {{title}}

    {{body}}


    {
    title: 'A post',
    body: 'Awesome post!'
    }
    +
    =

    A post

    Awesome post!


    View Slide


  11. {{title}}

    {{body}}

    {{#if author}}
    {{author.firstName}} {{author.lastName}}
    {{/if}}

    View Slide


  12. {{title}}

    {{body}}

    {{#if author}}
    {{author.firstName}} {{author.lastName}}
    {{/if}}
    Comments
    {{#each comments as |comment|}}
    By {{comment.author}}
    {{comment.text}}
    {{/each}}

    View Slide


  13. {{title}}

    {{body}}

    {{#if author}}
    {{fullName author}}
    {{/if}}
    Comments
    {{#each comments as |comment|}}
    By {{fullName comment.author}}
    {{comment.text}}
    {{/each}}

    View Slide

  14. …templates are not "logicless" but
    Handlebars.js itself is (more or less)

    View Slide

  15. in 2016 with Ember (and addons) you can
    do this:

    View Slide

  16. {{#each (take 1 (filter-by (shuffle users) 'isActive' true)) as |user|}}
    {{#let user.firstName user.lastName as |firstName lastName|}}

    {{concat (capitalize firstName) ' ' (capitalize lastName)}}
    {{#if (and (eq (capitalize (take 1 firstName)) 'J') (lte user.age 65))}}
    (he's the special one!)
    {{/if}}

    {{/let}}
    {{/each}}

    View Slide


  17. View Slide

  18. View Slide

  19. {{#each (take 1 (filter-by (shuffle model) 'isActive' true)) as |user|}}
    {{#let user.firstName user.lastName as |firstName lastName|}}

    {{concat (capitalize firstName) ' ' (capitalize lastName)}}
    {{#if (and (eq (capitalize (take 1 firstName)) 'J') (lte user.age 65))}}
    (he's the special one!)
    {{/if}}

    {{/let}}
    {{/each}}

    View Slide






  20. (he's the special one!)




    https://upload.wikimedia.org/wikipedia/commons/thumb/2/27/PHP-logo.svg/2000px-PHP-logo.svg.png
    http://vignette2.wikia.nocookie.net/villains/images/c/c8/TrollFace.png/revision/latest?cb=20150813022250

    View Slide


  21. View Slide

  22. it's fine (and necessary) to use logic in
    templates

    View Slide

  23. …but to what extent?

    View Slide

  24. export function gte([value, compareTo]) {
    return value >= compareTo;
    }
    export default Ember.Helper.helper(gte);
    {{#if (gte user.age 65)}}

    {{/if}}
    =
    {{#if user.isSenior}}

    {{/if}}
    isSenior: computed('age', function() {
    return this.get('age') >= 65;
    })

    View Slide

  25. export function gte([value, compareTo]) {
    return value >= compareTo;
    }
    export default Ember.Helper.helper(gte);
    {{#if (gte user.age 65)}}

    {{/if}}
    =
    {{#if user.isSenior}}

    {{/if}}
    isSenior: computed('age', function() {
    return gte(this.get(['age', 65]));
    })

    View Slide

  26. export function gte([value, compareTo]) {
    return value >= compareTo;
    }
    export default Ember.Helper.helper(gte);
    {{#if (gte user.age 65)}}

    {{/if}}
    =
    {{#if user.isSenior}}

    {{/if}}
    isSenior: computed.gte('age', 65)

    View Slide

  27. {{#each (filter-by users 'isActive' true) as |user|}}
    {{user.name}}
    {{/each}}
    =
    {{#each activeUsers as |user|}}
    {{user.name}}
    {{/each}}
    activeUsers: computed('[email protected]', function() {
    return this.get('users').filterBy('isActive', true);
    })
    export function filterBy([collection, filterProperty, filterValue]) {
    return collection.filterBy(filterProperty, filterValue);
    }
    export default Ember.Helper.helper(filterBy);

    View Slide

  28. {{#each (filter-by users 'isActive' true) as |user|}}
    {{user.name}}
    {{/each}}
    =
    {{#each activeUsers as |user|}}
    {{user.name}}
    {{/each}}
    activeUsers: computed('[email protected]', function() {
    return filterBy([this.get('users'), 'isActive', true]);
    })
    export function filterBy([collection, filterProperty, filterValue]) {
    return collection.filterBy(filterProperty, filterValue);
    }
    export default Ember.Helper.helper(filterBy);

    View Slide

  29. {{#each (filter-by users 'isActive' true) as |user|}}
    {{user.name}}
    {{/each}}
    =
    {{#each activeUsers as |user|}}
    {{user.name}}
    {{/each}}
    activeUsers: computed.filterBy('users', 'isActive', true)
    export function filterBy([collection, filterProperty, filterValue]) {
    return collection.filterBy(filterProperty, filterValue);
    }
    export default Ember.Helper.helper(filterBy);

    View Slide

  30. but what about helpers being pure?

    View Slide

  31. function fullName([firstName, lastName]) {
    return `${firstName} ${lastName}`;
    }
    fullName: computed('firstName', 'lastName', function() {
    return `${this.get('firstName')} ${this.get('lastName')}`;
    })
    Inputs

    View Slide

  32. fullName: computed('firstName', 'lastName', function() {
    return `${this.get('firstName')} ${this.get('lastName')}`;
    })
    import join from '../computeds/join';

    fullName: join('firstName', 'lastName')
    export default function join(first, second, glue = ' ') {
    let args = [first, second, function() {
    return [this.get(first), this.get(second)].join(glue);
    }];
    return computed(...args);
    }
    =

    View Slide

  33. fullName: join('firstName', 'lastName', ' ')
    https://github.com/cibernox/ember-cpm

    View Slide

  34. export default Ember.Component.extend({
    num1: 45,
    num2: 3.5,
    num3: 13.4,
    num4: -2,
    total: sum(
    sum('num1', 'num2', 'num3'),
    difference('num3', 'num2'),
    product(difference('num2', 'num1'), 'num4')
    )
    });
    https://github.com/cibernox/ember-cpm

    View Slide

  35. @computed('firstName', 'lastName')
    fullName(firstName, lastName) {
    return `${firstName} ${lastName}`;
    }
    https://github.com/rwjblue/ember-computed-decorators

    View Slide

  36. this topic is not limited to reactive logic

    View Slide

  37. value={{user.firstName}}
    onchange={{action (mut firstName) value='target.value'}}/>
    value={{user.lastName}}
    onchange={{action (mut lastName) value='target.value'}}/>
    Cancel
    (pipe
    (action 'validate')
    (action (mut user.firstName) firstName)
    (action (mut user.lastName) lastName)
    (action 'save')
    (action (mut isEditing) false)
    )}}>Save

    View Slide

  38. value={{user.firstName}}
    onchange={{action 'setFirstName' value='target.value'}}/>
    value={{user.lastName}}
    onchange={{action 'setLastName' value='target.value'}}/>
    Cancel
    Save
    save() {
    this._validate().then(() => {
    this.get('user').setProperties(
    this.getProperties('firstName', 'lastName')
    );
    }).then(() => {
    return this.get('user').save();
    }).then(() => {
    this.set('isEditing', false)
    });
    }
    +

    View Slide

  39. value={{user.firstName}}
    onchange={{action 'setFirstName' value='target.value'}}/>
    value={{user.lastName}}
    onchange={{action 'setLastName' value='target.value'}}/>
    Cancel
    Save
    +
    save() {
    pipe(
    () => this._validate(),
    () => this.get('user').setProperties(this.getProperties('firstName', 'lastName')),
    () => this.get('user').save(),
    () => this.set('isEditing', false)
    );
    }

    View Slide

  40. Why is this all even relevant?

    View Slide

  41. There are a bunch of tradeoffs you
    need to be aware of

    View Slide

  42. Helpers might not always behave
    like you think they would

    View Slide

  43. export function isSenior([user]) {
    return user.get('age') >= 65;
    }
    export default Ember.Helper.helper(gte);
    {{#if (is-senior user)}}

    {{/if}}

    View Slide

  44. Computed Properties can easily be
    unit-tested - when using helpers
    you must render the template

    View Slide

  45. {{#each (filter-by users 'isActive' true) as |user|}}
    {{user.name}}
    {{/each}}
    export function filterBy([collection, filterProperty, filterValue]) {
    return collection.filterBy(filterProperty, filterValue);
    }
    export default Ember.Helper.helper(filterBy);
    This is what you want to test

    View Slide

  46. Helpers are harder to debug

    View Slide

  47. Separation of Concerns

    View Slide

  48. potential Maintainability issues

    View Slide

  49. {{#each (filter-by 'isActive' users true) as |user|}}
    {{user.name}}
    {{/each}}

    View Slide

  50. {{#each activeUsers as |user|}}
    {{user.name}}
    {{/each}}
    http://blog.yhat.com/static/img/handlebars-logo.png

    View Slide

  51. value={{user.firstName}}
    onchange={{action (mut firstName) value='target.value'}}/>
    value={{user.lastName}}
    onchange={{action (mut lastName) value='target.value'}}/>
    Cancel
    (pipe
    (action 'validate')
    (action (mut user.firstName) firstName)
    (action (mut user.lastName) lastName)
    (action 'save')
    (action (mut isEditing) false)
    )}}>Save

    View Slide

  52. value={{user.firstName}}
    onchange={{action 'setFirstName' value='target.value'}}/>
    value={{user.lastName}}
    onchange={{action 'setLastName' value='target.value'}}/>
    Cancel
    Save
    +
    save() {
    pipe(
    () => this._validate(),
    () => this.get('user').setProperties(this.getProperties('firstName', 'lastName')),
    () => this.get('user').save(),
    () => this.set('isEditing', false)
    );
    }

    View Slide

  53. …so what's the advice?

    View Slide

  54. small demo project on github
    https://github.com/marcoow/templates-and-logic

    View Slide

  55. Thanks

    View Slide

  56. Q&A

    View Slide

  57. simplabs.com
    @simplabs

    View Slide