Skip to main content

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 NameTypeDescription
descstringThe description for the field.
disabledbooleanWhether or not the field is disabled.
errorTextstringThe error text to display if isInvalid is true.
isInvalidbooleanWhether or not the field is invalid.
isValidbooleanWhether or not the field is valid.
isWarningbooleanWhether or not the field is warning.
isEditedbooleanWhether or not the field is edited.
labelstringThe label for the field.
namestringThe name of the field. This is used to serialize the form data.
radiusstringThe radius of the field. One of round, square, or pill.
readOnlybooleanWhether or not the field is read only.
renderEndAddOnfunctionA function that returns a React node to render at the end of the field. Use one InputGroup component, Button, or Dropdown.
renderPrefixfunctionA function that returns a React node.
renderStartAddOnfunctionA function that returns a React node to render at the start of the field. Use one InputGroup component, Button, or Dropdown.
renderSuffixfunctionA function that returns a React node.
sizestringThe size of the field. One of sm, md, or lg.
validTextstringThe valid text to display if isValid is true.
warningTextstringThe warning text to display if isWarning is true.
weditedTextstringThe edited text to display if isEdited is true.

Validation States

Result
Loading...
Live Editor
<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

Result
Loading...
Live Editor
<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.

Result
Loading...
Live Editor
<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
Result
Loading...
Live Editor
<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.

Result
Loading...
Live Editor
<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.

Result
Loading...
Live Editor
<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.

Result
Loading...
Live Editor
<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.

Result
Loading...
Live Editor
<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.

Result
Loading...
Live Editor
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.

Result
Loading...
Live Editor
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 />);