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