API Docs 0.11.3.33c7ec07

import Ember from 'ember';
import FormGroup from 'ember-bootstrap/components/bs-form-group';
import Form from 'ember-bootstrap/components/bs-form';
import ComponentChild from 'ember-bootstrap/mixins/component-child';

const { computed, defineProperty, isArray, observer, on, run, warn } = Ember;

const nonTextFieldControlTypes = Ember.A([
  'checkbox',
  'select',
  'textarea'
]);

/**
 Sub class of `Components.FormGroup` that adds automatic form layout markup and form validation features.

 ### Form layout

 The appropriate Bootstrap markup for the given `formLayout` and `controlType` is automatically generated to easily
 create forms without coding the default Bootstrap form markup by hand:

 ```hbs
 {{#bs-form formLayout="horizontal" action="submit"}}
   {{bs-form-element controlType="email" label="Email" placeholder="Email" value=email}}
   {{bs-form-element controlType="password" label="Password" placeholder="Password" value=password}}
   {{bs-form-element controlType="checkbox" label="Remember me" value=rememberMe}}
   {{bs-button defaultText="Submit" type="primary" buttonType="submit"}}
 {{/bs-form}}
 ```

 ### Form validation

 In the following example the control elements of the three form elements value will be bound to the properties
 (given by `property`) of the form's `model`, which in this case is its controller (see `model=this`):

 ```hbs
 {{#bs-form formLayout="horizontal" model=this action="submit"}}
   {{bs-form-element controlType="email" label="Email" placeholder="Email" property="email"}}
   {{bs-form-element controlType="password" label="Password" placeholder="Password" property="password"}}
   {{bs-form-element controlType="checkbox" label="Remember me" property="rememberMe"}}
   {{bs-button defaultText="Submit" type="primary" buttonType="submit"}}
 {{/bs-form}}
 ```

 By using this indirection in comparison to directly binding the `value` property, you get the benefit of automatic
 form validation, given that your `model` has a supported means of validating itself.
 See [Components.Form](Components.Form.html) for details on how to enable form validation.

 In the example above the `model` was our controller itself, so the control elements were bound to the appropriate
 properties of our controller. A controller implementing validations on those properties could look like this:

 ```js
 import Ember from 'ember';
 import EmberValidations from 'ember-validations';

 export default Ember.Controller.extend(EmberValidations,{
   email: null,
   password: null,
   rememberMe: false,
   validations: {
     email: {
       presence: true,
       format: {
         with: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/
       }
     },
     password: {
       presence: true,
       length: { minimum: 6, maximum: 10}
     },
     comments: {
       length: { minimum: 5, maximum: 20}
     }
   }
 });
 ```

 If the `showValidation` property is `true` (which is automatically the case if a `focusOut` event is captured from the
 control element or the containing `Components.Form` was submitted with its `model` failing validation) and there are
 validation errors for the `model`'s `property`, the appropriate Bootstrap validation markup (see
 http://getbootstrap.com/css/#forms-control-validation) is applied:

 * `validation` is set to 'error', which will set the `has-error` CSS class
 * the `errorIcon` feedback icon is displayed if `controlType` is a text field
 * the validation messages are displayed as Bootstrap `help-block`s

 The same applies for warning messages, if the used validation library supports this. (Currently only
 [ember-cp-validations](https://github.com/offirgolan/ember-cp-validations))

 As soon as the validation is successful again...

 * `validation` is set to 'success', which will set the `has-success` CSS class
 * the `successIcon` feedback icon is displayed if `controlType` is a text field
 * the validation messages are removed

 ### Custom controls

 Apart from the standard built-in browser controls (see the `controlType` property), you can use any custom control simply
 by invoking the component with a block template. Use whatever control you might want, for example a select-2 component
 (from the [ember-select-2 addon](https://istefo.github.io/ember-select-2)):

 ```hbs
 {{#bs-form formLayout="horizontal" model=this action="submit"}}
   {{#bs-form-element label="Select-2" property="gender" useIcons=false as |value id validationState|}}
     {{select-2 id=id content=genderChoices optionLabelPath="label" value=value searchEnabled=false}}
   {{/bs-form-element}}
 {{/bs-form}}
 ```

 If your custom control does not render an input element, you should set `useIcons` to `false` since bootstrap only supports
 feedback icons with textual `<input class="form-control">` elements.

 @class FormElement
 @namespace Components
 @extends Components.FormGroup
 @public
 */
