Kelly Selden
@kellyselden
Custom helpers:
ember-truth-helpers
ember-composable-helpers
ember-math-helpers
others...
Custom macros:
ember-cpm
ember-awesome-macros
others...
{{#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
};
});
})
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')
});
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')
)
});
result: conditional(
and(not('value1'), 'value2'),
sum('value3', 1),
collect('value4', toUpper('value5'))
) // lisp much?
// from
shouldLoadMore: Ember.computed('offset', 'nextOffset', function() {
return this.get('offset') > this.get('nextOffset');
})
// to
shouldLoadMore: gt('offset', 'nextOffset')
// from
_isHidden: Ember.computed.not('isVisible'),
shouldDelete: Ember.computed.and('_isHidden', 'isDeleteEnabled')
// to
shouldDelete: and(not('isVisible'), 'isDeleteEnabled')
// from
shouldLoadMore: Ember.computed('offset', 'nextOffset', function() {
this.set('sideEffect', 'yes');
return this.get('offset') > this.get('nextOffset');
})
// to
shouldLoadMore: gt('offset', 'nextOffset')
// 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')
)
// from
result: Ember.computed('prop1', 'prop2', 'prop3', function() {
return this.get('prop1') + this.get('prop2');
})
// to
result: sum('prop1', 'prop2')
// 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))
import { or } from 'ember-awesome-macros';
{
input1: false,
input2: true,
output: or('input1', 'input2')
}
{
nextPage: sum('page', 1)
}
{
shouldShow: neq('myItem', null)
}
{
isMatch: eq('name', 'Kelly')
}
import raw from 'ember-macro-helpers/raw';
{
isMatch: eq('name', raw('Kelly'))
}
{
wasFound: includes('myArray', raw('my value'))
// equivalent to
wasFound: Ember.computed('myArray.[]', function() {
return this.get('myArray').includes('my value');
})
}
{
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}')
}
{
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');
});
{
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')
}
{
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?"
{
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');
result: conditional(
and(not('value1'), 'value2'),
sum('value3', 1),
collect('value4', toUpper('value5'))
)
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()];
}
})
// debug doesn't exist yet
result: conditional(
debug(and(not('value1'), 'value2')),
debug(sum('value3', 1)),
debug(collect('value4', toUpper('value5')))
)
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')
}
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
The base library powering:
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;
})