Home Reference Source

src/components/tree/Node.js

import _ from 'lodash';

export default class Node {
  constructor(
    parent,
    {
      data = {},
      children = [],
    } = {},
    {
      checkNode,
      createComponents,
      isNew = true,
      removeComponents,
      parentPath = ''
    } = {},
  ) {
    this.parent = parent;
    this.previousData = {};
    this.persistentData = _.cloneDeep(data);
    this.new = isNew;
    this.createComponents = createComponents;
    this.checkNode = checkNode;
    this.removeComponents = removeComponents;
    this.revertAvailable = false;
    this.editing = false;
    this.collapsed = false;
    this.components = [];
    this.children = [];
    this.parentPath = parentPath;

    this.resetData();
    this.children = children.map((child, index) => new Node(this, child, {
      checkNode,
      createComponents,
      isNew: false,
      removeComponents,
      parentPath: this.getChildrenPath(index),
    }));
}

  get value() {
    return this.new
      ? null // Check the special case for empty root node.
      : {
        data: _.cloneDeep(this.persistentData),
        children: this.children.filter((child) => !child.new).map((child) => child.value),
      };
  }

  get isRoot() {
    return this.parent === null;
  }

  get changing() {
    return this.new || this.editing;
  }

  get hasChangingChildren() {
    return this.changin || this.children.some((child) => child.hasChangingChildren);
  }

  get hasData() {
    return !_.isEmpty(this.persistentData);
  }

  get hasChildren() {
    return Array.isArray(this.children) && this.children.length > 0;
  }

  getChildrenPath(index) {
    return this.parentPath ? `${this.parentPath}.children[${index}]` : '';
  }

  eachChild(iteratee) {
    iteratee(this);
    this.children.forEach((child) => child.eachChild(iteratee));
    return this;
  }

  getComponents() {
    return this.children.reduce(
      (components, child) => components.concat(child.getComponents()),
      this.components,
    );
  }

  validateNode() {
    let valid = true;
    this.getComponents().forEach(comp => {
      comp.setPristine(false);
      valid &= comp.checkValidity(null, false, this.persistentData);
    });
    return valid;
  }

  addChild() {
    if (this.new) {
      return null;
    }

    const child = new Node(this, {}, {
      checkNode: this.checkNode,
      createComponents: this.createComponents,
      isNew: true,
      removeComponents: this.removeComponents,
      parentPath: this.getChildrenPath(this.children.length),
    });
    this.children = this.children.concat(child);
    return child;
  }

  removeChild(childToRemove) {
    if (!this.new) {
      this.children = this.children.filter((child) => child !== childToRemove);
    }

    return this;
  }

  edit() {
    if (this.new) {
      return this;
    }

    this.editing = true;
    return this.resetData();
  }

  save() {
    const isValid = this.validateNode();
    if (this.changing && isValid) {
      if (this.new) {
        this.new = false;
      }
      else {
        this.editing = false;
        this.revertAvailable = true;
      }
      this.commitData();
    }

    return isValid;
  }

  cancel() {
    if (this.new) {
      this.remove();
    }
    else if (this.editing) {
      this.editing = false;
      this.resetData();
    }

    return this;
  }

  remove() {
    this.parent.removeChild(this);
    this.parent = null;
    this.clearComponents();
    return this;
  }

  revert() {
    if (!this.revertAvailable) {
      return this;
    }

    this.data = this.previousData;
    return this.commitData();
  }

  commitData() {
    this.previousData = this.persistentData;
    this.persistentData = _.cloneDeep(this.data);
    this.clearComponents();
    return this;
  }

  resetData() {
    this.data = _.cloneDeep(this.persistentData);
    this.updateComponentsContext();
    return this;
  }

  updateComponentsContext() {
    if (this.changing) {
      this.instantiateComponents();
    }
    else {
      this.clearComponents();
    }

    return this;
  }

  instantiateComponents() {
    this.components = this.createComponents(this.data, this);
    this.components.forEach((component) => {
      if (this.parentPath) {
        const path = this.calculateComponentPath(component);
        component.path = path;
      }
    });
    this.checkNode(this);
  }

  clearComponents() {
    this.removeComponents(this.components);
    this.components = [];
  }

  /**
   * Return a path of component's value.
   *
   * @param {Object} component - The component instance.
   * @return {string} - The component's value path.
   */
   calculateComponentPath(component) {
    let path = '';
    if (component.component.key) {
      path = `${this.parentPath}.data.${component.component.key}`;
    }
    return path;
  }
}