export default FormGroup.extend(ComponentChild, {
  classNameBindings: ['disabled:is-disabled', 'required:is-required', 'isValidating'],

  /**
   * Text to display within a `<label>` tag.
   *
   * You should include a label for every form input cause otherwise screen readers
   * will have trouble with your forms. Use `invisibleLabel` property if you want
   * to hide them.
   *
   * @property label
   * @type string
   * @public
   */
  label: null,

  /**
   * Controls label visibilty by adding 'sr-only' class.
   *
   * @property invisibleLabel
   * @type boolean
   * @public
   */
  invisibleLabel: false,

  /**
   * The type of the control widget.
   * Supported types:
   *
   * * 'text'
   * * 'checkbox'
   * * 'select' (deprecated)
   * * 'textarea'
   * * any other type will use an input tag with the `controlType` value as the type attribute (for e.g. HTML5 input
   * types like 'email'), and the same layout as the 'text' type
   *
   * @property controlType
   * @type string
   * @public
   */
  controlType: 'text',

  /**
   * The value of the control element is bound to this property. You can bind it to some controller property to
   * get/set the control element's value:
   *
   * ```hbs
   * {{bs-form-element controlType="email" label="Email" placeholder="Email" value=email}}
   * ```
   *
   * Note: you loose the ability to validate this form element by directly binding to its value. It is recommended
   * to use the `property` feature instead.
   *
   *
   * @property value
   * @public
   */
  value: null,

  /**
   The property name of the form element's `model` (by default the `model` of its parent `Components.Form`) that this
   form element should represent. The control element's value will automatically be bound to the model property's
   value.

   Using this property enables form validation on this element.

   @property property
   @type string
   @public
   */
  property: null,

  /**
   * Control element's HTML5 placeholder attribute
   *
   * @property placeholder
   * @type string
   * @public
   */
  placeholder: null,

  /**
   * Control element's HTML5 disabled attribute
   *
   * @property disabled
   * @type boolean
   * @public
   */
  disabled: false,

  /**
   * Control element's HTML5 readonly attribute
   *
   * @property readonly
   * @type boolean
   * @public
   */
  readonly: false,

  /**
   * Control element's HTML5 required attribute
   *
   * @property required
   * @type boolean
   * @public
   */
  required: false,

  /**
   * Control element's HTML5 autofocus attribute
   *
   * @property autofocus
   * @type boolean
   * @public
   */
  autofocus: false,

  /**
   * Control element's name attribute
   *
   * @property name
   * @type string
   * @public
   */
  name: null,

  /**
   * An array of objects containing the selection of choices for multiple choice style form controls, e.g. select
   * boxes.
   *
   * ```hbs
   * {{bs-form-element controlType="select" choices=countries choiceLabelProperty="name" choiceValueProperty="id" label="Country" value=selectedCountry}}
   * ```
   *
   * Be sure to also set the `choiceValueProperty` and `choiceLabelProperty` properties.
   *
   * @property choices
   * @type array
   * @public
   */
  choices: Ember.A(),

  /**
   * The property of the `choices` array of objects, containing the value of the choice, e.g. the select box option.
   *
   * @property choiceValueProperty
   * @type string
   * @public
   */
  choiceValueProperty: null,

  /**
   * The property of the `choices` array of objects, containing the label of the choice, e.g. the select box option.
   *
   * @property choiceLabelProperty
   * @type string
   * @public
   */
  choiceLabelProperty: null,

  /**
   * Textarea's rows attribute (ignored for other `controlType`s)
   *
   * @property rows
   * @type number
   * @default 5
   * @public
   */
  rows: 5,

  /**
   * Textarea's cols attribute (ignored for other `controlType`s)
   *
   * @property cols
   * @type number
   * @public
   */
  cols: null,

  /**
   * The model used for validation. Defaults to the parent `Components.Form`'s `model`
   *
   * @property model
   * @public
   */
  model: computed.reads('form.model'),

  /**
   * The array of error messages from the `model`'s validation.
   *
   * @property errors
   * @type array
   * @protected
   */
  errors: null,

  /**
   * @property hasErrors
   * @type boolean
   * @readonly
   * @protected
   */
  hasErrors: computed.gt('errors.length', 0),

  /**
   * The array of warning messages from the `model`'s validation.
   *
   * @property errors
   * @type array
   * @protected
   */
  warnings: null,

  /**
   * @property hasWarnings
   * @type boolean
   * @readonly
   * @protected
   */
  hasWarnings: computed.gt('warnings.length', 0),

  /**
   * The array of validation messages (either errors or warnings) rom the `model`'s validation.
   *
   * @property validationMessages
   * @type array
   * @protected
   */
  validationMessages: computed('hasErrors', 'hasWarnings', 'errors.[]', 'warnings.[]', function() {
    if (this.get('hasErrors')) {
      return this.get('errors');
    }
    if (this.get('hasWarnings')) {
      return this.get('warnings');
    }
    return null;
  }),

  /**
   * @property hasValidationMessages
   * @type boolean
   * @readonly
   * @protected
   */
  hasValidationMessages: computed.gt('validationMessages.length', 0),

  /**
   * @property hasValidator
   * @type boolean
   * @readonly
   * @protected
   */
  hasValidator: computed.notEmpty('model.validate'),

  /**
   * Set a validating state for async validations
   *
   * @property isValidating
   * @type boolean
   * @default false
   * @public
   */
  isValidating: false,

  /**
   * If `true` form validation markup is rendered (requires a validatable `model`).
   *
   * @property showValidation
   * @type boolean
   * @default false
   * @public
   */
  showValidation: false,

  /**
   * @property showValidationMessages
   * @type boolean
   * @readonly
   * @protected
   */
  showValidationMessages: computed.and('showValidation', 'hasValidationMessages'),

  /**
   * Event or list of events which enable form validation markup rendering.
   * Supported events: ['focusOut', 'change']
   *
   * @property showValidationOn
   * @type string|array
   * @default ['focusOut']
   * @public
   */
  showValidationOn: ['focusOut'],

  /**
   * @property _showValidationOn
   * @type array
   * @readonly
   * @private
   */
  _showValidationOn: computed('showValidationOn', function() {
    let showValidationOn = this.get('showValidationOn');

    if (isArray(showValidationOn)) {
      return showValidationOn;
    }

    if (typeof showValidationOn.toString === 'function') {
      return [showValidationOn];
    }

    warn('showValidationOn must be a String or an Array');
    return [];
  }),

  /**
   * @method showValidationOnHandler
   * @private
   */
  showValidationOnHandler(event) {
    if (this.get('_showValidationOn').indexOf(event) !== -1) {
      this.set('showValidation', true);
    }
  },

  /**
   * @property showErrors
   * @type boolean
   * @readonly
   * @deprecated
   * @protected
   */
  showErrors: computed.deprecatingAlias('showValidationMessages'),

  /**
   * The validation ("error" or "success") or null if no validation is to be shown. Automatically computed from the
   * models validation state.
   *
   * @property validation
   * @readonly
   * @type string
   * @protected
   */
  validation: computed('hasErrors', 'hasWarnings', 'hasValidator', 'showValidation', 'isValidating', 'disabled', function() {
    if (!this.get('showValidation') || !this.get('hasValidator') || this.get('isValidating') || this.get('disabled')) {
      return null;
    }
    return this.get('hasErrors') ? 'error' : (this.get('hasWarnings') ? 'warning' : 'success');
  }),

  /**
   * @property hasLabel
   * @type boolean
   * @readonly
   * @protected
   */
  hasLabel: computed.notEmpty('label'),

  /**
   * True for text field `controlType`s
   *
   * @property useIcons
   * @type boolean
   * @readonly
   * @public
   */
  useIcons: computed('controlType', function() {
    return !nonTextFieldControlTypes.includes(this.get('controlType'));
  }),

  /**
   * The form layout used for the markup generation (see http://getbootstrap.com/css/#forms):
   *
   * * 'horizontal'
   * * 'vertical'
   * * 'inline'
   *
   * Defaults to the parent `form`'s `formLayout` property.
   *
   * @property formLayout
   * @type string
   * @public
   */
  formLayout: computed.alias('form.formLayout'),

  /**
   * @property isVertical
   * @type boolean
   * @readonly
   * @protected
   */
  isVertical: computed.equal('formLayout', 'vertical'),

  /**
   * @property isHorizontal
   * @type boolean
   * @readonly
   * @protected
   */
  isHorizontal: computed.equal('formLayout', 'horizontal'),

  /**
   * @property isInline
   * @type boolean
   * @readonly
   * @protected
   */
  isInline: computed.equal('formLayout', 'inline'),

  /**
   * The Bootstrap grid class for form labels within a horizontal layout form. Defaults to the value of the same
   * property of the parent form. The corresponding grid class for form controls is automatically computed.
   *
   * @property horizontalLabelGridClass
   * @type string
   * @default 'col-md-4'
   * @public
   */
  horizontalLabelGridClass: computed.oneWay('form.horizontalLabelGridClass'),

  /**
   * Computed property that specifies the Bootstrap grid class for form controls within a horizontal layout form.
   *
   * @property horizontalInputGridClass
   * @type string
   * @readonly
   * @protected
   */
  horizontalInputGridClass: computed('horizontalLabelGridClass', function() {
    let parts = this.get('horizontalLabelGridClass').split('-');
    Ember.assert('horizontalInputGridClass must match format bootstrap grid column class', parts.length === 3);
    parts[2] = 12 - parts[2];
    return parts.join('-');
  }),

  /**
   * Computed property that specifies the Bootstrap offset grid class for form controls within a horizontal layout
   * form, that have no label.
   *
   * @property horizontalInputOffsetGridClass
   * @type string
   * @readonly
   * @protected
   */
  horizontalInputOffsetGridClass: computed('horizontalLabelGridClass', function() {
    let parts = this.get('horizontalLabelGridClass').split('-');
    parts.splice(2, 0, 'offset');
    return parts.join('-');
  }),

  /**
   * ID for input field and the corresponding label's "for" attribute
   *
   * @property formElementId
   * @type string
   * @private
   */
  formElementId: computed('elementId', function() {
    let elementId = this.get('elementId');
    return `${elementId}-field`;
  }),

  /**
   * Reference to the parent `Components.Form` class.
   *
   * @property form
   * @protected
   */
  form: computed(function() {
    return this.nearestOfType(Form);
  }),

  formElementTemplate: computed('formLayout', 'controlType', function() {
    let formLayout = this.getWithDefault('formLayout', 'vertical');
    let inputLayout;
    let controlType = this.get('controlType');

    switch (true) {
      case nonTextFieldControlTypes.includes(controlType):
        inputLayout = controlType;
        break;
      default:
        inputLayout = 'default';
    }

    return `components/form-element/${formLayout}/${inputLayout}`;
  }),

  /**
   * Setup validation properties. This method acts as a hook for external validation
   * libraries to overwrite. In case of failed validations the `errors` property should contain an array of error messages.
   *
   * @method setupValidations
   * @protected
   */
  setupValidations: Ember.K,

  /**
   * Listen for focusOut events from the control element to automatically set `showValidation` to true to enable
   * form validation markup rendering if `showValidationsOn` contains `focusOut`.
   *
   * @event focusOut
   * @private
   */
  focusOut() {
    this.showValidationOnHandler('focusOut');
  },

  /**
   * Listen for change events from the control element to automatically set `showValidation` to true to enable
   * form validation markup rendering if `showValidationsOn` contains `change`.
   *
   * @event change
   * @private
   */
  change() {
    this.showValidationOnHandler('change');
  },

  init() {
    this._super();
    if (!Ember.isBlank(this.get('property'))) {
      defineProperty(this, 'value', computed.alias(`model.${this.get('property')}`));
      this.setupValidations();
    }
  },

  /*
   * adjust feedback icon position
   *
   * Bootstrap documentation:
   *  Manual positioning of feedback icons is required for [...] input groups
   *  with an add-on on the right. [...] For input groups, adjust the right
   *  value to an appropriate pixel value depending on the width of your addon.
   */
  adjustFeedbackIcons: on('didInsertElement', observer('hasFeedback', 'formLayout', function() {
    run.scheduleOnce('afterRender', () => {
      // validation state icons are only shown if form element has feedback
      if (this.get('hasFeedback') && !this.get('isDestroying')) {
        // form group element has
        this.$()
          // an input-group
          .has('.input-group')
          // an addon or button on right si de
          .has('.input-group input + .input-group-addon, .input-group input + .input-group-btn')
          // an icon showing validation state
          .has('.form-control-feedback')
          .each((i, formGroups) => {
            // clear existing adjustment
            this.$('.form-control-feedback').css('right', '');
            let feedbackIcon = this.$('.form-control-feedback', formGroups);
            let defaultPositionString = feedbackIcon.css('right');
            Ember.assert(
              defaultPositionString.substr(-2) === 'px',
              '.form-control-feedback css right units other than px are not supported'
            );
            let defaultPosition = parseInt(
              defaultPositionString.substr(0, defaultPositionString.length - 2)
            );
            // Bootstrap documentation:
            //  We do not support multiple add-ons (.input-group-addon or .input-group-btn) on a single side.
            // therefore we could rely on having only one input-group-addon or input-group-btn
            let inputGroupWidth = this.$('input + .input-group-addon, input + .input-group-btn', formGroups).outerWidth();
            let adjustedPosition = defaultPosition + inputGroupWidth;

            feedbackIcon.css('right', adjustedPosition);
          });
      }
    });
  }))
});