Home Reference Source

src/behaviors/sortable-store.js

import angular from 'angular';
import {Behavior} from './behavior';
import {Handler as handlerDecorator} from '../store';

import {
  Transformer,
  Transformable as transformableDecorator
} from './transformable';

import {
  addBehavior,
  camelcase,
  Inject as injectDecorator
} from '../utils';

export class SortableStoreBehavior extends Behavior {
  constructor(instance, {collection} = {}) {
    super(...arguments);

    this.collection = collection || 'items';
  }

  get $filter() {
    return this.instance.$filter;
  }

  get transformableCollection() {
    return this.instance.transformables[this.collection];
  }

  get transformer() {
    if (!this._transformer) {
      this._transformer = new Transformer('sortableStore', items => items);
    }
    return this._transformer;
  }

  /**
   * This is a handler proxy for the Store. It get's called with the payload of
   * the ENTITY_SORT_CHANGE action.
   *
   * @param  {String/Array/Boolean} expression
   * This can be any valid angular orderBy $filter expression, or a reverse boolean
   * if the collection we are sorting contains primitives.
   *
   *    Valid angular orderBy $filter expressions are
   *
   *    - `function`: Getter function. The result of this function will be sorted using the
   *      `<`, `===`, `>` operator.
   *    - `string`: An Angular expression. The result of this expression is used to compare elements
   *      (for example `name` to sort by a property called `name` or `name.substr(0, 3)` to sort by
   *      3 first characters of a property called `name`). The result of a constant expression
   *      is interpreted as a property name to be used in comparisons (for example `"special name"`
   *      to sort object by the value of their `special name` property). An expression can be
   *      optionally prefixed with `+` or `-` to control ascending or descending sort order
   *      (for example, `+name` or `-name`). If no property is provided, (e.g. `'+'`) then the array
   *      element itself is used to compare where sorting.
   *    - `Array`: An array of string predicates. The first predicate in the array
   *      is used for sorting, but when two items are equivalent, the next predicate is used.
   */
  onSortChange(expression = false) {
    const collection = this.transformableCollection;
    const transformer = this.transformer;

    let orderByExpression = expression;
    if (typeof expression === 'boolean') {
      orderByExpression = expression ? '-' : '+';
    }

    transformer.fn = items => {
      return this.$filter('orderBy')(items, orderByExpression);
    };

    if (collection.transformers.indexOf(transformer) >= 0) {
      collection.refresh();
    } else {
      collection.addTransformer(transformer);
    }
  }

  onSortClear() {
    this.transformableCollection.removeTransformer(this.transformer);
  }
}

export function SortableStore(config) {
  return cls => {
    let preparedConfig = config;
    if (angular.isString(config)) {
      preparedConfig = {entity: config};
    }

    preparedConfig = Object.assign({
      collection: 'items',
      entity: cls.name.replace(/store$/i, '')
    }, preparedConfig);
    preparedConfig.entity = camelcase(preparedConfig.entity);

    injectDecorator()(cls.prototype, '$filter');
    transformableDecorator()(cls.prototype, preparedConfig.collection);

    const changeHandlerName = `on${preparedConfig.entity}SortChange`;
    const clearHandlerName = `on${preparedConfig.entity}SortClear`;
    handlerDecorator(null, false)(cls.prototype, changeHandlerName);
    handlerDecorator(null, false)(cls.prototype, clearHandlerName);

    addBehavior(cls, SortableStoreBehavior, {
      property: 'sortableStore',
      config: preparedConfig,
      proxy: [
        `${changeHandlerName}:onSortChange`,
        `${clearHandlerName}:onSortClear`
      ]
    });
  };
}