Skip to content

mriska/ember-awesome-macros

 
 

Repository files navigation

ember-awesome-macros

==============================================================================

Greenkeeper badge npm version Build Status dependencies Status devDependencies Status Ember Observer Score Ember Version

A collection of Ember computed macros. All the macros are composable, meaning you can nest them to your heart's content, like so:

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

Arguments to macros can be:

  • A string, in which case it references the name of a property whose value is used for the calculation
  • A non-string value, in which case it is used as such
  • A raw macro, which lets you use a string as the value without referencing a property
  • Another macro

Examples of these different usages:

source1: 'my value',
source2: 2

value1: equal('source1', 'source2'), // false
value2: equal('source2', 2), // true
value3: equal('source1', raw('my value')) // true
value4: equal('source2', sum(1, 1)) // true

Two essential primitive macros come from a different addon: ember-macro-helpers

  • raw makes composing macros easier
  • writable makes setting macros possible

The API is not final until 1.0. I will be adding aliases as I think of better names for things, and possibly breaking or removing existing macros.

If you have any opinions or want a new macro added, just ask! Or feel free to submit a pull request.

Introduction Video

Ember Awesome Macros

Installation

ember install ember-awesome-macros

Usage

import nameOfMacro from 'ember-awesome-macros/name-of-macro';
// or
import { nameOfMacro } from 'ember-awesome-macros';

Macro list

Array
Boolean
Comparison
Number
Object
Math
Promise
String

Custom macros

