import * as React from 'react' import { FormContext, FormFieldProps, FormFieldComponentProps, AbstractFormField, } from '../interfaces' import { Row, Column } from '.' import * as cx from 'classnames' import * as PropTypes from 'prop-types' import { createEmptyFormField } from '../data' const styles = require('../../src/styles/components/forms.scss') const borderStyles = require('../../src/styles/common/borders.scss') /** * A wrapper component for form fields that * reads the form name of parent
elements */ export abstract class FormFieldWrapper extends React.Component { public static contextTypes = { formName: PropTypes.string, } public context: FormContext public formName: string public componentWillMount() { // console.log('FORM FIELD WRAPPER WILL MOUNT', this) const name = this.context ? this.context.formName : (this.props as any).formName if (name) { this.formName = name } } } /** * 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> { /** * 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 * this will also trigger the validation * but will not mark the field as dirty (only onFocus does that) */ public componentDidMount() { // console.log('FORM FIELD DID MOUNT', this) this.dispatchUpdate(this.getNextFieldState(this.getValueFromState())) if (this.props.autofocus) { this.field.focus() } } public render() { // console.log('RENDER FORM FIELD', this) // the field may not be initialized yet if (!this.props.field) { return null } return ( {this.renderField()} {this.renderErrors()} ) } 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 [] } /** * This function instructs the class in how to create a blank field * It may be different for different types of form fields * Since a lot of fields are based on text input a default version * has been provided that uses the empty string as a default value */ public initializeField(): AbstractFormField { const { value, required } = this.props return createEmptyFormField( value || '', required === false ? false : true, false, ) } /** * Create an object representing the next state state of the field * * @param value */ public getNextFieldState(value: T) { const field = { ...this.props.field, value } // 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: React.SyntheticEvent) { 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: React.SyntheticEvent) { e.preventDefault() const field = { ...this.props.field, focused: false } if (this.props.onBlur) { this.props.onBlur( this.props.name, this.props.formName, field, ) } return this.dispatchUpdate(field) } public onFocus(e: React.SyntheticEvent) { e.preventDefault() const field = { ...this.props.field, 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) { 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 } = this.props // if both a value and an onChange handler are provided // then the client wishes to control the field manually // otherwise assume the version provided by cosmo-ui formReducer is in play return value && onChange ? 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 becoes 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 } public classNames(...args: string[]): string { const { valid, errors, focused, required } = this.props.field const res = cx( styles.field, ...args, this.props.className, { [styles.disabled]: this.props.disabled === true, [borderStyles.error]: !this.props.disabled && this.shouldShowErrors(), [borderStyles.success]: this.shouldShowValid() , [borderStyles.info]: !this.props.disabled && focused && !valid, }, ) // console.log('classnames', this.props, res) return res } }