codevise/pageflow

View on GitHub
package/src/ui/views/mixins/inputView.js

Summary

Maintainability
A
0 mins
Test Coverage
import _ from 'underscore';

import {attributeTranslationKeys, findTranslation, translationKeysWithSuffix} from '../../utils/i18nUtils';
import {attributeBinding} from './attributeBinding';

/**
 * Mixin for input views handling common concerns like labels,
 * inline help, visiblity and disabling.
 *
 * ## Label and Inline Help Translations
 *
 * By default `#labelText` and `#inlineHelpText` are defined through
 * translations. If no `attributeTranslationKeyPrefixes` are given,
 * translation keys for labels and inline help are constructed from
 * the `i18nKey` of the model and the given `propertyName`
 * option. Suppose the model's `i18nKey` is "page" and the
 * `propertyName` option is "title". Then the key
 *
 *     activerecord.attributes.page.title
 *
 * will be used for the label. And the key
 *
 *     pageflow.ui.inline_help.page.title_html
 *     pageflow.ui.inline_help.page.title
 *
 * will be used for the inline help.
 *
 * ### Attribute Translation Key Prefixes
 *
 * The `attributeTranslationKeyPrefixes` option can be used to supply
 * an array of scopes in which label and inline help translations
 * shall be looked up based on the `propertyName` option.
 *
 * Suppose the array `['some.attributes', 'fallback.attributes']` is
 * given as `attributeTranslationKeyPrefixes` option. Then, in the
 * example above, the first existing translation key is used as label:
 *
 *     some.attributes.title.label
 *     fallback.attributes.title.label
 *     activerecord.attributes.post.title
 *
 * Accordingly, for the inline help:
 *
 *     some.attributes.title.inline_help_html
 *     some.attributes.title.inline_help
 *     fallback.attributes.title.inline_help_html
 *     fallback.attributes.title.inline_help
 *     pageflow.ui.inline_help.post.title_html
 *     pageflow.ui.inline_help.post.title
 *
 * This setup allows to keep all translation keys for an attribute
 * to share a common prefix:
 *
 *     some:
 *       attributes:
 *         title:
 *           label: "Label"
 *           inline_help: "..."
 *           inline_help_disabled: "..."
 *
 * ### Inline Help for Disabled Inputs
 *
 * For each inline help translation key, a separate key with an
 * `"_disabled"` suffix can be supplied, which provides a help string
 * that shall be displayed when the input is disabled. More specific
 * attribute translation key prefixes take precedence over suffixed
 * keys:
 *
 *     some.attributes.title.inline_help_html
 *     some.attributes.title.inline_help
 *     some.attributes.title.inline_help_disabled_html
 *     some.attributes.title.inline_help_disabled
 *     fallback.attributes.title.inline_help_html
 *     fallback.attributes.title.inline_help
 *     fallback.attributes.title.inline_help_disabled_html
 *     fallback.attributes.title.inline_help_disabled
 *     pageflow.ui.inline_help.post.title_html
 *     pageflow.ui.inline_help.post.title
 *     pageflow.ui.inline_help.post.title_disabled_html
 *     pageflow.ui.inline_help.post.title_disabled
 *
 * @param {string} options
 *   Common constructor options for all views that include this mixin.
 *
 * @param {string} options.propertyName
 *   Name of the attribute on the model to display and edit.
 *
 * @param {string} [options.label]
 *   Label text for the input.
 *
 * @param {string[]} [options.attributeTranslationKeyPrefixes]
 *   An array of prefixes to lookup translations for labels and
 *   inline help texts based on attribute names.
 *
 * @param {string} [options.additionalInlineHelpText]
 *   A text that will be appended to the translation based inline
 *   text.
 *
 * @param {string|string[]} [options.disabledBinding]
 *   Name of an attribute to control whether the input is disabled. If
 *   the `disabled` and `disabledBinding` options are not set,
 *   input will be disabled whenever this attribute has a truthy value.
 *   When multiple attribute names are passed, the function passed to
 *   the `disabled` option will receive an array of values in the same
 *   order.
 *
 * @param {function|boolean} [options.disabled]
 *   Render input as disabled. A Function taking the value of the
 *  `disabledBinding` attribute as parameter. Input will be disabled
 *  only if function returns `true`.
 *
 * @param {any} [options.disabledBindingValue]
 *   Input will be disabled whenever the value of the `disabledBinding`
 *   attribute equals the value of this option.
 *
 * @param {string|string[]} [options.visibleBinding]
 *   Name of an attribute to control whether the input is visible. If
 *   the `visible` and `visibleBindingValue` options are not set,
 *   input will be visible whenever this attribute has a truthy value.
 *   When multiple attribute names are passed, the function passed to
 *   the `visible` option will receive an array of values in the same
 *   order.
 *
 * @param {function|boolean} [options.visible]
 *   A Function taking the value of the `visibleBinding` attribute as
 *   parameter. Input will be visible only if function returns `true`.
 *
 * @param {any} [options.visibleBindingValue]
 *   Input will be visible whenever the value of the `visibleBinding`
 *   attribute equals the value of this option.
 *
 * @mixin
 */
