material-ui-form
Version:
State and validation management for Material-UI form components
666 lines (558 loc) • 20.6 kB
Markdown
### material-ui-form
[](https://www.npmjs.com/package/material-ui-form)
[](https://david-dm.org/unitedhubs/material-ui-form.svg)
[](https://travis-ci.com/unitedhubs/material-ui-form)
[](https://codecov.io/gh/unitedhubs/material-ui-form)
1. [About](#about)
2. [Setup](#setup)
3. [Props](#props)
- [Form props](#form-props-optional)
- [Field props](#field-props)
- [Other props](#other-props)
4. [Examples](#examples)
- [Nested fields](#nested-fields)
- [Custom validation messages](#custom-validation-messages)
- [Custom validators](#custom-validators)
- [Custom validation logic](#custom-validation-logic)
- [Server validations](#server-validations)
- [Misc form settings](#form-autocomplete-and-on-error-submission)
- [Getting values on field update](#getting-form-values-on-field-update)
- [Stepper](#stepper)
- [Dynamic array fields](#dynamic-array-fields-notice-the-deletefieldrow-prop-on-the-remove-row-button)
- [Custom component handler](#custom-components-with-custom-handlers)
5. [Contributing](#contributing)
6. [License](#license)
## About
_material-ui-form_ is a React wrapper for [Material-UI](https://material-ui-1dab0.firebaseapp.com/getting-started/usage/) form components. Simply replace the `<form>` element with `<MaterialUIForm>` to get out-of-the-box state and validation support ***as-is***. There's no need to use any other components, alter your form's nesting structure, or write onChange handlers.
Validation is done with [validator.js](https://github.com/chriso/validator.js) but you can extend/customize validation messages, validators, and use your own validation logic too. Steppers, dynamic array fields and custom components are also supported.
#### use and requirements
- requires React 16.3.0 or newer
- supports official and unofficial Material-UI fields (other input elements are rendered without state/validation support)
- every input field must have `value` and `name` props
- every input field should NOT have `onChange` and `onBlur` props (unless you need custom field-specific logic)
- add a `data-validators` prop to any input field (or FormControl / FormControlLabel) to specify validation rules
#### extra validators
_material-ui-form_ extends [_validator.js_ validators](https://github.com/chriso/validator.js#validators) with the following validators:
- isAlias `/^[a-zA-Z0-9-_\.]*$/i`
- isDate
- isNumber `/^([,.\d]+)$/`
- isRequired `value.length !== 0`
- isSerial `/^([-\s\da-zA-Z]+)$/`
- isSize `value >= min && value <= max`
- isTime
#### _NOTE!_
While most Material-UI field components are supported there may be some that are not. Support for Material-UI field component props is another issue. Please [check here](https://github.com/unitedhubs/material-ui-form/issues/5) to see what is currently tested to be working.
## Setup
#### install
```
npm install --save material-ui-form
```
#### demo
1. `$ git clone https://github.com/unitedhubs/material-ui-form.git`
2. `$ cd material-ui-form`
3. `$ npm install && npm run dev`
## Props
#### Form props (optional):
Prop | Description | Default
------------------------------| --------------------------|------------
[***activeStep***](#stepper) _[number]_ | Use together with `onFieldValidation` for better Stepper support |
[***autoComplete***](#form-autocomplete-and-on-error-submission) _[string]_ | Sets form _autoComplete_ prop. Accepts one of ["on", "off"] | "off"
[***disableSubmitButtonOnError***](#form-autocomplete-and-on-error-submission) _[boolean]_ | Disables submit button if any errors exist | true
[***onFieldValidation***](#stepper) _[func]_ | Returns _@field_ and _@errorSteps_ (if `activeStep` prop is provided) on field validation |
[***onSubmit***](#nested-fields) _[func]_ | Returns _@values_ and _@pristineValues_ on form submission |
[***onValuesChange***](#getting-form-values-on-field-update) _[func]_ | Returns _@values_ and _@pristineValues_ on field value change |
***validation*** _[object]_ | Object specifying validation config options (prefixed below with ↳) |
↳ [***messageMap***](#custom-validation-messages) _[object]_ | A key-value list where the key is the validator name and the value is the error message. Is exposed as a _material-ui-form_ export parameter | _object_
↳ [***messageKeyPrefix***](#custom-validation-messages) _[string]_ | Optional prefix to apply to all messageMap keys. If specified, field validator names will automatically be appended the prefix | ""
↳ [***requiredValidatorName***](#custom-validation-logic) _[boolean, string]_ | Specifies the validator name and matching messegeMap key for required fields. To disable and rely on the native _required_ field prop, set to `false` | "isRequired"
↳ [***validate***](#custom-validation-logic) _[func]_ | Overrides the internal validate method. Receives the following parameters: _@fieldValue_, _@fieldValidators_, and _@...rest_ (where _@...rest_ is the **validation** prop object) | _func_
↳ [***validators***](#custom-validators) _[object]_ | Defaults to an extended validator.js object. Is exposed as a _material-ui-form_ export parameter | _object_
↳ ***validateInputOnBlur*** _[boolean]_ | Makes text input validations happen on blur instead of on change | false
[***validations***](#server-validations) _[object]_ | Validations to pass to the form (i.e. from the server). Should be an object with keys representing field _name_ props and values as arrays of field error messages. The first error message will be displayed per field |
#### Field props:
Prop | Description | Required
------------------------------| --------------------------|------------
***value*** _[any]_ | The value of the field. If empty set an empty string | Yes
***name*** _[string]_ | The name of the field | Yes
***data-validators*** _[string, array[object]]_ | Validators to apply to the field. Multiple validator names can be specified with a comma-delimited string |
***onBlur*** _[func]_ | A custom handler that will be called after the field's `onBlur` event. Provides _@value/checked_, _@field_ and _@event_ parameters |
***onChange*** _[func]_ | A custom handler that will be called after the field's `onChange` event. Provides _@value/checked_, _@field_ and _@event_ parameters |
#### Other props:
Prop | Value | Description
-------------------------| ------------------|------------------------
[***deletefieldrow***](#dynamic-array-fields-notice-the-deletefieldrow-prop-on-the-remove-row-button) _[string]_ | Field **name** prop up to and including the row index (i.e. _rooms[2]_) | Add to button components that use _onClick_ to remove any array field rows
## Examples
#### Nested fields:
```jsx
import MaterialUIForm from 'material-ui-form'
class MyForm extends React.Component {
submit = (values, pristineValues) => {
// get all values and pristineValues on form submission
}
customInputHandler = (value, { name }, event) => {
// the form will update the field as usual, and then call this handler
// if you want to have complete control of the field, change the "value" prop to "defaultValue"
}
customToggleHandler = (checked, { name, value }, event) => {
// the form will update the field as usual, and then call this handler
// if you want to have complete control of the field, change the "value" prop to "defaultValue"
}
render() {
return (
<MaterialUIForm onSubmit={this.submit}>
<TextField
label="Name"
type="text"
name="name"
value=""
data-validators="isRequired,isAlpha"
onChange={this.customInputHandler}
/>
<fieldset>
<legend>Nested</legend>
<Checkbox
checked
name="love"
value="yes"
onChange={this.customToggleHandler}
/>
<span>I love it</span>
<FormControl required>
<InputLabel>Age</InputLabel>
<Select value="" name="age">
<MenuItem value=""><em>Please select your age ...</em></MenuItem>
<MenuItem value={10}>Teens</MenuItem>
<MenuItem value={20}>Twenties</MenuItem>
<MenuItem value={30}>Thirties</MenuItem>
<MenuItem value="40+">Fourties +</MenuItem>
</Select>
<FormHelperText>Some important helper text</FormHelperText>
</FormControl>
</fieldset>
<Button variant="raised" type="reset">Reset</Button>
<Button variant="raised" type="submit">Submit</Button>
</MaterialUIForm>
)
}
}
```
#### Custom validation messages:
```jsx
import Form, { messageMap } from '../../src/index'
const customMessageMap = Object.assign(messageMap, {
myCustomPrefix_isInt: 'Invalid integer',
myCustomPrefix_isEmail: 'メールアドレスが無効です',
myCustomPrefix_isIn: '「{0}」のいずれかを記入してください',
myCustomPrefix_isWhitelisted: '文字は「{0}」から選択してください',
myCustomPrefix_isLength: '文字数は{0}以上{1}以下であることは条件',
})
class MyForm extends React.Component {
submit = (values, pristineValues) => {
// get all values and pristineValues on form submission
}
render() {
return (
<MaterialUIForm
onSubmit={this.submit}
validation={{
messageMap: customMessageMap,
messageKeyPrefix: 'myCustomPrefix_',
}}
>
<TextField
label="Email"
type="text"
name="email"
value="invalid@email."
data-validators="isEmail"
/>
<TextField
label="Inclusion"
type="number"
name="number"
value="3"
data-validators={[{ isIn: [1, 2, 4] }]}
/>
<TextField
label="Whitelisted characters"
type="text"
name="whitelisted"
value="abc1234"
data-validators={[{ isWhitelisted: 'abc123' }]}
/>
<TextField
label="Lenght test"
type="text"
name="length"
value="123"
data-validators={[{ isLength: { min: 4, max: 5 } }]}
/>
<Button variant="raised" type="submit">Submit</Button>
</MaterialUIForm>
)
}
}
```
#### Custom validators:
```jsx
import Form, { messageMap, validators } from '../../src/index'
validators.isBorat = value => value === 'borat'
const customMessageMap = Object.assign(messageMap, {
isBorat: 'NAAAAAT! You can only write "borat" lol',
})
class MyForm extends React.Component {
submit = (values, pristineValues) => {
// get all values and pristineValues on form submission
}
render() {
return (
<MaterialUIForm
onSubmit={this.submit}
validation={{
messageMap: customMessageMap,
validators,
}}
>
<TextField
label="Write anything..."
type="text"
name="trickster"
value=""
helperText="this is not a trick"
data-validators="isBorat"
/>
<Button variant="raised" type="submit">Submit</Button>
</MaterialUIForm>
)
}
}
```
#### Custom validation logic:
```jsx
import MaterialUIForm from 'material-ui-form'
function validate(value, fieldValidators, options) {
const fieldValidations = []
fieldValidators.forEach((validator) => {
const validation = {
code: String(validator),
message: 'its invalid so maybe try harder...',
}
if (_.has(options, 'genericMessage')) {
validation.message = options.genericMessage
}
fieldValidations.push(validation)
})
return fieldValidations
}
const validationOptions = {
genericMessage: 'yeah... *tisk*',
}
class MyForm extends React.Component {
submit = (values, pristineValues) => {
// get all values and pristineValues on form submission
}
render() {
return (
<MaterialUIForm
onSubmit={this.submit}
validation={{
requiredValidatorName: false,
validate,
...validationOptions,
}}
>
<TextField
label="Whatever you write isn't gonna be good enough"
type="text"
name="test"
value=""
data-validators="whatever - our custom validator will ignore this"
required
/>
<Button variant="raised" type="submit">Submit</Button>
</MaterialUIForm>
)
}
}
```
#### Server validations:
```jsx
import MaterialUIForm from 'material-ui-form'
const mockServerValidations = {
name: [{ code: 'isInvalid', message: 'such invalid...' }],
}
class MyForm extends React.Component {
state = {
mockServerValidations,
}
componentDidMount() {
let validations = {
name: [{ message: 'such WOOOOOOOOOW...' }],
}
setTimeout(() => {
this.setState({ mockServerValidations: validations })
}, 1500)
setTimeout(() => {
validations = {
name: [{ message: 'so still haven\'t watched Italian Spiderman?' }],
}
this.setState({ mockServerValidations: validations })
}, 3000)
}
submit = (values, pristineValues) => {
// get all values and pristineValues on form submission
}
render() {
return (
<MaterialUIForm
onSubmit={this.submit}
validations={this.state.mockServerValidations}
>
<TextField
label="Name"
type="text"
name="name"
value="doge"
/>
<Button variant="raised" type="submit">Submit</Button>
</MaterialUIForm>
)
}
}
```
#### Form autoComplete and "on error" submission:
```jsx
import MaterialUIForm from 'material-ui-form'
class MyForm extends React.Component {
submit = (values, pristineValues) => {
// get all values and pristineValues on form submission
}
render() {
return (
<MaterialUIForm
autoComplete="on"
disableSubmitButtonOnError={false}
onSubmit={this.submit}
>
<TextField
label="Name"
type="text"
name="name"
value="doge"
data-validators="isInt"
/>
<Button variant="raised" type="submit">Submit</Button>
</MaterialUIForm>
)
}
}
```
#### Getting form values on field update:
```jsx
import MaterialUIForm from 'material-ui-form'
class MyForm extends React.Component {
handleValuesChange = (values, pristineValues) => {
// get all values and pristineValues when any field updates
}
handleFieldValidations = (field) => {
// get field object when its validation status updates
}
submit = (values, pristineValues) => {
// get all values and pristineValues on form submission
}
render() {
return (
<MaterialUIForm
onSubmit={this.submit}
onValuesChange={this.handleValuesChange}
onFieldValidation={this.handleFieldValidations}
>
<TextField
label="Name"
name="name"
value="doge"
required
/>
<Button variant="raised" type="submit">Submit</Button>
</MaterialUIForm>
)
}
}
```
#### Stepper:
```jsx
import Stepper, { Step, StepLabel } from 'material-ui/Stepper'
import MaterialUIForm from 'material-ui-form'
function getSteps() {
return [
'Step 1',
'Step 2',
]
}
class MyForm extends React.Component {
state = {
activeStep: 0,
errorSteps: [],
}
clickNext = () => {
this.setState({
activeStep: this.state.activeStep + 1,
})
}
clickBack = () => {
this.setState({
activeStep: this.state.activeStep - 1,
})
}
submit = (values, pristineValues) => {
// get all values and pristineValues on form submission
}
updateErrorSteps = (field, errorSteps) => {
this.setState({ errorSteps })
}
render() {
const steps = getSteps()
const { activeStep } = this.state
return (
<div>
<Stepper activeStep={activeStep} alternativeLabel>
{steps.map((label, i) => (
<Step key={label}>
<StepLabel error={errorSteps.includes(i)}>
{label}
</StepLabel>
</Step>
))}
</Stepper>
<MaterialUIForm
activeStep={activeStep}
onFieldValidation={this.updateErrorSteps}
onSubmit={this.submit}
>
{activeStep === 0 &&
<React.Fragment>
<TextField
label="Name"
name="name"
value=""
required
/>
<Button variant="raised" onClick={this.clickNext}>Next</Button>
</React.Fragment>
}
{activeStep === 1 &&
<React.Fragment>
<TextField
label="Address"
name="address"
value=""
required
/>
<Button variant="raised" onClick={this.clickBack}>Back</Button>
<Button variant="raised" type="submit">Submit</Button>
</React.Fragment>
}
</MaterialUIForm>
</div>
)
}
}
```
#### Dynamic array fields (notice the `deletefieldrow` prop on the "Remove Row" button):
```jsx
import MaterialUIForm from 'material-ui-form'
import formData from 'form-data-to-object'
class MyForm extends React.Component {
state = {
rows: [{ _id: _.uniqueId() }],
onSubmitValues: null,
}
addRow = () => {
const { rows } = this.state
rows.push({ _id: _.uniqueId() })
this.setState({ rows })
}
removeRow = (index) => {
const { rows } = this.state
if (rows.length > 1) {
rows.splice(index, 1)
this.setState({ rows })
}
}
submit = (values, pristineValues) => {
// you can parse values to turn:
// rows[0][label]: "label"
// into:
// rows: [{ label: "label" }]
const parsedValues = formData.toObj(values)
}
render() {
const steps = getSteps()
return (
<MaterialUIForm onSubmit={this.submit}>
{this.state.rows.map((row, i) => (
<Fragment key={row._id}>
<TextField
label="Label"
name={`rows[${i}][label]`}
value=""
required
/>
<TextField
label="Value"
name={`rows[${i}][value]`}
value=""
/>
{ this.state.rows.length > 1 &&
<Button
onClick={() => this.removeRow(i)}
deletefieldrow={`rows[${i}]`}
>
Remove Row
</Button>
}
</Fragment>
))}
<Button variant="raised" onClick={this.addRow}>Add row</Button>
<Button variant="raised" color="primary" type="submit">Submit</Button>
</MaterialUIForm>
)
}
}
```
#### Custom components with custom handlers:
```jsx
import MaterialUIForm from 'material-ui-form'
class MyForm extends React.Component {
uploadFile = (event) => {
console.log(event.target.files)
}
render() {
return (
<div>
<MaterialUIForm>
{'Upload file: '}
<input
accept="image/*"
style={{ display: 'none' }}
id="raised-button-file"
multiple
type="file"
onChange={this.uploadFile}
/>
<label htmlFor="raised-button-file">
<Button variant="raised" component="span">
Upload
</Button>
</label>
</MaterialUIForm>
</div>
)
}
}
```
## Contributing
This is a new project and contributions are welcome so feel free to [open an issue](https://github.com/unitedhubs/material-ui-form/issues) or fork and create a pull request.
__Now looking for maintainers!__ If interested please communicate so in [this issue](https://github.com/unitedhubs/material-ui-form/issues/60).
## License
This project is licensed under the terms of the [MIT license](https://github.com/unitedhubs/material-ui-form/blob/dev/LICENSE).