@koliseoapi/react-forms
v1.3.19
Published
HTML5 input components with automatic property type conversion
Downloads
83
Readme
react-forms
A library of HTML5 elements that convert user input automatically to state attributes. Give it a try.
import { Form, Input, Button } from '@koliseoapi/react-forms';
const state = { username: 'Foo', age: 20 };
function MyComponent(props) {
return (
<Form onSubmit={mySaveFunction} initialValues={state}>
<Input type="text" name="username" required maxLength="100" />
<Input type="number" name="age" min="0" step="1" />
<Button>Save</Button>
</Form>
);
});
The object passed to Form.initialValues
is shallow copied and stored internally as state. Anything introduced by the user is converted to a JavaScript type (number
, date
, boolean
) and assigned to the attribute indicated by name
. name
can also be a nested attribute like "location.address"
, in which case any missing intermediate nodes will be generated automatically.
When submitting, the Form
container validates all fields and triggers onSubmit
, passing the state object as only argument.
The following components are supported:
Form
Input (text, number, checkbox, radio, url, email, date, time, datetime-local)
TextArea
Select
Controlled vs Uncontrolled forms
You can use Form
in two ways:
- Controlled: You manage the state outside the form and pass it as
values
. This is useful when you want to manage the state in a parent component.
const [state, setState] = useState({ username: 'Foo', age: 20 });
<Form values={state} setValues={setState}>
<Input type="text" name="username" required />
<Input type="number" name="age" />
<Button>Save</Button>
</Form>
- Uncontrolled: You let the form manage the state internally. This is useful when you want to keep the state in the form itself.
<Form initialValues={{ username: 'Foo', age: 20 }}>
<Input type="text" name="username" required />
<Input type="number" name="age" />
<Button>Save</Button>
</Form>
You can also pass controlled values to a specific input element:
const [username, setUsername] = useState('Foo');
const [age, setAge] = useState(20);
<Input type="text" name="username" value={username} setValue={setUsername} />
<Input type="number" name="age" value={age} setValue={setAge} />
In this case the new value (age and username in this example) will both be stored in the local variable and as the root object in the associated Form
element.
Conversions
Each input field converts values automatically from string
to the expected type:
text
,url
andemail
are validated for format and passed as is:
<Input type="email" name="email" />
<TextArea name="description" />
The native HTML element is used for data input, and the data is validated again using JavaScript before submission.
number
is converted to a JavaScript with decimals according to the value ofstep
(default is1
):
<Input type="number" name="age" />
<Input type="number" name="percentage" step="0.01" />
checkbox
is transformed to a boolean:
<Input type="checkbox" name="subscribed" />
date
andtime
are validated for format and min/max restrictions, then passed as strings with formatyyyy-MM-dd
andHH:mm
respectively.
<Input type="date" name="birthdate" min="1900-01-01" max="2020-01-01" pattern="\d{4}-\d{2}-\d{2}"/>
<Input type="time" pattern="[0-9]{2}:[0-9]{2}">
datetime-local
does the same with format yyyy-MM-ddTHH:mm
. A pattern
attribute may be specified for browsers that do not support this input type.
<Input type="datetime-local" name="birthTimestamp" pattern="\d{4}-\d{2}-\d{2}T[0-9]{2}:[0-9]{2}"/>
Keep in mind that these will render the corresponding native HTML elements, which are supported on all major browsers except Safari on MacOS, where input[type=text]
will be used instead. For this reason we recommend to set a pattern
attribute like the example above. Even though the date picker doesn't use it, the text input fallback will.
Custom converters
You can override the converter associated to any form field:
const AllowedValues = { one: instanceOne, two: instanceTwo };
const state = { defcon: AllowedValues.one }
const myConverter =
// transform from String to Object
fromValue: function(value) {
return AllowedValues[value] || AllowedValues.one;
},
// transform from object to String
toValue: function(value) {
const key = Object.keys(AllowedValues).find((key) => AllowedValues[key] === value);
return key || 'one';
}
};
<Input type="text" name="defcon" converter={myConverter} state={state}/>
Validations
Before submitting the form, all user input is validated. The field validations are configured according to the following input atributes:
[required]
[pattern]
[min]
and[max]
for[type=number|date|time]
[type=email]
[type=url]
When the user submits a Form
, the onSubmit
callback will only be invoked after all validations pass. Otherwise, an error message will be displayed next to every element that didn't pass, and focus will be transferred to the first error component.
Both input fields and alert messages will use the corresponding ARIA attributes (aria-invalid
, aria-describedBy
and role=alert
) to be used by assistive technologies.
Disabling the form during submissions
Use FieldSet
and Button
to provide with visual feedback. During form submission the fieldset will be disabled and the submit button will display a "loading" class that you can tweak to display a spinner.
Custom Validations
To override the validation applied to a field, pass a function that returns a Promise
:
// return undefined if the validation passes, otherwise an i18n error entry
const fetchValidate = async (value, props) => {
const response = await fetch(`checkAvailability?username=${value}`);
const json = await response.json();
return json.ok? undefined : "usernameTaken"
}
<Form onSubmit={save} state={state}>
<label htmlFor="username">Choose your username</label>
<Input type="text" name="username" validate={fetchValidate} />
</Form>
The function should return undefined
if the validation passes, or an error message otherwise. The error code returned must correspond to an i18n entry.
Interaction between components
The following is an example where a checkbox makes an input appear or disappear based on state:
const values = {
/** true to ask to be reimbursed for expenses */
reimburseExpenses: false,
/** if `reimburseExpenses` is true, location they travel from */
travelFrom: undefined
}
const [reimburseExpenses, setReimburseExpenses] = useState(values.reimburseExpenses);
<Form state={values}>
<Input
type="checkbox"
name="reimburseExpenses"
onChange={(e, formContext) => {
const value = e.target.checked;
setReimburseExpenses(value);
// if reimburseExpenses is unchecked, travelFrom should be empty
!reimburseExpenses &&
formContext.setValue('travelFrom', undefined);
}}
/>
{
// if reimburseExpenses is checked, travelFrom is required
reimburseExpenses &&
<Input
type="text"
name="travelFrom"
required
/>
}
</Form>
Internationalization
To override the locale for validation messages, use I18nContext
:
import { I18nContext } from '@koliseoapi/react-forms';
<I18nContext.Provider value={{
required: "Por favor, rellena este campo",
min: "El valor debe ser mayor o igual que ${min}"
}}>
The full list of validation messages is here.