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.

3179e6bb62dc91d29bb906ffef4fa2d4?s=128

Marco Otte-Witte

October 27, 2016
Tweet

Transcript

  1. Templates and Logic

  2. Marco Otte-Witte @marcoow

  3. simplabs.com @simplabs

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

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

  7. Templates and Logic

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

  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/
  10. <div class="entry"> <h1>{{title}}</h1> <div class="body"> {{body}} </div> </div> { title:

    'A post', body: 'Awesome post!' } + = <div class="entry"> <h1>A post</h1> <div class="body"> Awesome post! </div> </div>
  11. <div class="entry"> <h1>{{title}}</h1> <div class="body"> {{body}} </div> {{#if author}} <h1>{{author.firstName}}

    {{author.lastName}}</h1> {{/if}} </div>
  12. <div class="entry"> <h1>{{title}}</h1> <div class="body"> {{body}} </div> {{#if author}} <h1>{{author.firstName}}

    {{author.lastName}}</h1> {{/if}} <h1>Comments</h1> {{#each comments as |comment|}} <h2>By {{comment.author}}</h2> <div class="body">{{comment.text}}</div> {{/each}} </div>
  13. <div class="entry"> <h1>{{title}}</h1> <div class="body"> {{body}} </div> {{#if author}} <h1>{{fullName

    author}}</h1> {{/if}} <h1>Comments</h1> {{#each comments as |comment|}} <h2>By {{fullName comment.author}}</h2> <div class="body">{{comment.text}}</div> {{/each}} </div>
  14. …templates are not "logicless" but Handlebars.js itself is (more or

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

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

    {{#let user.firstName user.lastName as |firstName lastName|}} <p> {{concat (capitalize firstName) ' ' (capitalize lastName)}} {{#if (and (eq (capitalize (take 1 firstName)) 'J') (lte user.age 65))}} (he's the special one!) {{/if}} </p> {{/let}} {{/each}}
  17. None
  18. {{#each (take 1 (filter-by (shuffle model) 'isActive' true)) as |user|}}

    {{#let user.firstName user.lastName as |firstName lastName|}} <p> {{concat (capitalize firstName) ' ' (capitalize lastName)}} {{#if (and (eq (capitalize (take 1 firstName)) 'J') (lte user.age 65))}} (he's the special one!) {{/if}} </p> {{/let}} {{/each}}
  19. <?php each (take 1 (filter-by (shuffle model) 'isActive' true)) as

    |user| ?> <?php let user.firstName user.lastName as |firstName lastName| ?> <p> <?php concat (capitalize firstName) ' ' (capitalize lastName) ?> <?php if (and (eq (capitalize (take 1 firstName)) 'J') (lte user.age 65)) ?> (he's the special one!) <?php /if ?> </p> <?php /let ?> <?php /each ?> 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
  20. it's fine (and necessary) to use logic in templates

  21. …but to what extent?

  22. 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; })
  23. 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])); })
  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.gte('age', 65)
  25. {{#each (filter-by users 'isActive' true) as |user|}} {{user.name}} {{/each}} =

    {{#each activeUsers as |user|}} {{user.name}} {{/each}} activeUsers: computed('users.@each.isActive', 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);
  26. {{#each (filter-by users 'isActive' true) as |user|}} {{user.name}} {{/each}} =

    {{#each activeUsers as |user|}} {{user.name}} {{/each}} activeUsers: computed('users.@each.isActive', 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);
  27. {{#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);
  28. but what about helpers being pure?

  29. function fullName([firstName, lastName]) { return `${firstName} ${lastName}`; } fullName: computed('firstName',

    'lastName', function() { return `${this.get('firstName')} ${this.get('lastName')}`; }) Inputs
  30. 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); } =
  31. fullName: join('firstName', 'lastName', ' ') https://github.com/cibernox/ember-cpm

  32. 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
  33. @computed('firstName', 'lastName') fullName(firstName, lastName) { return `${firstName} ${lastName}`; } https://github.com/rwjblue/ember-computed-decorators

  34. this topic is not limited to reactive logic

  35. <input type="text" value={{user.firstName}} onchange={{action (mut firstName) value='target.value'}}/> <input type="text" value={{user.lastName}}

    onchange={{action (mut lastName) value='target.value'}}/> <button onclick={{action (mut isEditing) false}}>Cancel</button> <button onclick={{action (pipe (action 'validate') (action (mut user.firstName) firstName) (action (mut user.lastName) lastName) (action 'save') (action (mut isEditing) false) )}}>Save</button>
  36. <input type="text" value={{user.firstName}} onchange={{action 'setFirstName' value='target.value'}}/> <input type="text" value={{user.lastName}} onchange={{action

    'setLastName' value='target.value'}}/> <button onclick={{action 'cancelEditing'}}>Cancel</button> <button onclick={{action 'save'}}>Save</button> save() { this._validate().then(() => { this.get('user').setProperties( this.getProperties('firstName', 'lastName') ); }).then(() => { return this.get('user').save(); }).then(() => { this.set('isEditing', false) }); } +
  37. <input type="text" value={{user.firstName}} onchange={{action 'setFirstName' value='target.value'}}/> <input type="text" value={{user.lastName}} onchange={{action

    'setLastName' value='target.value'}}/> <button onclick={{action 'cancelEditing'}}>Cancel</button> <button onclick={{action 'save'}}>Save</button> + save() { pipe( () => this._validate(), () => this.get('user').setProperties(this.getProperties('firstName', 'lastName')), () => this.get('user').save(), () => this.set('isEditing', false) ); }
  38. Why is this all even relevant?

  39. There are a bunch of tradeoffs you need to be

    aware of
  40. Helpers might not always behave like you think they would

  41. export function isSenior([user]) { return user.get('age') >= 65; } export

    default Ember.Helper.helper(gte); {{#if (is-senior user)}} {{/if}}
  42. Computed Properties can easily be unit-tested - when using helpers

    you must render the template
  43. {{#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
  44. Helpers are harder to debug

  45. Separation of Concerns

  46. potential Maintainability issues

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

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

  49. <input type="text" value={{user.firstName}} onchange={{action (mut firstName) value='target.value'}}/> <input type="text" value={{user.lastName}}

    onchange={{action (mut lastName) value='target.value'}}/> <button onclick={{action (mut isEditing) false}}>Cancel</button> <button onclick={{action (pipe (action 'validate') (action (mut user.firstName) firstName) (action (mut user.lastName) lastName) (action 'save') (action (mut isEditing) false) )}}>Save</button>
  50. <input type="text" value={{user.firstName}} onchange={{action 'setFirstName' value='target.value'}}/> <input type="text" value={{user.lastName}} onchange={{action

    'setLastName' value='target.value'}}/> <button onclick={{action 'cancelEditing'}}>Cancel</button> <button onclick={{action 'save'}}>Save</button> + save() { pipe( () => this._validate(), () => this.get('user').setProperties(this.getProperties('firstName', 'lastName')), () => this.get('user').save(), () => this.set('isEditing', false) ); }
  51. …so what's the advice?

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

  53. Thanks

  54. Q&A

  55. simplabs.com @simplabs