Learn more  » Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Bower components Debian packages RPM packages NuGet packages

skava / @skava/ui   js

Repository URL to install this package:

Version: 2.8.8 

/ src / forms / form / ObserverForm.tsx

import React from 'react'
import { isFunction, isArray, isSafe, isTrue } from 'exotic'
import { Serializable } from '@skava/typings'
import { observer } from 'xmobx/mobx-react'
import { action } from 'xmobx/mobx'
import { isValidSelect } from 'src/forms/deps'
import InputState from 'src/forms/input/InputState'
import Input from 'src/forms/input/ObserverInput'
import { isValid, errorMessage } from '../deps'
import { InputStateType } from '../input/typings'
import {
  ObserverFormClassListType,
  InputStateOptions,
  InputStateFromArgs,
  ObserverFormProps,
} 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
 */

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

@observer
class ObserverForm<
  Props extends ObserverFormProps = ObserverFormProps,
  State extends FormState = FormState
> extends React.Component<Props, State> {
  /**
   * @todo !!! PUT THESE AS GETTERS AND SETTERS TO DEPRECATE
   */

  /**
   * @todo renderProps
   */
  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

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

  static from(list: Array<InputStateOptions> | InputStateFromArgs) {
    let inputList = list
    if (isArray(list)) {
      inputList = {
        inputList: list,
      }
    }

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

  constructor(props: 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 !!!')
    }
  }

  validateInputItem = item => {
    console.log('validateInputItem', item)
    const {
      isHidden,
      isDisabled,
      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 (validationType === 'newPassword') {
        if (this.state.validateNewPassword(item) === false) {
          this.state.hasAllValidInputs = false
        }
      } else if (validationType === 'selectOption') {
        const { label } = item
        if (isValidSelect({ value, label })) {
          item.isValidInput = true
        } else {
          item.errorMessage = errorMessage(errorMessageFor)
          item.isValidInput = 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
        }
      }
    }
  }

  /**
   * @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
   */
  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
    }
  }

  /**
   * @todo @name handleCancelClick
   * @deprecated should remove, not part of form at all
   */
  onCancelClick = (event: 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)

    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: InputState) =>
    this.onPrefil(inputState) || inputState

  _onInputInit = (input: InputState, index: number) => {
    const inputState = this._toInput(input, index)
    this.onInputInit(inputState)
  }
  _toInput = (input: InputState, index: number) => {
    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)
Loading ...