Home Reference Source

src/components/address/Address.js

import autocompleter from 'autocompleter';
import _ from 'lodash';

import Formio from '../../Formio';
import { GoogleAddressProvider } from '../../providers/address/GoogleAddressProvider';

import Field from '../_classes/field/Field';
import NestedComponent from '../_classes/nested/NestedComponent';
import ContainerComponent from '../container/Container';

export const AddressComponentMode = {
  Autocomplete: 'autocomplete',
  Manual: 'manual',
};

const RemoveValueIconHiddenClass = 'address-autocomplete-remove-value-icon--hidden';
const ChildConditional = 'show = _.get(instance, \'parent.manualMode\', false);';

export default class AddressComponent extends ContainerComponent {
  static schema(...extend) {
    return ContainerComponent.schema({
      type: 'address',
      label: 'Address',
      key: 'address',
      switchToManualModeLabel: 'Can\'t find address? Switch to manual mode.',
      provider: '',
      providerOptions: {},
      manualModeViewString: '',
      hideLabel: false,
      disableClearIcon: false,
      enableManualMode: false,
      components: [
        {
          label: 'Address 1',
          tableView: false,
          key: 'address1',
          type: 'textfield',
          input: true,
          customConditional: ChildConditional,
        },
        {
          label: 'Address 2',
          tableView: false,
          key: 'address2',
          type: 'textfield',
          input: true,
          customConditional: ChildConditional,
        },
        {
          label: 'City',
          tableView: false,
          key: 'city',
          type: 'textfield',
          input: true,
          customConditional: ChildConditional,
        },
        {
          label: 'State',
          tableView: false,
          key: 'state',
          type: 'textfield',
          input: true,
          customConditional: ChildConditional,
        },
        {
          label: 'Country',
          tableView: false,
          key: 'country',
          type: 'textfield',
          input: true,
          customConditional: ChildConditional,
        },
        {
          label: 'Zip Code',
          tableView: false,
          key: 'zip',
          type: 'textfield',
          input: true,
          customConditional: ChildConditional,
        },
      ],
    }, ...extend);
  }

  static get builderInfo() {
    return {
      title: 'Address',
      group: 'advanced',
      icon: 'home',
      documentation: 'http://help.form.io/userguide/#address',
      weight: 35,
      schema: AddressComponent.schema(),
    };
  }

  mergeSchema(component = {}) {
    let { defaultSchema } = this;

    if (component.components) {
      defaultSchema = _.omit(defaultSchema, 'components');
    }

    return _.defaultsDeep(component , defaultSchema);
  }

  init() {
    this.components = this.components || [];
    if (this.builderMode || this.manualModeEnabled) {
      NestedComponent.prototype.addComponents.call(this, this.manualMode ? this.address : {});
    }
    Field.prototype.init.call(this);

    if (!this.builderMode) {
      if (this.component.provider) {
        const {
          provider,
          providerOptions,
        } = this.component;
        this.provider = this.initializeProvider(provider, providerOptions);
      }
      else if (this.component.map) {
        // Fallback to legacy version where Google Maps was the only provider.
        this.component.provider = GoogleAddressProvider.name;
        this.component.providerOptions = this.component.providerOptions || {};

        const {
          map,
          provider,
          providerOptions,
        } = this.component;

        const {
          key,
          region,
        } = map;

        if (key) {
          _.set(providerOptions, 'params.key', key);
        }
        if (region) {
          _.set(providerOptions, 'params.region', region);
        }

        this.provider = this.initializeProvider(provider, providerOptions);
      }
    }
  }

  initializeProvider(provider, options = {}) {
    const Provider = Formio.Providers.getProvider('address', provider);
    return new Provider(options);
  }

  get emptyValue() {
    return this.manualModeEnabled
      ? {
        mode: AddressComponentMode.Autocomplete,
        address: {},
      }
      : {};
  }

  get mode() {
    return this.manualModeEnabled
      ? this.dataValue
        ? this.dataValue.mode
        : this.dataValue
      : AddressComponentMode.Autocomplete;
  }

