Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
ui-component-library / src / forms / form / ObserverForm.tsx
Size: Mime:
/* eslint-disable brace-style */
// modules
import React from 'react'
import { isFunction, isArray, isSafe, Serializable, isTrue } from 'exotic'
import { observer } from 'xmobx/mobx-react'
import { action } from 'xmobx/mobx'
// local
import InputState from 'src/forms/input/InputState'
import Input from 'src/forms/input/ObserverInput'
import { isValid, errorMessage } from '../deps'
import {
  ObserverFormClassListType,
  InputStateOptions,
  InputStateFromArgs,
} from './typings'
import FormState from './FormState'
import { FormElement, SubmitButton, CancelButton } from './_elements'
import { wording } from './fixture'
import { isInputState, toInputStateAt, toClassList } from './deps'

/**
 * @todo https://www.w3.org/TR/wai-aria/states_and_properties#aria-errormessage
 *
 *
 * @note need to add refs like in my example, or pass in states
 * @todo - finish
 * @type {React.Component}
 * @type {Observer}
 * @type {CommonAbstractInterface}
 */

const handleCancel = event => {
  event.preventDefault()
}

// State = Props.state | any
@observer
class ObserverForm<Props = any, State = any> extends React.Component<Props, State> {
  // @todo should deprecate
  static FormState = FormState
  static Form = FormElement

  // @todo renderProps
  // @note - extend to easily change
  Form = FormElement
  Input = Input
  SubmitButton = SubmitButton
  CancelButton = CancelButton
  shouldResetFormOnUnmount = false

  /**
   * should have a simple object with wording & classnames, one property
   * @todo could also pass these in via defaultProps
   */
  defaultSubmitButtonLabel = wording.submit
  defaultCancelButtonLabel = wording.cancel
  // could handle submitButton & cancelButton
  wording: Object

  /**
   * state...
   */
  isSubmitButtonNeeded = true
  isCancelButtonNeeded = false
  handleCancel = handleCancel

  /**
   * @alias isAllInputsValid
   * @todo - should be on state
   */
  // state.hasAllValidInputs = true

  /**
   * @todo @important @deprecated @invalid @name
   */
  SubmitCustomClass: string
  CancelCustomClass: string
  ButtonGroupCustomClass: string
  // classList: ObserverFormClassListType

  /**
   * @param {Array} list
   * @return {Form}
   */
  static from(list: Array<InputStateOptions> | InputStateFromArgs) {
    let inputList = list
    if (isArray(list)) {
      inputList = {
        inputList: list,
      }
    }

    const state = new FormState(inputList)
    return function FormFrom(props) {
      return <ObserverForm state={state} {...props} />
    }
  }

  /**
   * @param {React.Props} props
   * @description @modifies this.state
   */
  constructor(props) {
    super(props)
    // @caution
    this.classList = this.classList || {}

    this.state = props.state

    if (!isSafe(this.state) || Object.keys(this.state).length <= 0) {
      console.warn('did not pass state prop - use defaultProps')
      this.state = new FormState(props)
    }
    if (isFunction(this.state.setProps)) {
      this.state.setProps(props)
    } else {
      console.warn('!!! FORM MISSING SETPROPS !!!')
    }
  }

  /**
   * onInvalid
   * onInput
   *
   * @todo - should put all validationType: toArray()
   */

  // @todo - on FormState
  // @action
  // eslint-disable-next-line
  validateInputItem = item => {
    console.log('validateInputItem', item)
    const { isHidden, validationType, value, errorMessageFor, type } = item

    // @todo - dedupe the other place & split out
    if ((!isSafe(isHidden) || !isHidden) && isSafe(validationType)) {
      if (validationType === 'month' || validationType === 'year') {
        if (this.state.validateExpiryDate(item) === false) {
          this.state.hasAllValidInputs = false
          item.errorMessage = errorMessage(errorMessageFor)
          item.isValidInput = false
        }
      } else if (
        validationType === 'creditCard' ||
        validationType === 'securityCode'
      ) {
        if (this.state.validateCreditCard(item) === false) {
          this.state.hasAllValidInputs = false
          item.errorMessage = errorMessage(errorMessageFor)
          item.isValidInput = false
        }
      } else if (validationType === 'confirmPassword') {
        if (this.state.validateConfirmPassword(item) === false) {
          this.state.hasAllValidInputs = false
        }
      } else if (type === 'groupElements') {
        item.props.elementList.forEach(this.validateInputItem)
      } else {
        const hasPassedValidation = item.isEnabled
          ? isValid(value, validationType)
          : true
        if (hasPassedValidation === false) {
          /**
           * @todo @standard validationMessage
           */
          item.errorMessage = errorMessage(errorMessageFor)
          item.isValidInput = false
          // item.setIsValid(false)

          this.state.hasAllValidInputs = false
          this.goToTop()
        } else {
          item.isValidInput = true
        }
      }
    }
  }

  /**
   * @action
   * @todo - debounce
   */

  @action.bound
  validateForm = () => {
    // @todo - should calling this.state.validate(), this.state.isValid()
    // Resetting the form validation state
    this.state.hasAllValidInputs = true
    this.state.inputsList.forEach(this.validateInputItem)
    return this.state.hasAllValidInputs
  }

  /**
   * @tutorial https://mobx.js.org/refguide/create-transformer.html
   * @description Lifecycle event hook
   * @listens onSubmit
   * @see handleSubmit
   */
  onSubmitValid(serialized: Serializable): void {
    //
  }
  /**
   * @description Lifecycle event hook
   * @listens onSubmit
   * @see handleSubmit
   */
  onSubmitError(serialized: Serializable): void {
    //
  }

  /**
   * @todo - using super when it's a bound fn may not work?
   * @listens onSubmit
   */
  // eslint-disable-next-line

  goToTop = () => {
    let topOffset = 0
    const element = document.getElementById('topHeader')
    if (element) {
      const headerOffset = element.offsetHeight / 2
      const target = this.state.form && this.state.form.offsetTop
      topOffset = target === 0 ? headerOffset : target - headerOffset
      document.body.scrollTop = topOffset
      document.documentElement.scrollTop = topOffset
    } else {
      console.log('Cannot scroll to top as the header is not present')
    }
  }

  handleSubmit = (event: Event) => {
    if (this.validateForm()) {
      const serialized = this.state.toSerialized()
      console.log('[form] valid & serialized: ', serialized)
      this.onSubmitValid(serialized)
      return serialized
    } else {
      console.log('[form] Form has invalid inputs!')
      const serialized = {
        hasError: true,
      }
      this.onSubmitError(serialized)
      return serialized
    }
  }

  onCancelClick = event => {
    event.preventDefault()
    if (this.props.onHandleCancel) {
      this.props.onHandleCancel(this.props, this.props.cancelState)
    } else {
      this.handleCancel(event)
    }
  }

  renderInput = (item: InputState, index: number) => {
    const instantiated = this._toInput(item, index)
    // log('renderingInput', {
    //   item,
    //   index,
    //   instantiated,
    //   self: this,
    // })
    /**
     * @todo - should be on the input state.......
     */
    // const handleClick = submitEvent => {
    //   submitEvent.persist()
    //   log(submitEvent)
    // }

    const attributes = {}
    if (this.classList.input) {
      attributes.className = this.classList.input
    }

    if (instantiated.isEnabled) {
      return (
        <this.Input
          key={instantiated.name || instantiated.identifier || index}
          state={instantiated}
          {...attributes}
          {...instantiated}
          // onClick={handleClick}
        />
      )
    } else {
      return ''
    }
  }

  renderInputList() {
    // console.log('total list', this.state.inputList)
    // log('renderingInputList', {
    //   list: this.state.list,
    //   self: this,
    // })
    //
    // wonder if there is something amis with rehydrating it
    return this.state.inputsList.map(this.renderInput)
  }

  /**
   * @see componentWillMount
   * @listens componentWillMount
   * @event prefil
   */
  onPrefil(inputState: InputState): void {
    //
  }
  onInputInit(input: InputState, index?: number): void {
    //
  }

