const MyTextComponent = ({ field, ...innerProps }) => {
return (
<Input
value={field.value}
onBlur={field.touch}
onChange={ev => field.set(ev.value)}
{...innerProps}
className={field.touched ? field.error : ""}
/>
);
};
const FormComponent = () => {
const form = useForm(
{
id: "",
name: "",
description: "",
},
{
fieldValidation:
{
fields: {
// This is a validator for a specific field, triggered onchange.
id: id => !id && "Id is an required field",
},
onChange: values => values.name && values.name.length < 1 && "The name needs to have more letters",
// This is a validator for all the fileds, triggered at submit
onSubmit: values => !values.name && !values.description && "You need to specify either name or description",
}
onSubmit: values => doSomething(values),
},
);
return (
<form>
<MyTextComponent field={form.fields.id} />
<MyTextComponent field={form.fields.name} />
<MyTextComponent field={form.fields.description} />
</form>
);
};
It also works with arrays. You can specify individual field validators as well as an validator for the whole array.
const List = ({ arrayField }) => {
const addType = (e) => {
e.preventDefault();
arrayField.push({type: ""})
}
return (
<div>
<button onClick={addType}>Add a new Type</button>
<ul>
{ arrayField.items.map((val, index) =>
<li key={index}>
<MyTextComponent field={val} />
<button onClick={() => arrayField.remove(index)} />
</li>)
}
</ul>
</div>
);
};
const FormComponent = () => {
const form = useForm(
{
id: "",
name: "",
description: "",
list: [{type: ""}]
},
{
fieldValidation: {
fields: {
id: id => (!id ? "Id is an required field" : undefined),
list: {
// Validate the content in the list indiually
item: {
// The content is an complex field, add a field validator for the object
fields: {
type: {
onChange: value => (!value ? "type is required" : undefined),
}
}
}
onSubmit: (list => (list.length > 10 ? "You can max have 10 listOfObjects": undefined))
}
}
},
onSubmit: values => doSomething(values),
},
);
return (
<form>
<MyTextComponent field={form.fields.id} />
<MyTextComponent field={form.fields.name} />
<MyTextComponent field={form.fields.description} />
<List field={form.fields.listOfObjects} />
</form>
);
};
To work with optional sub branches of the form, just add a complex type that can be null. Use the .set method on the parent field to add/remove the subform part.
const MyChildrenComponent = ({ complexField }) => {
return (
<div>
<MyTextComponent field={complexField.foo} />
<MyTextComponent field={complexField.bar} />
<MyTextComponent field={complexField.baz} />
</div>
);
};
const FormComponent = () => {
const form = useForm(
{
id: "",
name: "",
description: "",
requiredInfo: { foo: "", bar: "", baz: "" }
extraInfo: null
},
{
fieldValidation: {
fields: {
requiredInfo: {
fields: {
foo: {
onChange: value => !value && "foo is required",
}
},
onChange: values => !values.bar && !values.baz && "You need to specify either bar or baz",
},
}
},
onSubmit: values => doSomething(values),
},
);
const addExtraInfo = (e) => {
e.preventDefault();
form.value.extraInfo.set({ foo: "", bar: "", baz: "" });
}
return (
<form>
<MyTextComponent field={form.fields.id} />
<MyTextComponent field={form.fields.name} />
<MyTextComponent field={form.fields.description} />
<MyChildrenComponent field={form.fields.requiredInfo.fields} />
{ form.fields.extraInfo.fields
? <MyChildrenComponent field={form.fields.extraInfo.fields} />
: <button onClick={addExtraInfo}>Add Extra Info</button>
}
</form>
);
};
In form validation undefined
and null
has special meanings. Using null
explicitly means that there is no error and undefined
means that an eventual error should be untouched (keep was there).
const MyChildrenComponent = ({ complexField }) => {
return (
<div>
<MyTextComponent field={complexField.foo} />
<MyTextComponent field={complexField.bar} />
<MyTextComponent field={complexField.baz} />
</div>
);
};
const FormComponent = () => {
const form = useForm(
{
id: "",
useDescription: false,
description: "",
},
{
formValidation: (values) => {
return {
fields: {
description:
values.useDescription && !values.description
? "Description is required when 'use description' is checked"
: null,
},
};
},
onSubmit: (values) => doSomething(values),
},
);
const addExtraInfo = (e) => {
e.preventDefault();
form.value.extraInfo.set({ foo: "", bar: "", baz: "" });
};
return (
<form>
<MyTextComponent field={form.fields.id} />
<MyTextComponent field={form.fields.name} />
<MyTextComponent field={form.fields.description} />
<MyChildrenComponent field={form.fields.requiredInfo.fields} />
{form.fields.extraInfo.fields ? (
<MyChildrenComponent field={form.fields.extraInfo.fields} />
) : (
<button onClick={addExtraInfo}>Add Extra Info</button>
)}
</form>
);
};
TODO when interface is stable
- Async field level validation
- No opiniated way of doing validation (could perhaps support yoi out of the box but without a dependency)
- Arrays must not be used as tuples (same type is assumed on all indexes)
- No debouncing on async validation (should probably be implemented)
- No cancelation of async validation
- Only basic array manipulation supported (push and remove) (implement more when needed)
- No pre-bundled helper components to render fields (formik has it, but it may not be needed)
- No pre-bundled helper components to render validation errors (formik has it, but it may not be needed)