See [https://github.com/kellyselden/ember-macro-helpers#custom-macros].

Details

add

alias for sum

and

same as Ember.computed.and, but allows composing

source1: false,
source2: true,
source3: false,
value1: and('source1', 'source2', 'source3'), // false
value2: and(not('source1'), 'source2', not('source3')) // true
array.any

wraps Ember.Array.any, allows composing

array: Ember.A([1, 2]),
value1: array.any('array', val => val === 2), // true
value2: array.any('array', val => val === 3) // false
array.compact

wraps Ember.Array.compact, allows composing

array: Ember.A([1, 2, null]),
value: array.compact('array') // [1, 2]
array.concat

wraps Array.prototype.concat(), allows composing

array1: Ember.A([1, 2]),
array2: Ember.A([3, 4]),
string: '3,4',
example: array.concat('array1', 'array2'), // [1, 2, 3, 4]
composingExample: array.concat('array1', split('string', raw(','))) // [1, 2, 3, 4]
array.every

wraps Ember.Array.every, allows composing

array: Ember.A([1, 1]),
value1: array.every('array', val => val === 1), // true
value2: array.every('array', val => val === 2) // false
array.filterBy

wraps Ember.Array.filterBy, allows composing

array: Ember.A([{ test: 1 }, { test: 2 }]),
key: 'test',
referenceValue: 1,
value1: array.filterBy('array', 'key', 2), // [{ test: 2 }]
value2: array.filterBy('array', raw('test'), 'referenceValue') // [{ test: 1 }]
array.filter

wraps Ember.Array.filter, allows composing

array: Ember.A([{ test: 1 }, { test: 2 }]),
value: array.filter('array', item => item.test === 2) // [{ test: 2 }]
array.findBy

wraps Ember.Array.findBy, allows composing

array: Ember.A([{ test: 1 }, { test: 2 }]),
key: 'test',
referenceValue: 1,
value1: array.findBy('array', 'key', 2), // { test: 2 }
value2: array.findBy('array', raw('test'), 'referenceValue') // { test: 1 }
array.find

wraps Ember.Array.find, allows composing

array: Ember.A([{ test: 1 }, { test: 2 }]),
value: array.find('array', item => item.test === 2) // { test: 2 }
array.first

get the first item of an array

array: ['1', '2'],
string: '1, 2',
example: array.first('array'), // '1'
composingExample: array.first(split('string', raw(', '))) // '1'
array.groupBy

allows to group an array of objects by key with an optional comparator

array: Ember.A([{ test: 1, name: 'foo'}, { test: 2, name: 'foo' }, { test: 1, name: 'bar' }]),
key: 'test',
value: array.groupBy('array', 'key')
/*
  [
    {
      key: 'test',
      value: 1,
      items: [{ test: 1 , name: 'foo' }, { test: 1 , name: 'bar' }]
    },
    {
      key: 'test',
      value: 2,
      items: [{ test: 2 , name: 'foo' }]
    }
  ]
*/

The comparator is required if grouping by a key representing a complex object like Date

// Given two different objects that represent the same info
// today1 = new Date();
// today2 = new Date();
// randomDate = new Date(2017, 1, 1);
array: Ember.A([
  { test: 1 , date: today1 },
  { test: 2 , date: today2 },
  { test: 1 , date: randomDate }
]),
key: 'date'

// without comparator
value1: array.groupBy('array', 'key'),
/*
  [
    {
      key: 'date',
      value: today1,
      items: [{ test: 1 , date: today1 }]
    },
    {
      key: 'date',
      value: today2,
      items: [{ test: 2 , date: today2 }]
    },
    {
      key: 'date',
      value: randomDate,
      items: [{ test: 1 , date: randomDate }]
    }
  ]
*/

// with comparator
value2: array.groupBy('array', 'key', (groupValue, currentValue) => {
  return groupValue.getTime() === currentValue.getTime();
})
/*
  [
    {
      key: 'date',
      value: today1,
      items: [{ test: 1 , date: today1 }, { test: 2 , date: today2 }]
    },
    {
      key: 'date',
      value: randomDate,
      items: [{ test: 1 , date: randomDate }]
    }
  ]
*/
array.includes

implements Array.prototype.includes(), allows composing

array: Ember.A(['my value 1', 'my value 2']),
source1: 'my value 2',
source2: 'my value 3',
value1: array.includes('array', 'source1'), // true
value2: array.includes('array', 'source2'), // false
value3: array.includes(collect(raw('my value 1'), raw('my value 2')), raw('my value 1')) // true
array.indexOf

wraps Array.prototype.indexOf(), allows composing

array: [2, 5, 9, 2],
referenceValue: 9,
value1: array.indexOf('array', 2), // 0
value2: array.indexOf('array', 2, 2), // 3
value3: array.indexOf('array', 'referenceValue') // 2
array.invoke

wraps Ember.Array.invoke(), allows composing

array: [{
  foo(arg) {
    return 'bar-' + arg;
  }
}],
value1: array.invoke('array', 'foo', raw('baz')), // ['bar-baz']
value2: array.invoke('array', 'foo', 'arg'), // ['bar-hello']
value3: array.invoke('array', 'foo'), // ['bar-']
arg: 'hello'
array.isAny

wraps Ember.Enumerable.isAny, allows composing

array: Ember.A([{ test: 1 }, { test: 2 }]),
key: 'test',
value1: 2,
value2: 3,
result1: array.isAny('array', 'key', 'value1'), // true
result2: array.isAny('array', 'key', 'value2') // false
array.isEvery

wraps Ember.Enumerable.isEvery, allows composing

array1: Ember.A([{ test: 1 }, { test: 1 }]),
key: 'test',
value1: 1,
value2: 2,
result1: array.isEvery('array', 'key', 'value1'), // true
result2: array.isEvery('array', 'key', 'value2') // false
array.join

wraps Array.prototype.join(), allows composing

array: Ember.A(['1', '2']),
separator: ', ',
value1: array.join('values', 'separator'), // '1, 2'
value2: array.join(collect(raw('1'), raw('2')), raw(', ')) // '1, 2'
array.lastIndexOf

wraps Array.prototype.lastIndexOf(), allows composing

array: [2, 5, 9, 2],
referenceValue: 9,
value1: array.lastIndexOf('array', 2), // 3
value2: array.lastIndexOf('array', 2, 2), // 0
value3: array.lastIndexOf('array', 'referenceValue') // 2
array.last

get the last item of an array

array: ['1', '2'],
string: '1, 2',
example: array.last('array'), // '2'
composingExample: array.last(split('string', raw(', '))) // '2'
array.length

wraps Array.prototype.length, allows composing

array: Ember.A([1, 2]),
string: '1,2',
example: array.length('array'), // 2
composingExample: array.length(split('string', raw(','))) // 2
array.mapBy

wraps Ember.Array.mapBy, allows composing

array: Ember.A([{ test: 1 }, { test: 2 }]),
key: 'test',
value: array.mapBy('array', 'key') // [1, 2]
array.map

wraps Ember.Array.map, allows composing

array: Ember.A([{ test: 1 }, { test: 2 }]),
value: array.map('array', item => item.test) // [1, 2]
array.objectAt

wraps Ember.Array.objectAt, allows composing

array: Ember.A(['my value']),
source1: 0,
source2: 1,
value1: array.objectAt('array', 'source1'), // 'my value'
value2: array.objectAt('array', 'source2'), // undefined
value3: array.objectAt(collect(raw('my value 1')), raw(0)) // 'my value'
array.reduce

wraps Array.prototype.reduce(), allows composing

Your initial value must be a factory function if you plan to mutate it in the reduce, otherwise it would continually mutate the same object every recalculation. You can still pass an object, just be careful not to mutate it.

array: ['one', 'two'],
value1: array.reduce(
  'array',
  (obj, cur, i) => {
    obj[cur] = i;
    return obj;
  },
  // initial value is a factory function because we mutate the object
  () => ({})
), // { one: 0, two: 1 }

string: 'one, two',
value2: array.reduce(
  split('string', raw(', ')),
  (arr, cur, i) => arr.concat(cur, i),
  //initial value is an array because it is not mutated
  []
) // ['one', 0, 'two', 1]
array.rejectBy

wraps Ember.Array.rejectBy, allows composing

array: Ember.A([{ test: 1 }, { test: 2 }]),
key: 'test',
value: array.rejectBy('array', 'key', 2) // [{ test: 1 }]
array.reverse

wraps Array.prototype.reverse() (calls slice() first as to not mutate), allows composing

array: [1, 2, 3],
value1: array.reverse('array'), // [3, 2, 1]
value2: array.reverse(array.reverse('array')) // [1, 2, 3]
array.slice

wraps Array.prototype.slice(), allows composing

array: [1, 2, 3],
value1: array.slice('array', 1), // [2, 3]
value2: array.slice('array', difference('array.length', 1)) // [3]
array.sort

combines the functionality of both Array.prototype.sort() and Ember.computed.sort

array1: Ember.A(['xyz', 'abc']),
array2: Ember.A([{ key: 'abc' }, { key: 'xyz' }]),
value1: array.sort('array1'), // ['abc', 'xyz']
value2: array.sort('array2', ['key:desc']), // [{ key: 'xyz' }, { key: 'abc' }]
value3: array.sort('array2', (a, b) => a.key < b.key), // [{ key: 'xyz' }, { key: 'abc' }]
array.uniqBy

wraps Ember.Array.uniqBy, allows composing

array: Ember.A([{ test: 1 }, { test: 2 }, { test: 2 }]),
key: 'test',
value: array.uniqBy('array', 'key') // [{ test: 1 }, { test: 2 }]
array.uniq

wraps Ember.Array.uniq, allows composing

array: Ember.A([1, 2, 2]),
value: array.uniq('array') // [1, 2]
array.without

wraps Ember.Enumerable.without, allows composing

array: Ember.A([1, 2, 3]),
referenceValue: 3,
value1: array.without('array', 'referenceValue'), // [1, 2]
value2: array.without('array', 2), // [1, 3]
value3: array.without('array', array.objectAt(1)) // [1, 3]
collect

same as Ember.computed.collect, but allows composing

source1: 'my value 1',
source2: 'my value 2',
value: collect('source1', collect('source2')) // ['my value 1', ['my value 2']]
conditional

implements the ternary operator, allows composing

condition1: true,
condition2: false,
expr1: 'my value 1',
expr2: 'my value 2',
value1: conditional('condition1', 'expr1', 'expr2'), // 'my value 1'
value2: conditional('condition2', 'expr1', 'expr2'), // 'my value 2'
value3: conditional(or('condition1', 'condition2'), raw('my value 1'), raw('my value 2')) // 'my value 1'
defaultTrue

true if source is undefined

source1: undefined,
source2: false,
source3: 'my value',
value1: defaultTrue('source1'), // true
value2: defaultTrue('source2'), // false
value3: defaultTrue('source3') // 'my value'
difference

subtracts numbers

source1: 3,
source2: 2,
source3: 1,
value1: difference('source1', 'source2', 'source3'), // 0
value2: difference('source1', collect('source2', 'source3')) // 2
divide

alias for quotient

eq

alias for equal

equal

like Ember.computed.equal, but uses dependent properties on both sides. allows N number of arguments.

source1: 'my value',
source2: 'my other value',
source3: 'my value',
value1: equal('source1', 'source2'), // false
value2: equal('source1', 'source3'), // true
value3: equal('source1', 'source2', 'source3') // false
getBy

get a variable property name from an object

key: 'modelProperty',
model: {
  modelProperty: 'my value'
},
value1: getBy('model', 'key'), // 'my value'
value2: getBy('model', raw('modelProperty')) // 'my value'
gt

like Ember.computed.gt, but uses dependent properties on both sides and allows composing

source1: 1,
source2: 2,
source3: 1,
value1: gt('source1', 'source2'), // false
value2: gt('source1', 'source3'), // false
value3: gt('source2', 'source3') // true
gte

like Ember.computed.gte, but uses dependent properties on both sides and allows composing

source1: 1,
source2: 2,
source3: 1,
value1: gte('source1', 'source2'), // false
value2: gte('source1', 'source3'), // true
value3: gte('source2', 'source3') // true
hash

build a hash out of computed properties, allows composing

source1: 'my value 1',
source2: 'my value 2',

value1: hash({
  prop1: 'source1',
  prop2: hash({
    prop: 'source2'
  })
}), // { prop1: 'my value 1', prop2: { prop: 'my value 2' } }

// you can also build the hash using property key names
value2: hash('source1', 'source2'), // { source1: 'my value 1', source2: 'my value 2' }

// or you can mix and match, the result will be merged
value3: hash('source1', { prop2: 'source2' }) // { source1: 'my value 1', prop2: 'my value 2' }
instanceOf

wraps instanceof operator

key1: {},
key2: false,
key3: '',
value1: instanceOf('key1', Object), // true
value2: instanceOf(or('key2', 'key3'), String) // true
lt

like Ember.computed.lt, but uses dependent properties on both sides and allows composing

source1: 1,
source2: 2,
source3: 1,
value1: lt('source1', 'source2'), // true
value2: lt('source1', 'source3'), // false
value3: lt('source2', 'source3') // false
lte

like Ember.computed.lte, but uses dependent properties on both sides and allows composing

source1: 1,
source2: 2,
source3: 1,
value1: lte('source1', 'source2'), // true
value2: lte('source1', 'source3'), // true
value3: lte('source2', 'source3') // false
math

exposes all Math functions

source1: 2.2,
source2: 2.7,
value1: math.ceil('source1'), // 3
value2: math.floor(sum('source1', 'source2')) // 4
mod

the modulus operator

number1: 123,
number2: 45,
example: mod('number1', 'number2'), // 33
composingExample: mod(sum('number1', 'number2'), 39) // 12
multiply

alias for product

neq

alias for notEqual

not

same as Ember.computed.not, but allows composing

source1: true,
source2: false,
value1: not('source1'), // false
value2: not(and('source1', 'source2')) // true
notEqual

the inverse of equal, allows composing

source1: 'my value',
source2: 'my other value',
source3: 'my value',
value1: notEqual('source1', 'source2'), // true
value2: notEqual('source1', 'source3'), // false
value3: notEqual('source1', 'source2', 'source3') // true
or

same as Ember.computed.or, but allows composing

source1: true,
source2: false,
source3: true,
value1: or('source1', 'source2', 'source3'), // true
value2: or(not('source1'), 'source2', not('source3')) // false
parseFloat

wraps parseFloat, allows composing

string1: '12.34',
string2: '12',
string3: '34',
example: parseFloat('string1'), // 12.34
composingExample: parseFloat(tag`${'string2'}.${'string3'}`) // 12.34
parseInt

wraps parseInt, allows composing

string: '123',
example: parseInt('string'), // 123
composingExample: parseInt(substr('string', 1), 8) // 19
product

multiplies numbers

source1: 1,
source2: 2,
source3: 3,
value1: product('source1', 'source2', 'source3'), // 6
value2: product('source1', collect('source2', 'source3')) // 6
promise.all

combines promises using RSVP.all

promise1: computed(function() {
  return RSVP.resolve('value1');
}),
promise2: computed(function() {
  return RSVP.resolve('value2');
}),
promise: promise.all('promise1', 'promise2') // resolves to ['value1', 'value2']
promise.array

wraps a promise in the equivalent of DS.PromiseArray (ArrayProxy and PromiseProxyMixin)

productsPromise: computed(function() {
  return this.store.findAll('product');
}),
products: promise.array('productsPromise')

can also wrap a computed property

products: promise.array(computed(function() {
  return this.store.findAll('product');
}))
promise.hash

combines promises using RSVP.hash

promise1: computed(function() {
  return RSVP.resolve('value1');
}),
promise2: computed(function() {
  return RSVP.resolve('value2');
}),
promise: promise.hash('promise1', 'promise2') // resolves to { promise1: 'value1', promise2: 'value2' }
promise.object

wraps a promise in the equivalent of DS.PromiseObject (ObjectProxy and PromiseProxyMixin)

productPromise: computed(function() {
  return this.store.findRecord('product', 1);
}),
product: promise.object('productPromise')

can also wrap a computed property

product: promise.object(computed(function() {
  return this.store.findRecord('product', 1);
}))
promise.resolve

wraps a value in an RSVP.resolve

key1: 'my value',
promise1: promise.resolve('key1'), // a resolved promise if you need it

key2: computed(function() {
  return this.store.findRecord('user');
}),
promise2: promise.resolve(conditional('someBool', 'key1', 'key2')) // resolve an object if you don't know if it is a promise or not
promise.then

calls .then() on a promise and then .get on the result

key: 'firstName',
userPromise: computed(function() {
  return this.store.findRecord('user'); // { firstName: 'Mary' }
}),

firstName: promise.then('userPromise', 'key') // 'Mary'
quotient

divides numbers

source1: 3,
source2: 2,
source3: 1,
value1: quotient('source1', 'source2', 'source3'), // 1.5
value2: quotient('source1', collect('source2', 'source3')) // 1.5
string.camelize

wraps Ember.String.camelize, allows composing

originalValue: 'test-string',
newValue: string.camelize('originalValue') // 'testString'
string.capitalize

wraps Ember.String.capitalize, allows composing

originalValue: 'test string',
newValue: string.capitalize('originalValue') // 'Test string'
string.classify

wraps Ember.String.classify, allows composing

originalValue: 'test string',
newValue: string.classify('originalValue') // 'TestString'
string.dasherize

wraps Ember.String.dasherize, allows composing

originalValue: 'TestString',
newValue: string.dasherize('originalValue') // 'test-string'
string.decamelize

wraps Ember.String.decamelize, allows composing

originalValue: 'TestString',
newValue: string.decamelize('originalValue') // 'test_string'
string.escapeExpression

wraps Ember.Handlebars.Utils.escapeExpression, allows composing

originalValue: '<input>',
newValue: string.escapeExpression('originalValue') // '&lt;input&gt;'
string.htmlSafe

wraps Ember.String.htmlSafe, allows composing

originalValue: '<input>',
newValue: string.htmlSafe('originalValue') // will not be escaped
string.indexOf

wraps String.prototype.indexOf(), allows composing

string: '121',
value: '1',
example: string.indexOf('string', 'value'), // 0
composingExample: string.indexOf(substr('string', 1), raw('1')) // 1
string.isHtmlSafe

wraps Ember.String.isHTMLSafe, allows composing

source1: '<input>',
source2: string.htmlSafe('<input>'),
value1: string.isHtmlSafe('source1'), // false
value2: string.isHtmlSafe('source2') // true
string.lastIndexOf

wraps String.prototype.lastIndexOf(), allows composing

string: '121',
value: '1',
example: string.lastIndexOf('string', 'value'), // 2
composingExample: string.lastIndexOf(substr('string', 0, 2), raw('1')) // 0
string.length

wraps String.prototype.length, allows composing

string1: 'abc',
string2: 'xyz',
example: string.length('string1'), // 3
composingExample: string.length(tag`${'string1'}${'string2'}`) // 6
string.match

wraps String.prototype.match, allows composing

string1: 'abc',
string2: 'xyz',
regex1: /abc/,
regex2: /xyz/,
example: string.match('string1', 'regex1'), // ['abc']
example: string.match('string1', 'regex2'), // null
composingExample: string.match(tag`${'string1'}${'string2'}`, 'regex2') // ['xyz']
string.replace

wraps String.prototype.replace, allows composing

string: 'abc',
substr: 'bc',
newSubstr: 'cb',
value1: string.replace('string', 'substr', 'newSubstr'), // 'acb'
value2: string.replace('source', 'substr', string.toUpper('newSubstr')) // 'aCB'
string.split

wraps String.prototype.split, allows composing

source: 'val1,val2',
key: ',',
value1: string.split('source', 'key'), // ['val1', 'val2']
value2: string.split('source', raw(',')) // ['val1', 'val2']
string.substr

wraps String.prototype.substr(), allows composing

string1: 'abcxyz',
string2: 'abc',
string3: 'xyz',
example: string.substr('string1', 2, 2), // 'cx'
composingExample: string.substr(tag`${'string2'}${'string3'}`, 2, 2) // 'cx'
string.substring

wraps String.prototype.substring(), allows composing

string1: 'abcxyz',
string2: 'abc',
string3: 'xyz',
example: string.substring('string1', 2, 4), // 'cx'
composingExample: string.substring(tag`${'string2'}${'string3'}`, 2, 4) // 'cx'
string.titleize

capitalizes words, allows composing

originalValue: 'james mcAvoy',
newValue: string.titleize('originalValue') // 'James Mcavoy'
string.toLower

wraps String.prototype.toLowerCase(), allows composing

originalValue: 'TestString',
newValue: string.toLower('originalValue') // 'teststring'
string.toUpper

wraps String.prototype.toUpperCase(), allows composing

originalValue: 'TestString',
newValue: string.toUpper('originalValue') // 'TESTSTRING'
string.trim

wraps String.prototype.trim(), allows composing

originalValue: ' TestString ',
newValue: string.trim('originalValue') // 'TestString'
string.underscore

wraps Ember.String.underscore, allows composing

originalValue: 'TestString',
newValue: string.underscore('originalValue') // 'test_string'
subtract

alias for difference

sum

adds numbers

source1: 1,
source2: 2,
source3: 3,
value1: sum('source1', 'source2', 'source3'), // 6
value2: sum('source1', collect('source2', 'source3')) // 6
tag

use a tagged template literal as a computed macro, allows composing

source: 'two',
value1: tag`one ${'source'} three`, // 'one two three'
value2: tag`one ${toUpper('source')} three` // 'one TWO three'
toStr

calls toString with args on whatever you provide

key1: {},
key2: 253,
key3: 254,
value1: toStr('key1'), // '[object Object]'
value2: toStr(math.max('key2', 'key3'), 16) // 'fe'
toString

alias for toStr

The toString name conflicts with window.toString and breaks Babel, which means you need to do something like this:

import { toString as toStr } from 'ember-awesome-macros';

That's why we provide the toStr alias.

typeOf

wraps typeOf operator

key1: {},
key2: false,
key3: '',
value1: typeOf('key1'), // 'object'
value2: typeOf(or('key2', 'key3')) // 'string'
unless

macro version of {{unless}} and the inverse of conditional, allows composing

condition1: false,
condition2: true,
expr1: 'my value 1',
expr2: 'my value 2',
value1: unless('condition1', 'expr1', 'expr2'), // 'my value 1'
value2: unless('condition2', 'expr1', 'expr2'), // 'my value 2'
value3: unless(and('condition1', 'condition2'), raw('my value 1'), raw('my value 2')) // 'my value 1'

Contributing

Installation

  • git clone <repository-url>
  • cd ember-awesome-macros
  • npm install

Linting

  • npm run lint:js
  • npm run lint:js -- --fix

Running tests

  • ember test – Runs the test suite on the current Ember version
  • ember test --server – Runs the test suite in "watch mode"
  • ember try:each – Runs the test suite against multiple Ember versions

Running the dummy application

For more information on using ember-cli, visit https://ember-cli.com/.

License

This project is licensed under the MIT License.

About

A random collection of Ember computed macros

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • JavaScript 99.2%
  • HTML 0.8%