Intro
The @emoney/kyber-forms package can be used to create any type of form. The examples in this readme are oriented from simple to complex.
Fields
Kyber's form components are stateless high level components that encapsulate serialization and validation. Most of them can be used uncontrolled or controlled. Each field component has the following shared props:
Prop Name | Type | Description |
---|---|---|
desc | string | The description for the field. |
disabled | boolean | Whether or not the field is disabled. |
errorText | string | The error text to display if isInvalid is true. |
isInvalid | boolean | Whether or not the field is invalid. |
isValid | boolean | Whether or not the field is valid. |
isWarning | boolean | Whether or not the field is warning. |
isEdited | boolean | Whether or not the field is edited. |
label | string | The label for the field. |
name | string | The name of the field. This is used to serialize the form data. |
radius | string | The radius of the field. One of round , square , or pill . |
readOnly | boolean | Whether or not the field is read only. |
renderEndAddOn | function | A function that returns a React node to render at the end of the field. Use one InputGroup component, Button, or Dropdown. |
renderPrefix | function | A function that returns a React node. |
renderStartAddOn | function | A function that returns a React node to render at the start of the field. Use one InputGroup component, Button, or Dropdown. |
renderSuffix | function | A function that returns a React node. |
size | string | The size of the field. One of sm , md , or lg . |
validText | string | The valid text to display if isValid is true. |
warningText | string | The warning text to display if isWarning is true. |
weditedText | string | The edited text to display if isEdited is true. |
Validation States
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, minmax(100px, auto))', gridGap: '1rem', }} > <TextField label="Valid" name="valid" isValid validText="This field is valid" value="Valid" /> <TextField label="Invalid" name="invalid" errorText="This field is invalid" isInvalid value="Invalid" /> <TextField label="Warning" name="warning" isWarning warningText="This field has a warning" value="Warning" /> </div>
Addons, Prefixes, and Suffixes
<div style={{ display: 'grid', gridGap: '1rem' }}> <TextField renderStartAddOn={() => <InputGroup.Text>Start AddOn</InputGroup.Text>} renderEndAddOn={() => <InputGroup.Text>End AddOn</InputGroup.Text>} renderPrefix={() => 'Prefix'} renderSuffix={() => 'Suffix'} /> <TextField renderStartAddOn={() => <Button variant="outline-secondary">Start AddOn</Button>} renderEndAddOn={() => <Button variant="outline-secondary">End AddOn</Button>} renderPrefix={() => 'Prefix'} renderSuffix={() => 'Suffix'} /> <TextField renderStartAddOn={() => ( <DropdownButton title="Start AddOn" variant="outline-secondary"> <DropdownItem>Item</DropdownItem> </DropdownButton> )} renderEndAddOn={() => ( <DropdownButton title="End AddOn" variant="outline-secondary"> <DropdownItem>Item</DropdownItem> </DropdownButton> )} renderPrefix={() => 'Prefix'} renderSuffix={() => 'Suffix'} /> </div>
Multiple prefixes or suffixes
Some field components (e.g. NumberField) utilize prefixes or suffixes to display controls like the plus and minus spinner. In these cases it may be necessary to set the flex order of your prefix/suffix to make sure it's displayed correctly.
<NumberField renderSuffix={() => <span className="order-first me-2">USD</span>} />
Using fields within addons
It's possible to use certain field components within the addons of other fields. This is useful for creating complex forms with custom layouts. Only certain field components can be used in this way, such as TextField, NumberField, Select, and DatePicker.
When using field components with addons you should:
- Spread the addonProps onto the field component
- Set a static width for components like Select to avoid layout issues if the value changes
<div className="d-grid gap-3"> <TextField label="Start and End Addons" renderStartAddOn={(addonProps) => ( <Select {...addonProps} aria-label="Select" style={{ width: 150 }}> <Item key="one">One</Item> <Item key="two">Two</Item> </Select> )} renderEndAddOn={(addonProps) => ( <Select {...addonProps} aria-label="Select" placement="bottom right" style={{ width: 150 }}> <Item key="one">One</Item> <Item key="two">Two</Item> </Select> )} /> <TextField label="Start Addon" renderStartAddOn={(addonProps) => ( <Select {...addonProps} aria-label="Select" style={{ width: 150 }}> <Item key="one">One</Item> <Item key="two">Two</Item> </Select> )} /> <TextField label="End Addon" renderEndAddOn={(addonProps) => ( <Select {...addonProps} aria-label="Select" placement="bottom right" style={{ width: 150 }}> <Item key="one">One</Item> <Item key="two">Two</Item> </Select> )} /> <TextField label="Sentence Maker" renderPrefix={() => 'Verb'} value="am" renderStartAddOn={(addonProps) => ( <TextField {...addonProps} aria-label="Start Addon" style={{ width: 150 }} placeholder="Start Addon" renderPrefix={() => 'Subject'} value="I" /> )} renderEndAddOn={(addonProps) => ( <TextField {...addonProps} aria-label="End Addon" style={{ width: 150 }} placeholder="End Addon" renderPrefix={() => 'Noun'} value="dog" /> )} /> <TextField label="Custom Question" value="How much are you willing to pay?" renderEndAddOn={(addonProps) => ( <NumberField {...addonProps} aria-label="End Addon" style={{ width: 150 }} placeholder="End Addon" value={100} renderPrefix={() => <span className="icon-currency-dollar" />} /> )} /> <Select label="Important Dates" value="How much are you willing to pay?" renderEndAddOn={(addonProps) => ( <DatePicker {...addonProps} aria-label="End Addon" style={{ width: 150 }} /> )} > <Item key="Birthday">Birthday</Item> <Item key="Retirement">Retirement</Item> <Item key="Death">Death</Item> </Select> </div>
Within a form
When using field components as addons it's possible to serialize their values within the form. The recommendation is to use a nested object structure and dot notation names.
<Form onSubmit={(values, { setSubmitting }) => { console.log(values); setSubmitting(false); }} initialValues={{ textfield: { text: 'Custom', startAddon: 'one', endAddon: 'two', }, }} > <FormField> <TextField name="textfield.text" label="TextField" renderStartAddOn={(addonProps) => ( <FormField> <Select {...addonProps} name="textfield.startAddon" aria-label="Textfield Start Addon" style={{ width: 150 }} > <Item key="one">One</Item> <Item key="two">Two</Item> <Item key="three">Three</Item> <Item key="four">Four</Item> </Select> </FormField> )} renderEndAddOn={(addonProps) => ( <FormField> <Select {...addonProps} name="textfield.endAddon" aria-label="Textfield End Addon" placement="bottom right" style={{ width: 150 }} > <Item key="one">One</Item> <Item key="two">Two</Item> <Item key="three">Threeeeeeeeeeee</Item> <Item key="four">Four</Item> </Select> </FormField> )} /> </FormField> </Form>
Sizes
The size prop can be used to change the size of the field. The default size is md
.
<div style={{ display: 'grid', gridGap: '2rem', }} > <TextField label="Extra Small" renderStartAddOn={() => <InputGroup.Text>Start</InputGroup.Text>} renderEndAddOn={() => <InputGroup.Text>End</InputGroup.Text>} renderPrefix={() => 'Prefix'} renderSuffix={() => 'Suffix'} size="xs" value="Extra Small" description="Description" /> <TextField label="Small" renderStartAddOn={() => <InputGroup.Text>Start</InputGroup.Text>} renderEndAddOn={() => <InputGroup.Text>End</InputGroup.Text>} renderPrefix={() => 'Prefix'} renderSuffix={() => 'Suffix'} size="sm" value="Small" description="Description" /> <TextField label="Medium (default)" renderStartAddOn={() => <InputGroup.Text>Start</InputGroup.Text>} renderEndAddOn={() => <InputGroup.Text>End</InputGroup.Text>} renderPrefix={() => 'Prefix'} renderSuffix={() => 'Suffix'} value="Medium (default)" description="Description" /> <TextField label="Large" renderStartAddOn={() => <InputGroup.Text>Start</InputGroup.Text>} renderEndAddOn={() => <InputGroup.Text>End</InputGroup.Text>} renderPrefix={() => 'Prefix'} renderSuffix={() => 'Suffix'} size="lg" value="Large" description="Description" /> <TextField label="Extra Large" renderStartAddOn={() => <InputGroup.Text>Start</InputGroup.Text>} renderEndAddOn={() => <InputGroup.Text>End</InputGroup.Text>} renderPrefix={() => 'Prefix'} renderSuffix={() => 'Suffix'} size="xl" value="Extra Large" description="Description" /> <TextField label="Extra Extra Large" renderStartAddOn={() => <InputGroup.Text>Start</InputGroup.Text>} renderEndAddOn={() => <InputGroup.Text>End</InputGroup.Text>} renderPrefix={() => 'Prefix'} renderSuffix={() => 'Suffix'} size="xxl" value="Extra Extra Large" description="Description" /> </div>
Radii
The radius prop can control the corner styling of any field component. The default is round
. The other options are square
and pill
.
<div style={{ display: 'grid', gridGap: '1rem', }} > <TextField renderStartAddOn={() => <InputGroup.Text>Start</InputGroup.Text>} renderEndAddOn={() => <InputGroup.Text>End</InputGroup.Text>} renderPrefix={() => 'Prefix'} renderSuffix={() => 'Suffix'} radius="square" value="Square" /> <TextField renderStartAddOn={() => <InputGroup.Text>Start</InputGroup.Text>} renderEndAddOn={() => <InputGroup.Text>End</InputGroup.Text>} renderPrefix={() => 'Prefix'} renderSuffix={() => 'Suffix'} value="Round (default)" /> <TextField renderStartAddOn={() => <InputGroup.Text>Start</InputGroup.Text>} renderEndAddOn={() => <InputGroup.Text>End</InputGroup.Text>} renderPrefix={() => 'Prefix'} renderSuffix={() => 'Suffix'} radius="pill" value="Pill" /> </div>
Usage
Form Component
If you need more control over how the form inputs are rendered (E.g. columns or other custom layouts) you can use the individual form components inside of a Kyber Form component. The Form component will still help you manage state and validation while you have direct access to each input component.
<Form validationSchema={object().shape({ name: string().required().label('Name'), age: number().required().label('Age'), })} onSubmit={(values, actions) => { actions.setSubmitting(false); }} > <FormField> <TextField label="Name" name="name" /> </FormField> <FormField> <NumberField label="Age" name="age" /> </FormField> </Form>
DataForm Component
If your form is straightforward you can build it using just the DataForm component and an object based schema.
DataForm is only available in the @emoney/kyber-forms
package.
const schema = object().shape({ name: string().required().label('Name'), age: number().required().label('Age'), }); render( <DataForm id="dataform" validationSchema={schema} fields={[ { as: 'TextField', name: 'name', fieldLabel: 'Name' }, { as: 'NumberField', name: 'age', fieldLabel: 'Age' }, ]} onSubmit={(values, actions) => { actions.setSubmitting(false); console.log({ values }); }} />, );
Self Managed
If you need complete control over the form you can manage state and validation yourself by importing the individual form components and combining them with third party form management and validation libraries. The Kyber team recommends Formik and Yup, but other libraries like React Hook Form can also work.
Note: This example uses the HTML <form>
element, not the Kyber Form component.
setLocale(yupValidation); const schema = object().shape({ name: string().required().label('Name'), age: number().required().label('Age'), }); function Example() { const { values, touched, errors, setFieldValue, handleBlur, handleSubmit } = useFormik({ initialValues: { name: '', age: '', }, onSubmit: (values) => { console.log(values); }, validationSchema: schema, }); const handleChange = (name) => (value) => { setFieldValue(name, value); }; return ( <form onSubmit={handleSubmit}> <TextField errorText={errors.name} isInvalid={touched.name && errors.name} label="Name" name="name" onBlur={handleBlur} onChange={handleChange('name')} value={values.name} /> <NumberField errorText={errors.age} isInvalid={touched.age && errors.age} label="Age" name="age" onBlur={handleBlur} onChange={handleChange('age')} value={values.age} /> <Button className="mt-3" id="form-submit" type="submit" primary> Submit </Button> </form> ); } render(<Example />);