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