Home Reference Source

src/types/data/map.js

import {List, Map} from 'immutable';

import {parseRef} from '~/refs';

import DataType, {ImmutableDataType} from './';
import ValidationError from './validationError';

/**
 * The data type for maps of data.
 *
 * Allowed options:
 *
 * |Name|Type|Attribute|Description|
 * |----|----|---------|-----------|
 * |data|({@link Object} | {@link DataType})[] | | A list of children data types. Its key in the map is the name of the child data type. If a data types is provided without a path, the path is assumed to be the name of the data type. |
 * |data.path| {@link string}[] | | Where to access the child data type. @see https://facebook.github.io/immutable-js/docs/#/Map/getIn |
 * |data.field| {@link DataType} | | The child data type. |
 *
 * @extends {ImmutableDataType}
 */
export default class ImmutableMapType extends ImmutableDataType {
  static typeName = 'map';

  static parseOptions(field, parseField) {
    return super.parseOptions(field, parseField)
      .update('data', data => data
        .map(fieldData => fieldData.get('type') ?
          Map({
            path: List([fieldData.get('name')]),
            field: parseField(fieldData)
          }) :
          fieldData
            .update('field', parseField)
        )
      );
  }

  initialize(value, renderOptions) {
    value = this.getValue(value);
    return Promise.all(this.getData()
      .map(fieldData => {
        const field = fieldData.get('field');
        const path = fieldData.get('path');
        return field.initialize
          ? field.initialize(value.getIn(path), renderOptions)
          : Promise.resolve();
      })
    );
  }

  getDefaultValue() {
    return super.getDefaultValue(Map());
  }

  getData() {
    return this.options.get('data')
      .map(fieldData => fieldData instanceof DataType ?
        Map({
          path: List([fieldData.getName()]),
          field: fieldData
        }) :
        fieldData
      );
  }

  getDataNames() {
    return this.getData()
      .map(fieldData => fieldData.get('field').getName());
  }

  hasValue(model, checkDefault) {
    if (!super.hasValue(model, checkDefault)) {
      return false;
    }
    return this.getData()
      .map(fieldData => fieldData.get('field')
        .hasValue(model.getIn(fieldData.get('path'), checkDefault))
      )
      .reduce((modelHasValue, fieldHasValue) => modelHasValue || fieldHasValue);
  }

  getDisplay(model, renderOptions) {
    model = this.getValue(model);
    return this.getData()
      .map(fieldData => ({
        key: fieldData.get('field').getName(),
        value: model.getIn(fieldData.get('path'))
      }))
      .map(({key, value}) => `${key}: ${value}`)
      .join(', ');
  }

  getFieldData(ref) {
    return this.getData()
      .find(fieldData => fieldData.get('field').getName() == ref.ref);
  }

  getValue(model, ref, renderOptions) {
    model = model || this.getDefaultValue();
    if (ref) {
      return this.getFieldAndValue(model, ref, renderOptions).value;
    } else {
      return model
        .update(model => this.getData()
          .reduce(
            (model, fieldData) => {
              const field = fieldData.get('field');
              const path = fieldData.get('path');
              const value = field.getValue(model.getIn(path));
              return model.setIn(path, value);
            },
            model
          )
        );
    }
  }

  getField(refs, renderOptions) {
    if (!List.isList(refs)) {
      refs = List([refs]);
    }

    if (refs.size == 0) {
      return this;
    }

    const firstRef = refs.first();
    if (firstRef.ref === '') {
      return this.getNextField(this, refs.rest(), renderOptions);
    }

    const fieldData = this.getFieldData(firstRef);
    if (!fieldData) {
      throw new Error(`Cannot find field for ref "${firstRef}" on "${this.getName()}": "${this.getDataNames()}"`);
    }

    const field = fieldData.get('field');

    return this.getNextField(field, refs.rest(), renderOptions);
  }

  getFieldAndValue(model, refs, renderOptions) {
    if (!List.isList(refs)) {
      refs = List([refs]);
    }

    if (refs.size == 0) {
      return {};
    }

    if (!model) {
      return {field: this.getField(refs, renderOptions)};
    }

    const firstRef = refs.first();
    if (firstRef.ref === '') {
      return this.getNextFieldAndValue(this, model, refs.rest(), renderOptions);
    }

    const fieldData = this.getFieldData(firstRef);
    if (!fieldData) {
      throw new Error(`Cannot find field for ref "${firstRef}" on "${this.getName()}": "${this.getDataNames()}"`);
    }
    
    const path = fieldData.get('path');
    const field = fieldData.get('field');
    const value = field.getValue(model.getIn(path));

    return this.getNextFieldAndValue(field, value, refs.rest(), renderOptions);
  }

  setValue(model, refs, newValue, renderOptions) {
    if (!model) {
      throw new Error('Invalid arguments to setDataValue: model = null');
    }

    if (!List.isList(refs)) {
      refs = List([refs]);
    }

    if (refs.size == 0) {
      throw new Error(`Invalid arguments to setDataValue: refs = ${refs}`);
    }

    const firstRef = refs.first();
    if (firstRef.ref === '') {
      return this.setNextValue(this, model, newValue, refs.rest(), renderOptions);
    }

    const fieldData = this.getFieldData(firstRef)
    if (!fieldData) {
      throw new Error(`Cannot find field for ref "${firstRef}" on "${this.getName()}": "${this.getDataNames()}"`);
    }

    const path = fieldData.get('path');
    const field = fieldData.get('field');
    const oldValue = model.getIn(path);

    return model
      .setIn(path, this
        .setNextValue(field, oldValue, newValue, refs.rest(), renderOptions)
      );
  }

  validate(model) {
    return this.getData()
      .map(fieldData => {
        const field = fieldData.get('field');
        const ref = parseRef(field.getName());
        return this.validateSingle(model, ref);
      })
      .flatten(false)
      .filter(error => error)
      .map(error => {
        error.addRef(parseRef(error.field.getName()));
        error.field = this;
        return error;
      });
  }

  // TODO: Probably move this out of here and into some form validation / utils module.
  validateSingle(model, ref) {
    const {field, value} = this.getFieldAndValue(model, ref, {});

    return List([
      field.getValidationLinks()
        .map(linkRef => this.validateSingle(model, parseRef(linkRef)))
        .flatten(true)
        .filter(error => error),

      List([
        field.validate(value),
        field.getValidator()(value, model, field)
      ])
        .filter(error => error)
    ]).flatten(true);
  }

  exclude(model, deep=true) {
    model = super.exclude(model);
    return !model
      ? model
      : this.getData()
        .reduce((model, fieldData) => {
          const field = fieldData.get('field');
          const path = fieldData.get('path');
          return field.isExcluded()
            ? model.deleteIn(path)
            : model.updateIn(path, value => field.exclude(value))
        }, model);
  }
}