Custom Components
Customized components let you extend Formitiva with your own field types while seamlessly integrating with its validation, layout, localization, and builder ecosystem. This allows you to model complex data structures, build specialized inputs, and maintain a consistent form experience throughout your application.
In this guide, you’ll learn how to:
-
Define a custom React input component
-
Register the new component in your app.
Then you can use this component the same as other built-in components.
-
Define an example JSON schema using the new component
-
Use the definition in a form
-
Define type validation handler and register it
The following example demonstrates a Point2D component that captures a pair of values [X, Y] and validates them as a single value.
Step 1: Define Custom Component
Define Point2DInput component which has [x, y] value.
import React from "react";
import type { BaseInputProps, DefinitionPropertyField} from "@formitiva/react";
import { StandardFieldLayout, CSS_CLASSES, combineClasses } from "@formitiva/react";
import { useFormitivaContext, useFieldValidator, useUncontrolledValidatedInput } from "@formitiva/react";
type Point2DValue = [string, string];
export type Point2DInputProps = BaseInputProps<Point2DValue, DefinitionPropertyField>;
// Custom Point2DInput component
const Point2DInput: React.FC<Point2DInputProps> = ({ field, value, onChange, onError, error: externalError }) => {
const { t } = useFormitivaContext();
const fieldValidate = useFieldValidator(field, externalError);
// Normalize the input value to a [string, string] tuple for useUncontrolledValidatedInput input
const normalizedValue: Point2DValue = Array.isArray(value)
? [String(value[0] ?? ""), String(value[1] ?? "")]
: ["", ""];
// Define a change handler that normalizes the input to a [string, string] tuple
// and calls onChange with the new value
const handleChange = React.useCallback(
(nextValue: string | string[]) => {
const arr = Array.isArray(nextValue) ? nextValue : [nextValue, ""];
onChange?.([String(arr[0] ?? ""), String(arr[1] ?? "")]);
},
[onChange]
);
// Define a validation handler that validates the input as a 2D point array
const handleValidation = React.useCallback(
(nextValue: string | string[], trigger?: 'change' | 'blur' | 'sync') => {
const arr = Array.isArray(nextValue) ? nextValue : [nextValue, ""];
return fieldValidate([String(arr[0] ?? ""), String(arr[1] ?? "")], trigger as any);
},
[fieldValidate]
);
// Use useUncontrolledValidatedInput for managing input state and validation
const { getInputRef, error, getHandleChange } = useUncontrolledValidatedInput<HTMLInputElement, string[]>({
value: normalizedValue, // Normalized value for the hook
count: 2, // two inputs (x and y)
onError,
onChange: handleChange,
validate: handleValidation,
});
return (
<StandardFieldLayout field={field} error={error}>
<input
id={`${field.name}-x`}
type="text"
placeholder={t('X')}
defaultValue={normalizedValue[0]}
ref={getInputRef(0)}
onChange={getHandleChange(0)}
className={combineClasses( CSS_CLASSES.input, CSS_CLASSES.inputNumber)}
aria-label={'X'}
/>
<input
id={`${field.name}-y`}
type="text"
placeholder={t('Y')}
defaultValue={normalizedValue[1]}
ref={getInputRef(1)}
onChange={getHandleChange(1)}
aria-label={'Y'}
className={combineClasses( CSS_CLASSES.input, CSS_CLASSES.inputNumber)}
/>
</StandardFieldLayout>
);
};
export default React.memo(Point2DInput);
Note: In above implementation, pay attention following rules to make this component works as built-in components
- use
useFieldValidatorto respects the FieldValidationMode specified in Formitiva formconst fieldValidate = useFieldValidator(field, externalError); - Use
useUncontrolledValidatedInputto use uncontrolled input logic in this componentconst { getInputRef, error, getHandleChange } = useUncontrolledValidatedInput<HTMLInputElement, string[]>({
value: normalizedValue, // Normalized value for the hook
count: 2, // two inputs (x and y)
onError,
onChange: handleChange,
validate: handleValidation,
});
Step 2: Using registerComponent to register the component
The registration call shoulbe be happens before use Formitiva form. It can be called globally or in app on mount.
registerComponent("point2d", Point2DInput);
Step 3: Create an example definition using the component
Then in you app, you can define a defintion which use the new type point2d
const rectDef = {
{
"name": "RectangleDefinition",
"displayName": "Dectangle Definition",
"version": "1.0.0",
"properties": [
{
"type": "point2d", // new point2d type
"name": "topLeft",
"displayName": "Top Left",
"defaultValue": [
"0",
"0"
],
"required": true
},
{
"type": "point2d", // new point2d type
"name": "bottomRight",
"displayName": "Bottom Right",
"defaultValue": [
"640",
"480"
],
"required": true
}
]
};
Step 4: Use in app
Finally use the definition in Formitiva form
export default function App() {
return (
<div className="app">
<h2>Custom Component: Point2D</h2>
<Formitiva definitionData={rectDef} />
</div>
);
}
Step 5: Type validation
If the component includes default validation logic, register it using registerFieldTypeValidationHandler.
Within the registered validation handler, validate that the x and y values are valid numbers.
This validation respects the FieldValidationMode and runs before both custom field validation and form-level validation.
registerFieldTypeValidationHandler('point2d', (
field: DefinitionPropertyField,
input: FieldValueType,
t: TranslationFunction) =>
{
void field; // unused
if (!Array.isArray(input) || input.length !== 2) {
return t('Value must be a 2D point array');
}
const [x, y] = input;
const xNum = Number(x);
const yNum = Number(y);
if (!Number.isFinite(xNum)) {
return t('X must be a valid number');
}
if (!Number.isFinite(yNum)) {
return t('Y must be a valid number');
}
return undefined;
});
Example reference
Please check Github custom-component-app