  /**
   * @private
   * @inheritdoc
   * @description compat
   *              so it returns the inputState
   *              will deprecate when not using .map
   */
  _onPrefil = inputState => this.onPrefil(inputState) || inputState

  _onInputInit = (input, index) => {
    const inputState = this._toInput(input, index)
    this.onInputInit(inputState)
  }
  _toInput = (input, index) => {
    return isInputState(input) ? input : toInputStateAt(this.state, index)
  }

  /**
   * @todo put prefil as action on state
   * @todo do a timeout loader
   *
   * !!!!!! changed from componentWillMount => componentDidMount
   *
   * @name onPrefil
   * @name prefilItem
   * @name prefilInputItem
   * @name prefilInput
   */
  componentDidMount() {
    console.debug('[forms] componentDidMount')

    // don't want to assign to state
    let inputsList = this.state.inputsList
    // this causes many issues?
    inputsList = inputsList.map(this._toInput)
    // call the action
    this.state.setInputsList(inputsList)

    if (isFunction(this.onPrefil)) {
      inputsList = inputsList.map(this._onPrefil)
      this.state.setInputsList(inputsList)
    }
    if (isFunction(this._onInputInit)) {
      inputsList.forEach(this._onInputInit)
    }
  }
  componentWillReceiveProps(prevProps, nextProps) {
    this.classList = toClassList(this)
  }
  componentWillUnmount() {
    console.debug('[forms] componentWillUnmount')

    if (isTrue(this.shouldResetFormOnUnmount)) {
      this.state.inputsList.map(this.resetFormState)
    }
    // remove subscribers, make  backup
    // this.state.__INPUTS_LIST = JSON.stringify(this.inputsList, null, 2)
    // this.state.inputsList = []
  }

  resetFormState(inputState) {
    // To reset textbox
    if (inputState.type !== 'button') {
      inputState.setValue('')
      inputState.isValidInput = true
    }

    // To reset groupElements
    if (inputState.elementList.length > 0) {
      inputState.elementList.forEach(inputElement => {
        if (inputElement.type !== 'button') {
          inputElement.setValue('')
          inputElement.isValidInput = true
        }
      })
    }

    // To reset the 'show password' state
    if (inputState.name === 'password') {
      inputState.type = 'password'
    }
  }

  render() {
    // log('ObserverForm', this)
    // console.log('testing123')

    // @todo if it is not here, or in another lifecycle method, it won't show when state updates
    // @example promocodes
    // but when it is here, dangerous for updates
    // this.classList = toClassList(this)
    const classList = toClassList(this)

    const {
      isSubmitButtonNeeded,
      isCancelButtonNeeded,
      onSubmit,
      formId,
    } = this

    // @todo - this should be computed just once, at mount...
    const listView = this.renderInputList()

    // has to be dom element to render ref to element
    return (
      <this.Form
        // data-is-valid={this.state.hasAllValidInputs}
        // isValid={this.state.hasAllValidInputs}
        aria-invalid={this.state.hasAllValidInputs === false}
        setRef={this.state.setFormReference}
        className={classList.form}
        handleSubmit={onSubmit}
        formId={formId}
      >
        {listView}

        <div className={classList.buttonGroup}>
          {isSubmitButtonNeeded && (
            <this.SubmitButton
              type={'submit'}
              className={classList.submitButton}
              onClick={this.handleSubmit}
              onSubmit={this.handleSubmit}
              data-qa={this.submitDataQa}
            >
              {this.defaultSubmitButtonLabel}
            </this.SubmitButton>
          )}
          {isCancelButtonNeeded && (
            <this.CancelButton
              className={classList.cancelButton}
              onClick={this.onCancelClick}
              data-qa={this.cancelDataQa}
              // isLowPriority
              // onReset={this.onCancelClick}
            >
              {this.defaultCancelButtonLabel}
            </this.CancelButton>
          )}
        </div>
      </this.Form>
    )
  }
}

export { ObserverForm as ObservableForm }
export { ObserverForm as Form }
export { ObserverForm }
export default ObserverForm