  set mode(value) {
    if (this.manualModeEnabled) {
      this.dataValue.mode = value;
    }
  }

  get autocompleteMode() {
    return this.mode === AddressComponentMode.Autocomplete;
  }

  get manualMode() {
    return this.mode === AddressComponentMode.Manual;
  }

  get manualModeEnabled() {
    return Boolean(this.component.enableManualMode);
  }

  restoreComponentsContext() {
    this.getComponents().forEach((component) => {
      component.data = this.address;
      component.setValue(component.dataValue, {
        noUpdateEvent: true,
      });
    });
  }

  get address() {
    return (this.manualModeEnabled && this.dataValue) ? this.dataValue.address : this.dataValue;
  }

  set address(value) {
    if (this.manualModeEnabled) {
      this.dataValue.address = value;
    }
    else {
      this.dataValue = value;
    }
  }

  get defaultSchema() {
    return AddressComponent.schema();
  }

  isValueInLegacyFormat(value) {
    return value && !value.mode;
  }

  normalizeValue(value) {
    return (this.manualModeEnabled && this.isValueInLegacyFormat(value))
      ? {
        mode: AddressComponentMode.Autocomplete,
        address: value,
      }
      : value;
  }

  setValue(value, flags = {}) {
    const changed = Field.prototype.setValue.call(this, value, flags);

    if (this.manualMode) {
      this.restoreComponentsContext();
    }

    if (changed) {
      this.redraw();
    }

    return changed;
  }

  static get modeSwitcherRef() {
    return 'modeSwitcher';
  }

  static get removeValueIconRef() {
    return 'removeValueIcon';
  }

  static get searchInputRef() {
    return 'searchInput';
  }

  get modeSwitcher() {
    return this.refs
      ? (this.refs[AddressComponent.modeSwitcherRef] || null)
      : null;
  }

  get removeValueIcon() {
    return this.refs
      ? (this.refs[AddressComponent.removeValueIconRef] || null)
      : null;
  }

  get searchInput() {
    return this.refs
      ? (this.refs[AddressComponent.searchInputRef] || null)
      : null;
  }

  get searchInputAttributes() {
    const attr = {
      name: this.options.name,
      type: 'text',
      class: 'form-control',
      lang: this.options.language,
      tabindex: this.component.tabindex || 0,
    };

    if (this.component.placeholder) {
      attr.placeholder = this.t(this.component.placeholder);
    }

    if (this.disabled) {
      attr.disabled = 'disabled';
    }

    _.defaults(attr, this.component.attributes);

    return attr;
  }

  get templateName() {
    return 'address';
  }

  render() {
    return super.render(this.renderTemplate(this.templateName, {
      children: (this.builderMode || this.manualModeEnabled) ? this.renderComponents() : '',
      nestedKey: this.nestedKey,
      inputAttributes: this.searchInputAttributes,
      ref: {
        modeSwitcher: AddressComponent.modeSwitcherRef,
        removeValueIcon: AddressComponent.removeValueIconRef,
        searchInput: AddressComponent.searchInputRef,
      },
      displayValue: this.getDisplayValue(),
      mode: {
        autocomplete: this.autocompleteMode,
        manual: this.manualMode,
      },
    }));
  }

