Skip to content

Instantly share code, notes, and snippets.

@dpoindexter
Last active August 29, 2015 14:23
Show Gist options
  • Save dpoindexter/be7f062f09cf40f3daea to your computer and use it in GitHub Desktop.
Save dpoindexter/be7f062f09cf40f3daea to your computer and use it in GitHub Desktop.
React validation concept
- 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);
}
}
}
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>
);
});
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);
}
}
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