The world of Ember macros and how to create your own

Kelly Selden

@kellyselden

Agenda

  1. Why use a macro?
  2. Use pre-made macros
  3. Make your own!

In your template

Custom helpers:

ember-truth-helpers

ember-composable-helpers

ember-math-helpers

others...

In your JS

Custom macros:

ember-cpm

ember-awesome-macros

others...

Where does your logic go?

{{#each users as |user|}}
  <p>
    {{user.fullName}}
    {{#if user.isSpecial}}
      (he's the special one)
    {{/if}}
  </p>
{{/each}}

=>

users: Ember.computed('model', function() {
  let model = this.get('model');
  let shuffledModel = this.shuffle(model);
  let filteredModel = shuffledModel.filterBy('isActive', true);
  let users = filteredModel.slice(0, 1);
  return users.map(user => {
    let firstName = user.get('firstName');
    let lastName = user.get('lastName');
    let fullName = `${firstName.capitalize()} ${lastName.capitalize()}`;
    let firstLetter = firstName[0];
    let capitalLetter = firstLetter.capitalize();
    let isJ = capitalLetter === 'J';
    let isNotSenior = user.get('age') <= 65;
    let isSpecial = isJ && isNotSenior;
    return {
      fullName,
      isSpecial
    };
  });
})
users: Ember.computed('model', function() {
  return this.shuffle(this.get('model')).filterBy('isActive', true).slice(0, 1).map(user => {
    let firstName = user.get('firstName');
    let lastName = user.get('lastName');
    return {
      fullName: `${firstName.capitalize()} ${lastName.capitalize()}`,
      isSpecial: firstName[0].capitalize() === 'J' && user.get('age') <= 65
    };
  });
})

More can be read here:

What is a macro?

Ember computed property macros use dependent properties to lazily calculate a value. The value is cached for subsequent lookups and recalculated when the dependent properties change.

import Ember from 'ember';

export default Ember.Component.extend({
  propA: false,
  propB: true,

  // computed property
  propC: Ember.computed('propA', 'propB', function() {
    return this.get('propA') || this.get('propB');
  }),

  // computed property macro
  propD: Ember.computed.or('propA', 'propB')
});

Macros in action

Prior art

ember-cpm

import Ember from 'ember';
import EmberCPM from 'ember-cpm';

const { Macros: { sum, difference, product } } = EmberCPM;

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')
  )
});

ember-awesome-macros

result: conditional(
  and(not('value1'), 'value2'),
  sum('value3', 1),
  collect('value4', toUpper('value5'))
) // lisp much?

Code reduction

// from
shouldLoadMore: Ember.computed('offset', 'nextOffset', function() {
  return this.get('offset') > this.get('nextOffset');
})

// to
shouldLoadMore: gt('offset', 'nextOffset')

No more temporary properties

// from
_isHidden: Ember.computed.not('isVisible'),
shouldDelete: Ember.computed.and('_isHidden', 'isDeleteEnabled')

// to
shouldDelete: and(not('isVisible'), 'isDeleteEnabled')

Makes side effects impossible

// from
shouldLoadMore: Ember.computed('offset', 'nextOffset', function() {
  this.set('sideEffect', 'yes');
  return this.get('offset') > this.get('nextOffset');
})

// to
shouldLoadMore: gt('offset', 'nextOffset')

Reduces bugs waiting to happen

// from
hasMoreReplies: Ember.computed('offset', 'totalReplies', function() {
  return this.get('shouldFetchReplies') &&
    this.get('offset') < this.get('totalReplies') &&
    this.get('nextOffset') !== this.get('totalReplies');
})

// to
hasMoreReplies: and(
  'shouldFetchReplies',
  lt('offset', 'totalReplies'),
  neq('nextOffset', 'totalReplies')
)

Reduces bugs waiting to happen cont.

// from
result: Ember.computed('prop1', 'prop2', 'prop3', function() {
  return this.get('prop1') + this.get('prop2');
})

// to
result: sum('prop1', 'prop2')

Reduces bugs waiting to happen cont.

// from
shouldAllowDelete: Ember.computed('photos', function() {
  let photos = this.get('photos');
  return photos !== null && photos.length > 0;
})

// to
shouldAllowDelete: and(neq('photos', null), gt('photos.length', 0))

Macro list

Lets explain the concepts

(there's a couple of mental hurdles)

Most basic macro

String keys

import { or } from 'ember-awesome-macros';

{
  input1: false,
  input2: true,

  output: or('input1', 'input2')
}

What else can you put in there?

{
  nextPage: sum('page', 1)
}
{
  shouldShow: neq('myItem', null)
}

What if I want to use string values?

{
  isMatch: eq('name', 'Kelly')
}
import raw from 'ember-macro-helpers/raw';

{
  isMatch: eq('name', raw('Kelly'))
}

Array macros respond to length change

{
  wasFound: includes('myArray', raw('my value'))

  // equivalent to
  wasFound: Ember.computed('myArray.[]', function() {
    return this.get('myArray').includes('my value');
  })
}

Macros support property brace expansion

{
  areAllTrue: and('myObj.{prop1,prop2,prop3}')

  // equivalent to
  areAllTrue: and('myObj.prop1', 'myObj.prop2', 'myObj.prop3')
}
{
  isGreaterThan: gt('myObj.{prop1,prop2}')

  areEqual: eq('myObj.{prop1,prop2,prop3}')
}

The future of string formatting!

{
  source: 'two',

  value1: tag`one ${'source'} three`, // 'one two three'
  value2: capitalize(
    tag`one ${toUpper('source')} three`
  ) // 'One TWO three'
}
{
  style: htmlSafe(tag`background-image: url(${'url'});`)
}
import { moduleForComponent, test } from 'ember-qunit';
import hbs from 'htmlbars-inline-precompile';

moduleForComponent('my-component', 'Integration | Component | my component', {
  integration: true
});

test('it renders', function(assert) {

  // Set any properties with this.set('myProperty', 'value');
  // Handle any actions with this.on('myAction', function(val) { ... });

  this.render(hbs`{{my-component}}`);

  assert.equal(this.$().text().trim(), '');

  // Template block usage:
  this.render(hbs`
    {{#my-component}}
      template block text
    {{/my-component}}
  `);

  assert.equal(this.$().text().trim(), 'template block text');
});

Working with promises is easy(er)

{
  product: Ember.computed(function() {
    return RSVP.resolve({
      name: 'my name'
    });
  }),
  productName: Ember.computed.alias('product.name')
}
{
  product: promiseObject(Ember.computed(function() {
    return RSVP.resolve({
      name: 'my name'
    });
  })),
  productName: Ember.computed.alias('product.name')
}

The macros are read-only (on purpose)

{
  isEither: or('prop1', 'prop2')
}

// ...

// throws read-only error
this.set('isEither', true);
{
    isEither: Ember.computed.or('prop1', 'prop2')
}

// ...

this.set('isEither', true);
{
    isEither: Ember.computed.or('prop1', 'prop2').readOnly()
}

// ...

// throws read-only error
this.set('isEither', true);

"In most cases, we likely actually want our computed properties to be readOnly. This was discussed for ember 2.0.0, but due to the potential upgrade pain it was decided against. In the future this may be reconsidered, but in the interim we can still benefit from this convention.

 

Today, we can easily mark any computed property as readOnly, but wouldn't it be nicer if it was the default?"

Unfortunately, you cannot blindly convert computed properties to this new style

{
  normalizedName: Ember.computed('name', function() {
    return this.get('name').trim().capitalize();
  })
}

// ...

this.set('normalizedName', 'Kelly');
{
  normalizedName: capitalize(trim('name'))
}

// ...

// throws read-only error
this.set('normalizedName', 'Kelly');
import writable from 'ember-macro-helpers/writable';

{
  normalizedName: writable(capitalize(trim('name')))
}

// ...

// no more error!
this.set('normalizedName', 'Kelly');

Last but not least Composing!

result: conditional(
  and(not('value1'), 'value2'),
  sum('value3', 1),
  collect('value4', toUpper('value5'))
)

Caveat: Debugging

result: conditional(
  and(not('value1'), 'value2'),
  sum('value3', 1),
  collect('value4', toUpper('value5'))
)
result: Ember.computed('value1', 'value2', 'value3', 'value4', 'value5', function() {
  let { value1, value1, value1, value1, value1 } = this.getProperties('value1', 'value2', 'value3', 'value4', 'value5');
  debugger;
  if (!value1 && value2) {
    return value3++;
  } else {
    return [value4, value5.toUpperCase()];
  }
})

Caveat: Debugging

// debug doesn't exist yet
result: conditional(
  debug(and(not('value1'), 'value2')),
  debug(sum('value3', 1)),
  debug(collect('value4', toUpper('value5')))
)

Something cool

import math from 'ember-awesome-macros/math';

{
  newValue: math.floor('oldValue')
}
import { floor } from 'ember-awesome-macros/math';

{
  newValue: floor('oldValue')
}
// pseudocode

let macros = {};
for (let i in Math) {
  macros[i] = createMacro(i);
}

export default macros;
// pseudocode

export { createMacro('ceil') as ceil };
export { createMacro('floor') as floor };
// ...
import math from 'ember-awesome-macros/math';

{
  newValue: math.floor('oldValue')
}
import { floor } from 'ember-awesome-macros/math';

{
  newValue: floor('oldValue')
}

Future of

ember-awesome-macros

possible merger with ember-cpm

possible Ember.computed macros deprecation

road to 1.0

* lots of macros being added

* experiments in progress

* possible breaking changes

* help is appreciated!

automatic tree-shaking

use broccoli to create build-time macros

What if the macro you want is specific to your app?

Let's make our own!

ember-macro-helpers

The base library powering:

  • ember-awesome-macros
  • ember-moment
  • ember-computed-decorators
  • ember-cpm
import Ember from 'ember';

// ...

result: Ember.computed('key1', 'key2', function() {
  let key1 = this.get('key1');
  let key2 = this.get('key2');
  return key1 + key2;
})
import computed from 'ember-macro-helpers/computed';

// ...

result: computed('key1', 'key2', function() {
  let key1 = this.get('key1');
  let key2 = this.get('key2');
  return key1 + key2;
})
import computed from 'ember-macro-helpers/computed';

// ...

result: computed('key1', 'key2', function() {
  let key1 = this.get('key1');
  let key2 = this.get('key2');
  return key1 + key2;
})
import computed from 'ember-macro-helpers/computed';

// ...

result: computed('key1', 'key2', (key1, key2) => {
  return key1 + key2;
})
import computed from 'ember-macro-helpers/computed';

// ...

result: computed('array.[]', array => {
  return array.length > 123;
})
import computed from 'ember-macro-helpers/computed';

// ...

result: computed('array.@each.key', array => {
  return array.mapBy('key');
})
import computed from 'ember-macro-helpers/computed';

// ...

result: computed('obj.{key1,key2}', (key1, key2) => {
  return key1 + key2;
})

ember install ember-macro-helpers

ember help generate

ember generate macro add

Demo time

Demo time

Demo time

Thank you

The world of Ember macros and how to create your own

By Kelly Selden

The world of Ember macros and how to create your own

  • 4,269