addon/components/e3-collection.js

Summary

Maintainability
A
1 hr
Test Coverage
import Ember from 'ember';
import layout from '../templates/components/e3-collection';
import Renderable from '../utils/shadow/renderable';
import calculateChanges from '../utils/e3-calculate-changes';
const {keys} = Object;
const {
  get, set, computed, guidFor,
  run: {scheduleOnce}
} = Ember;

let e3Collection = Ember.Component.extend({
  tagName: '',
  layout: layout,
  data: {},

  /*
   This represents the components we should be rendering for each of the
   items in the array. 'tis a map!
   */
  components: computed(function() {
    return {};
  }),

  /*
   We should only yield the first item and then capture which "register" events
   are created. Then, we can re-create those register events for each of the subsequent
   items in the array.

   - Then, we need to update ALL
   - Keep a Map internally of all the objects; need to determine which are new, which are
     going away, etc.
   */

  // Methods that need to be handled
  getType() {
    return this.getAttr('_e3Context').getType();
  },

  /*
   Prevent the child from doing any actual rendering.
   */
  animateTo() {},

  /*
   When registering a child, this is an indication that we have a component that
   should be created for each of the items in the array.
   */
  register(component) {
    let guid = guidFor(component);
    set(this, 'components.'+guid, {
      component: component,
      dataState: [],
      children: Object.create(null)
    });

    this.queueProcessing();
  },

  childWillDestroy(component) {
    let components = get(this, 'components');
    let guid = guidFor(component);
    let {children} = components[guid];
    delete components[guid];

    keys(children).forEach(key => {
      let pseudoComponent = children[key];
      this.removePseudoComponent(component, pseudoComponent);
    });

    return true;
  },

  didUpdateAttrs(attrs) {
    let updated = this.getAttrFor(attrs.newAttrs, 'iterable');
    let old = this.getAttrFor(attrs.oldAttrs, 'iterable');

    // If the iterable changed only:
    if(updated !== old) {
      this.queueProcessing();
    }
  },

  childDidUpdateAttrs() {
    this.queueProcessing();
    return true;
  },

  registerPseudoComponent(pseudoComponent) {
    this.getAttr('_e3Context').register(pseudoComponent);
  },

  unregisterPseudoComponent(pseudoComponent) {
    this.getAttr('_e3Context').unregister(pseudoComponent);
  },


  queueProcessing() {
    scheduleOnce('afterRender', this, 'processIterable');
  },

  /*
   This will go and register/unregister/update all of the combinations of components
   and items in the array according to the newest state.
   */
  processIterable() {
    let iterable = this.getAttr('iterable');
    let itemKey = this.getAttr('key');
    let components = get(this, 'components');

    let iterableGuids = [];
    let iterableMap = Object.create(null);

    // Populate the guids/map for the current data.
    iterable.forEach(data => {
      let guid = itemKey ? get(data, itemKey) : guidFor(data);
      iterableGuids.push(guid);
      iterableMap[guid] = data;
    });

    // For each of the components, perform operations on enter/exit/active
    keys(components).forEach(key => {
      let component = components[key];
      this.processIterableComponent(component, iterableGuids, iterableMap);
    });
  },

  /*
   Given one of the descendant components, process the changes given the
   */
  processIterableComponent(componentItem, iterableGuids, iterableMap) {
    let {component, children, dataState} = componentItem;
    let {enter, exit, active} = calculateChanges(dataState, iterableGuids);
    let type = this.getType();
    componentItem.dataState = active;

    // For the new items, create a shadow object.
    enter.forEach(guid => {
      let data = iterableMap[guid];
      let enterState = component.generateState('enterState', data);

      // Save this shadow as a child.
      let pseudoComponent = {
        _previousState: enterState,
        lastData: data,
        guid: guid,
        shadow: this.generateShadowObject(component, type, enterState, data)
      };

      children[guid] = pseudoComponent;
      this.registerPseudoComponent(pseudoComponent);
    });

    // For the leaving items, animate them out.
    exit.forEach(guid => {
      let pseudoComponent = children[guid];
      delete children[guid];
      this.removePseudoComponent(component, pseudoComponent);
    });

    active.forEach(guid => {
      let data = iterableMap[guid];
      let child = children[guid];
      let activeState = component.generateState('activeState', data);
      let animation = component.generateAnimationState(data);
      child.lastData = data;
      child.shadow.setData(data);
      this.triggerAnimateTo(child, activeState, animation);
    });
  },

  triggerAnimateTo(pseudoComponent, resultState, animation, finishedCallback) {
    this.getAttr('_e3Context').animateTo(pseudoComponent, resultState, animation, finishedCallback);
  },

  /*
   Hook to animate out a pseudo compoennt
   */
  removePseudoComponent(component, pseudoComponent) {
    let data = pseudoComponent.lastData;
    let exitState = component.generateState('exitState', data);
    let animation = component.generateAnimationState(data);

    this.triggerAnimateTo(pseudoComponent, exitState, animation, () => {
      this.unregisterPseudoComponent(pseudoComponent);
      pseudoComponent.shadow.destroy();
    });
  },

  /*
   Generate the shadow object.
   */
  generateShadowObject(component, contextType, attrs, data) {
    let shadow = new Renderable(component, get(component, 'shadowType'), contextType, attrs);
    shadow.setData(data);
    return shadow;
  },

  /*
   Update the attributes on the shadow object.
   */
  updateShadowObject(shadow, attrs) {
    shadow.setAttributes(attrs);
  }
});

e3Collection.reopenClass({
  positionalParams: ['_e3Context', 'iterable']
});

export default e3Collection;