export const inputView = {
  ...attributeBinding,

  ui: {
    label: 'label',
    labelText: 'label .name',
    inlineHelp: 'label .inline_help'
  },

  /**
   * Returns an array of translation keys based on the
   * `attributeTranslationKeyPrefixes` option and the given keyName.
   *
   * Combined with {@link #i18nutils
   * i18nUtils.findTranslation}, this can be used inside input views
   * to obtain additional translations with the same logic as for
   * labels and inline help texts.
   *
   * findTranslation(this.attributeTranslationKeys('default_value'));
   *
   * @param {string} keyName
   * Suffix to append to prefixes.
   *
   * @param {string} [options.fallbackPrefix]
   *   Optional additional prefix to form a model based translation
   *   key of the form `prefix.modelI18nKey.propertyName.keyName
   *
   * @return {string[]}
   * @since 0.9
   * @member
   */
  attributeTranslationKeys: function(keyName, options) {
    return attributeTranslationKeys(
      this.options.propertyName,
      keyName,
      _.extend({
        prefixes: this.options.attributeTranslationKeyPrefixes,
        fallbackModelI18nKey: this.model.i18nKey
      }, options || {})
    );
  },

  onRender: function() {
    this.$el.addClass('input');
    this.$el.addClass(this.model.modelName + '_' + this.options.propertyName);

    this.$el.data('inputPropertyName', this.options.propertyName);
    this.$el.data('labelText', this.labelText());
    this.$el.data('inlineHelpText', this.inlineHelpText());

    this.ui.labelText.text(this.labelText());

    this.updateInlineHelp();
    this.setLabelFor();

    this.setupBooleanAttributeBinding('disabled', this.updateDisabled);
    this.setupBooleanAttributeBinding('visible', this.updateVisible);
  },

  /**
   * The label to display in the form.
   * @return {string}
   */
  labelText: function() {
    return this.options.label || this.localizedAttributeName();
  },

  localizedAttributeName: function() {
    return findTranslation(this.attributeTranslationKeys('label', {
      fallbackPrefix: 'activerecord.attributes'
    }));
  },

  updateInlineHelp: function() {
    this.ui.inlineHelp.html(this.inlineHelpText());

    if (!this.inlineHelpText()) {
      this.ui.inlineHelp.hide();
    }
  },

  /**
   * The inline help text for the form field.
   * @return {string}
   */
  inlineHelpText: function() {
    var keys = this.attributeTranslationKeys('inline_help', {
      fallbackPrefix: 'pageflow.ui.inline_help'
    });

    if (this.isDisabled()) {
      keys = translationKeysWithSuffix(keys, 'disabled');
    }

    return _.compact([
      findTranslation(keys, {defaultValue: '', html: true}),
      this.options.additionalInlineHelpText
    ]).join(' ');
  },

  setLabelFor() {
    if (this.ui.input &&
        this.ui.label.length === 1 &&
        !this.ui.input.attr('id')) {
      const id = 'input_' + this.model.modelName + '_' + this.options.propertyName

      this.ui.input.attr('id', id);
      this.ui.label.attr('for', id);
    }
  },

  isDisabled: function() {
    return this.getBooleanAttributBoundOption('disabled');
  },

  updateDisabled: function() {
    this.$el.toggleClass('input-disabled', !!this.isDisabled());
    this.updateInlineHelp();

    if (this.ui.input) {
      this.updateDisabledAttribute(this.ui.input);
    }
  },

  updateDisabledAttribute: function(element) {
    if (this.isDisabled()) {
      element.attr('disabled', true);
    }
    else {
      element.removeAttr('disabled');
    }
  },

  updateVisible: function() {
    this.$el.toggleClass('hidden_via_binding',
                         this.getBooleanAttributBoundOption('visible') === false);
  }
};