import * as React from 'react' import { StylableComponent, StyleContext } from '../theme' import { FormContext } from '../form' import { AbstractFormField, FormFieldComponentProps } from './interfaces' import { string, object } from 'prop-types' import { createDefaultFormField, createAsyncFormField } from './data' /** * A wrapper component for form fields that * reads the form name of parent
elements */ export abstract class FormFieldWrapper extends StylableComponent { public static contextTypes = { formName: string, themeNotifier: object, } public context: any public formName: string /** * Set the form name */ public componentWillMount() { // console.log('FORM FIELD WRAPPER WILL MOUNT', this) // if the form name is passed in as a prop that takes precedence this.formName = (this.props as any).formName || (this.context ? this.context.formName : null) } } /** * The base form field component is responsible for: * * - initializing field data using a set of standard fields (AbstractFormField) * - dispatching events and updating the store * - running the validators prior to updating the store * - rendering error messages underneath the field * - styling success and errors in red/green * * All of these are intedend to be handled in a standard way and thus * have been delegated to this class. Every class that extends this one * must implement the renderField method to render a specific input type */ export abstract class BaseFormField extends React.Component, any> { public static defaultProps = { showErrorMessages: true, required: true, disabled: false, autofocus: false, fullWidth: false, } /** * A reference to the form element itself */ public field: any constructor(props: FormFieldComponentProps) { super(props) this.validate = this.props.validate ? this.props.validate.bind(this) : this.validate.bind(this) this.onChange = this.onChange.bind(this) this.onBlur = this.onBlur.bind(this) this.onFocus = this.onFocus.bind(this) } /** * Use React refs to set the field elem */ public setFieldRef = (e: any) => this.field = e /** * Initialize the field in redux store */ /** * Initialize the field * this will also trigger the validation * but will not mark the field as dirty (only onFocus does that) */ public init() { this.dispatchUpdate(this.getNextFieldState(this.getValueFromState())) } public abstract renderField(): JSX.Element public renderErrors() { if (this.shouldShowErrors()) { const { errors } = this.props.field return {errors[0]} } } /** * Default validation function * Checks to see if the field is required * And if a value is present * * @param value */ public validate(value: T): string[] | boolean { if (this.props.field.required && !value) { return ['* required'] } return [] } /** * Create an object representing the next state state of the field * * @param value */ public getNextFieldState(value: T) { const field = { ...this.props.field, value } if (!field) { return this.props.asyncValidate ? createAsyncFormField(this.props) : createDefaultFormField(this.props) } // do not mess with the validation if an async validation function is present if (!this.props.asyncValidate) { // people might think that the validation function is supposed // to return a boolean rather than an array of string errors // rather than leave this as a potential bug better to support it here const res = this.validate(value) field.errors = res === true ? [] : (res === false ? ['* error'] : res) field.valid = field.errors && field.errors.length ? false : true } return field } public onChange(e: any) { e.preventDefault() const { name, formName, onChange, asyncOnChange } = this.props // console.log('handling change', this.props) const field = this.getNextFieldState(this.getValueFromEvent(e)) if (onChange) { onChange(name, formName, field) } if (asyncOnChange) { asyncOnChange(name, formName, field) } return this.dispatchUpdate(field) } public onBlur(e: any) { e.preventDefault() const field = { ...this.getNextFieldState(this.props.field.value), focused: false, } if (this.props.onBlur) { this.props.onBlur( this.props.name, this.props.formName, field, ) } return this.dispatchUpdate(field) } public onFocus(e: any) { e.preventDefault() const field = { ...this.getNextFieldState(this.props.field.value), focused: true, dirty: true, } if (this.props.onFocus) { this.props.onFocus( this.props.name, this.props.formName, field, ) } return this.dispatchUpdate(field) } public dispatchUpdate(field: AbstractFormField) { this.props.setFormField( this.props.name, this.props.formName, field, ) } public getValueFromEvent(e: React.SyntheticEvent) { // console.log('GET VALUE FROM EVENT', e.currentTarget.valu) return e.currentTarget.value } /** * Decide which value to use - props.value or props.field.value * This distinction is there because some inputs are 'semi-controlled' * This happens when setting and updating the value is handled by the client * but fields such as dirty, valid, focused etc are handled by cosmo ui * * NOTE: this function should not be typed because in the case of some inputs * such as the number input, it is saved as a number in the state but it is * inputted as a string */ public getValueFromState(): any { const { value, onChange, field, disabled } = this.props // if both a value and an onChange handler are provided // then it's clear the client wishes to control the field manually // if a value is provided as a prop and the field is disabled then we can infer // that the client just wants to read the value without changing it // so we should use the provided value // likewise if a value is provided and the field is pristine then we can infer // that we should use the provided value const pristine = !field || !field.dirty // if the field is dirty, however, then that means the user has made changes // in this case if then we must use the latest version of the field // since there is no onChange handler the latest version is now in the formReducer // if no value is provided at all then of course we just use the formReducer's value return value && (onChange || disabled || pristine) ? value : field.value } public shouldShowErrors() { const { valid, dirty, submitted, focused } = this.props.field // to show errors then the field must be invalid and either submitted or // dirty and not focused (if it's focused the user is changing stuff) return !valid && (submitted || (dirty && !focused)) } public shouldShowValid() { const { valid, dirty, submitted, focused, required, errors } = this.props.field // obviously it becomes visible if its dirty or submitted // but if the field isn't required then we won't show any colour // at all whilst it's still pristine (hence the required part here) const visible = (required || dirty || submitted) return !this.props.disabled && visible && valid && !errors.length } }