Last active
August 29, 2015 14:23
-
-
Save dpoindexter/be7f062f09cf40f3daea to your computer and use it in GitHub Desktop.
React validation concept
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
- Use ComponentDidMount and context to attach to container | |
- On every render, all attached components evaluate their own validation state, and call setState on the corresponding Form property | |
<Form> | |
<ArbitraryComponent/> | |
<Input> | |
</Form> | |
Const ArbitraryComponent = component(() => <div><Input /></div>); | |
class Input extends ReactComponent { | |
componentDidMount () { | |
const meRef = Symbol(); | |
this.context.validationState[meRef] = { valid: true, messages: [] }; | |
} | |
componentWillReceiveProps (nextProps) { | |
const validationResult = this.validate(nextProps); | |
if (this.context.validationState[meRef] !== validationResult) { | |
this.context.update(validationResult, meRef); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const example = ( | |
<Form data={data}> | |
<Field value={data.get('firstName')} /> | |
</Form> | |
); | |
const Form = component({ | |
childContextTypes: { | |
registerValidation: React.PropTypes.func, | |
}, | |
getChildContext () { | |
return { | |
registerValidation: this.registerValidation | |
} | |
}, | |
componentWillMount () { | |
this.validationState = Immstruct({}); | |
this.validationStateRef = this.validationState.reference(); | |
this.validationStateRef.observe(() => { | |
this.setState(); | |
}); | |
}, | |
registerValidation (forProp) { | |
this.validationState.cursor().set(forProp, Immutable.toJS({ | |
isValid: true, | |
messages: [] | |
})); | |
return this.validationState.cursor(); | |
} | |
}, function ({ children }) { | |
const messages = this.validationState | |
.filter((k, v) => !v.isValid()) | |
.map((k, v) => <div>{k}: {v.message}</div>); | |
return ( | |
<div> | |
{children} | |
{messages} | |
</div> | |
); | |
}); | |
const Field = component({ | |
childContextTypes: { | |
registerValidation: React.PropTypes.func | |
}, | |
componentDidMount () { | |
this.validationState = this.context.registerValidation(this.props.name); | |
}, | |
componentWillUpdate (nextProps) { | |
const validationResult = this.validate(nextProps); | |
if (this.validationState.get() !== validationResult) { | |
this.validationState.update(() => validationResult); | |
} | |
} | |
}, function (props) { | |
const messages = (!this.validationState.get('isValid')) | |
? <div>{this.validationState.get('messages').map(m => <span>{m}</span>)}</div> | |
: null; | |
return ( | |
<div> | |
<label></label> | |
<input type="text" name={props.name} value={props.value}/> | |
{messages} | |
</div> | |
); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const personStruct = Immstruct({ | |
firstName: '', | |
lastName: '', | |
title: '', | |
titleOptions: [ | |
{ value: 0, label: 'Mr.' }, | |
{ value: 0, label: 'Mrs.' } | |
], | |
address: { | |
street: '', | |
city: '', | |
state: '' | |
zip: '' | |
} | |
}); | |
function foo (data, listOfTransformations) { | |
return data.current.map(m => { | |
}) | |
} | |
const validateFirstName = rules( | |
'firstName' | |
p => p.get('firstName'), | |
rule(required, 'First name is required'), | |
rule(startsWith('m'), 'Your name must start with an "m"') | |
); | |
const validateLastName = rules( | |
'lastName' | |
p => p.get('lastName'), | |
rule(required, 'Last name is required') | |
); | |
const validTitles = person.get('titleOptions').map(t => t.label); | |
const validateTitle = rules( | |
'title' | |
p => p.get('title'), | |
rule(isOneOf(validTitles), `Please choose one of the following options: ${validTitles.join(', ')}`); | |
); | |
const personValidator = compose(validateFirstName, validateLastName, validateTitle); | |
<Validator data={person} validator={personValidator} form={(person, validation) => { | |
<Form data={person}> | |
<Field value={person.get('firstName')} validationMessage={validation.get('firstName')}/> | |
<Field value={person.lastName} validationMessage={validation.get('lastName')} /> | |
<Dropdown selected={person.title} validationMessage={validation.get('title')} options={person.titleOptions} /> | |
</Form> | |
}} /> | |
class Validator extends ReactComponent { | |
componentWillReceiveProps (nextProps) { | |
this.validationResult = this.props.validator(ValidationState.create(nextProps.data)); | |
} | |
render () { | |
return {this.props.form(this.props.data, this.props.validationResult)}; | |
} | |
} | |
function rules (forProp, getContext, ...ruleSet) { | |
return (validationState) => { | |
const ctx = getContext(validationState.data); | |
return ruleSet.reduce((st, rule) => { | |
return st.addMessage(forProp, rule(ctx)); | |
}, validationState) | |
} | |
} | |
function rule (isValid, message) { | |
return (ctx) => { | |
const valid = isValid(ctx); | |
return { isValid: valid, message: (valid) ? '' : message }; | |
} | |
} | |
class ValidationState { | |
constructor (data) { | |
this.data = data; | |
this.isValid = true; | |
this.messages = {}; | |
} | |
addMessage (forProp, result) { | |
if (!Object.prototype.hasOwnProperty(this.messages, forProp)) { | |
this.messages[forProp] = []; | |
} | |
if (this.isValid) { | |
this.isValid = result.isValid; | |
} | |
if (result.isValid) { | |
this.messages[forProp].push(result.message); | |
} | |
return this; | |
} | |
static create (data) { | |
return new ValidationState(data); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { required, minLength, USZip } from './validationRules'; | |
const struct = Immutable.fromJS({ | |
city: '', | |
state: '', | |
zip: '' | |
}); | |
const AddressForm = component({ addressCursor } => { | |
return ( | |
<form> | |
<TextInput label="City" value={addressCursor.cursor('city')} /> | |
<TextInput label="State" value={addressCursor.cursor('state')} /> | |
<TextInput label="Zip" value={addressCursor.cursor('zip')} /> | |
</form> | |
); | |
}); | |
const TextField = component({ label, valueCursor } => { | |
return ( | |
<div> | |
<label>{label}</label> | |
<input type="text" value={valueCursor.deref()} /> | |
<ValidationMessage {...props} /> | |
</div> | |
); | |
}); | |
const Validatable = (ComponentToValidate, validationConfig) => { | |
let { val, rules, context } = validationConfig; | |
rules = (isArray(rules)) ? rules : []; | |
context = (isFunction(context)) | |
? context | |
: (props) => props; | |
const validateSelf = (isFunction(val)) | |
? (props) => { | |
const val = val(props); | |
const ctx = getContext(props); | |
return rules.map(r => r(val, ctx)); | |
} : () => []; | |
return component({ | |
statics: { | |
isValidatable: true, | |
validate (props) { | |
const selfState = validateSelf(props); | |
const childrenStates = React.Children.map(props.children, (c) => { | |
if (!c.type.isValidatable) return null; | |
return c.type.validate(c.props); | |
}); | |
return selfState.concat(...childrenStates)); | |
} | |
} | |
}, function (props, statics) { | |
const validationState = statics.validate(props); | |
return (<ComponentToValidate {...props} {...validationState} />); | |
}); | |
} | |
const StateField = Validatable(TextInput, { | |
val: (props) => props.valueCursor, | |
rules: [required, minLength(20)], | |
context: (props) => {} | |
}); | |
function liftValidator (validator, msg) { | |
return (val, ctx = {}, acc) => { | |
if (!acc) acc = { isValid: true, messages: [] }; | |
const validated = validation(val, ctx); | |
if (!validated) { | |
acc.isValid = false; | |
acc.messages.push(msg); | |
} | |
return acc; | |
} | |
} | |
function required (val) { | |
return !!val; | |
} | |
function minLength (len) { | |
return (val) => val.length > len; | |
} | |
const stateIsRequired = liftValidator(required, 'Please enter a value for state'); | |
const stateMustBeLongerThan20 = liftValidator(minLength(20), `Please use the state's full name, not an abbreviation`); | |
const stateValidation = compose(stateIsRequired, stateMustBeLongerThan20); | |
const stateIsValid = stateValidation(state).isValid; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment