The world of Ember macros and how to create your own
Kelly Selden

@kellyselden

Agenda
- Why use a macro?
- Use pre-made macros
- 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,597