ember-classy-computed icon indicating copy to clipboard operation
ember-classy-computed copied to clipboard

An Ember addon for Class based Computed Properties

ember-classy-computed

npm version Build Status

ember-classy-computed introduce a mechanism for class based computed properties which essentially enables keeping state in computed properties (as opposed to normal computed properties which are stateless).

Keeping that state inside of the computed property instead of in the instance of the class it is defined on enables new kinds of computed property macros that are currently hard to implement:

  • computed property macros can use services without having to inject the service into the class they are defined on
  • computed property macros with dynamic dependent keys
  • computed property macros that depend on external invalidation triggers other than those expressible as dependent keys, e.g. events

Class based computed properties are essentially equivalent to class based helpers.

Use Case

An example use case for a class based computed property is a macro that creates a computed property with dynamic dependent keys that cannot be know upfront.

Defining a computed property that filters a collection property by the value of an attribute of each element is as easy as

filteredUsers: Ember.computed('[email protected]', function() {
  return this.get('users').filterBy('isActive')
})

This will filter the users collection by the isActive attribute of each user so that filteredUsers only includes users for which that attribute is true. This computed property depends on each user's isActive property obviously.

What if the property that the users are to be filtered by might change though? In that case you might write sth. like this:

filteredUsers: Ember.computed('users', 'filter', function() {
  return this.get('users').filterBy(this.get('filter'))
})

This now filter the users by whatever property name is returned by filter. The problem with this is that the filteredUsers property does not depend on any of the individual user's properties anymore so that it would not be recomputed when any of these user's properties change. There is also no way to express the fact that filteredUsers depends on [email protected] when filter is 'isActive' and on [email protected] when filter is 'isAdmin'.

Typically this case would be solved by defining an observer on the context object's filter property and whenever that changes redefining the filteredUsers computed property with the correct dependent keys for the current value of filter (an alternative solution would be to override the filter property's set method and redefine filteredUsers there).

That would make it impossible to reuse the implementation though (except in a mixin which leads to other problems though). ember-classy-computed' mechanism for class based computed properties makes it possible to reuse that implementation- by providing a context for the computed property itself that the observer etc. can be defined on. This allows something like:

import filterByProperty from 'app/computeds/filter-by';

…

filteredUsers: filterByProperty('users', 'filter')

The logic for the filterByProperty macro is encapsulated in the DynamicFilterByComputed class:

// app/computeds/filter-by.js
import Ember from 'ember';
import ClassBasedComputedProperty from 'ember-classy-computed';

const { observer, computed: { filter }, defineProperty } = Ember;

const DynamicFilterByComputed = ClassBasedComputedProperty.extend({
  contentDidChange: observer('content', function() {
    // This method is provided by the ClassBasedComputedProperty
    // base class and invalidates the computed property so that
    // it will get recomputed on the next access.
    this.invalidate();
  }),

  filterPropertyDidChange: observer('filterProperty', function() {
    let filterProperty = this.get('filterProperty');
    let property = filter(`collection.@each.${filterProperty}`, (item) => item.get(filterProperty));
    defineProperty(this, 'content', property);
  }),

  // This method is called whenever the computed property on the context object
  // is recomputed. The same lazy recomputation behavior as for regular computed
  // properties applies here of course. The method receives the current values
  // of its dependent properties as its arguments.
  compute(collection, filterProperty) {
    this.set('collection', collection);
    this.set('filterProperty', filterProperty);

    return this.get('content');
  }
});

export default ClassBasedComputedProperty.property(DynamicFilterByComputed);

Here the computed property's logic is completely self-contained in the DynamicFilterByComputed class so that it can easily be used via the filterByProperty macro while still ensuring correct dependent keys even when the dynamic filter property changes.

Performance

You might wonder whether this adds a huge performance overhead over regular computed properties. While this certainly introduces some overhead, our benchmarks show that for typical use cases, this overhead over functionally equivalent solutions is pretty much negligible.

Installation

ember install ember-classy-computed

License

ember-classy-computed is developed by and © simplabs GmbH and contributors. It is released under the MIT License.