  attach(element) {
    const result = ((this.builderMode || this.manualMode) ? super.attach : Field.prototype.attach).call(this, element);

    if (!this.builderMode) {
      if (!this.provider && this.component.provider) {
        const {
          provider,
          providerOptions,
        } = this.component;
        this.provider = this.initializeProvider(provider, providerOptions);
      }
    }

    this.loadRefs(element, {
      [AddressComponent.modeSwitcherRef]: 'single',
      [AddressComponent.removeValueIconRef]: 'single',
      [AddressComponent.searchInputRef]: 'single',
    });

    if (!this.builderMode && this.searchInput && this.provider) {
      autocompleter({
        input: this.searchInput,
        debounceWaitMs: 300,
        fetch: (text, update) => {
          const query = text;
          this.provider.search(query).then(update);
        },
        render: (address) => {
          const div = this.ce('div');
          div.textContent = this.getDisplayValue(address);
          return div;
        },
        onSelect: (address) => {
          this.address = address;
          this.triggerChange({
            modified: true,
          });

          if (this.searchInput) {
            this.searchInput.value = this.getDisplayValue();
          }
          this.updateRemoveIcon();
        },
      });

      this.addEventListener(this.searchInput, 'blur', () => {
        if (!this.searchInput) {
          return;
        }

        if (this.searchInput.value) {
          this.searchInput.value = this.getDisplayValue();
        }
      });

      this.addEventListener(this.searchInput, 'keyup', () => {
        if (!this.searchInput) {
          return;
        }

        if (!this.searchInput.value) {
          this.clearAddress();
        }
      });
    }

    if (this.modeSwitcher) {
      this.addEventListener(this.modeSwitcher, 'change', () => {
        if (!this.modeSwitcher) {
          return;
        }

        this.dataValue = this.emptyValue;
        this.mode = this.modeSwitcher.checked
          ? AddressComponentMode.Manual
          : AddressComponentMode.Autocomplete;

        if (!this.builderMode) {
          if (this.manualMode) {
            this.restoreComponentsContext();
          }

          this.triggerChange({
            modified: true,
          });
        }

        this.redraw();
      });
    }

    if (!this.builderMode && this.removeValueIcon) {
      this.updateRemoveIcon();

      const removeValueHandler = () => {
        this.clearAddress();
        this.focus();
      };

      this.addEventListener(this.removeValueIcon, 'click', removeValueHandler);
      this.addEventListener(this.removeValueIcon, 'keydown', ({ key }) => {
        if (key === 'Enter') {
          removeValueHandler();
        }
      });
    }

    return result;
  }

  addChildComponent(component) {
    component.customConditional = ChildConditional;
  }

  redraw() {
    const modeSwitcherInFocus = (this.modeSwitcher && (document.activeElement === this.modeSwitcher));

    return super.redraw()
      .then((result) => {
        if (modeSwitcherInFocus && this.modeSwitcher) {
          this.modeSwitcher.focus();
        }

        return result;
      });
  }

  clearAddress() {
    if (!this.isEmpty()) {
      this.triggerChange();
    }

    this.dataValue = this.emptyValue;
    if (this.searchInput) {
      this.searchInput.value = '';
    }
    this.updateRemoveIcon();
  }

  getDisplayValue(value = this.address) {
    return (this.provider && !this.manualMode)
      ? this.provider.getDisplayValue(value)
      : '';
  }

  validateMultiple() {
    // Address component can't be multivalue.
    return false;
  }

  updateRemoveIcon() {
    if (this.removeValueIcon) {
      if (this.isEmpty() || this.disabled) {
        this.addClass(this.removeValueIcon, RemoveValueIconHiddenClass);
      }
      else {
        this.removeClass(this.removeValueIcon, RemoveValueIconHiddenClass);
      }
    }
  }

  getValueAsString(value, options) {
    if (!value) {
      return '';
    }

    const normalizedValue = this.normalizeValue(value);

    const {
      address,
      mode,
    } = (
      this.manualModeEnabled
        ? normalizedValue
        : {
          address: normalizedValue,
          mode: AddressComponentMode.Autocomplete,
        }
    );
    const valueInManualMode = (mode === AddressComponentMode.Manual);

    if (this.provider && !valueInManualMode) {
      return this.getDisplayValue(address);
    }

    if (valueInManualMode) {
      if (this.component.manualModeViewString) {
        return this.interpolate(this.component.manualModeViewString, {
          address,
          data: this.data,
          component: this.component,
        });
      }

      return this.getComponents()
        .filter((component) => component.hasValue(address))
        .map((component) => [component, _.get(address, component.key)])
        .filter(([component, componentValue]) => !component.isEmpty(componentValue))
        .map(([component, componentValue]) => component.getValueAsString(componentValue, options))
        .join(', ');
    }

    return super.getValueAsString(address, options);
  }

  focus() {
    if (this.searchInput) {
      this.searchInput.focus();
    }
  }
}