Custom form fields
Release requirement: This guide uses Studio extension APIs that are not present in npm
@flexkit/studio@0.0.31. See package availability before using the example.
A form contribution supplies a React component for a supported input key. For example, contributes.formFields.text replaces the text-field contribution used by attributes with inputType: 'text' in that project.
Form contract
The exported FormFieldProps includes control, fieldSchema, readOnly, setValue, getValues, scope, defaultScope, entity identity, and the schema. The field value is a structured FormFieldValue, not just a scalar.
When updating a value, preserve the existing wrapper and replace its value property. Do not discard scope/default metadata. Respect both readOnly and the field’s inherited/default state. Render an associated label, help text, and validation messages.
Example: a text-field override
Install react-hook-form as a direct dependency compatible with your Studio release. This component preserves the field wrapper and uses the form controller to subscribe to changes:
'use client';
import { useId } from 'react';
import { useController } from 'react-hook-form';
import { DefaultValueSwitch, type FormFieldProps, type FormFieldValue, type StudioExtension } from '@flexkit/studio';
function CatalogText({ control, fieldSchema, readOnly, setValue }: FormFieldProps) {
const id = useId();
const { field, fieldState } = useController({ name: fieldSchema.name, control });
const current = field.value as FormFieldValue | undefined;
return (
<div>
<label htmlFor={id}>{fieldSchema.label}</label>
<input
id={id}
name={field.name}
ref={field.ref}
onBlur={field.onBlur}
value={String(current?.value ?? '')}
disabled={readOnly || current?.disabled}
aria-invalid={Boolean(fieldState.error)}
aria-describedby={fieldState.error ? `${id}-error` : undefined}
onChange={(event) =>
setValue(
fieldSchema.name,
{ ...current, value: event.currentTarget.value },
{ shouldDirty: true, shouldValidate: true }
)
}
/>
<DefaultValueSwitch
checked={current?.disabled ?? false}
disabled={readOnly}
scope={current?.scope}
onChange={(disabled) => setValue(fieldSchema.name, { ...current, disabled }, { shouldDirty: true })}
/>
{fieldState.error ? <p id={`${id}-error`}>{fieldState.error.message}</p> : null}
</div>
);
}
export function CatalogFields(): StudioExtension {
return {
id: 'catalog-fields',
contributes: {
formFields: { text: { component: CatalogText } },
},
};
}Integration steps
- Create the field component against the exported form types and your form library.
- Register it under a supported contribution key.
- Use the matching
inputTypein the entity schema. - Test create, existing edit, empty value, validation, read-only access, and non-default scopes.
- Save and reopen a record to verify persistence.
An override of text affects other text attributes in that project. Inspect fieldSchema if the presentation should vary by attribute. Do not assume every arbitrary input key has a runtime renderer merely because extension configuration accepts a string key.
See custom previews for list rendering and conditional fields for schema-driven visibility.