Repository URL to install this package:
Version:
0.14.1 ▾
|
/* 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