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 ...