@insticc/genericform
v2.1.3
Published
A generic form with a comprehensive set of field types, validation, and external state management
Maintainers
Readme
@insticc/genericform
A flexible, schema-driven React form component that renders any combination of field types from a plain JavaScript array. No boilerplate, built-in validation, modal support, and full external state control out of the box.
Table of Contents
- @insticc/genericform
Installation
npm install @insticc/genericformImport the component and its stylesheet:
import GenericForm from '@insticc/genericform';Peer Dependencies
The following must be installed in your project:
| Package | Version |
|---|---|
| react | ≥ 17 |
| react-dom | ≥ 17 |
| react-bootstrap | ≥ 2 |
| semantic-ui-react | ≥ 2 |
| react-datetime | ≥ 3 |
Quick Start
import { useState } from 'react';
import GenericForm from '@insticc/genericform';
import '@insticc/genericform/dist/index.css';
const MyForm = () => {
const [result, setResult] = useState(null);
const fields = [
{ name: 'firstName', type: 'text', label: 'First Name', required: true, value: '' },
{ name: 'lastName', type: 'text', label: 'Last Name', required: true, value: '' },
{ name: 'email', type: 'email', label: 'Email', required: true, value: '' },
{ name: 'age', type: 'int', label: 'Age', min: 0, max: 120, value: '' },
{
name: 'country', type: 'select', label: 'Country', value: '',
options: [
{ value: 'PT', text: 'Portugal' },
{ value: 'ES', text: 'Spain' },
{ value: 'FR', text: 'France' },
],
},
];
return (
<GenericForm
title="New User"
fields={fields}
submitText="Save"
onSubmit={(data) => setResult(data)}
onCancel={() => setResult(null)}
/>
);
};The onSubmit callback receives the complete form data object keyed by each field's name.
Field Types
| type value | Rendered as | Key extra props |
|---|---|---|
| text / string | Text input | placeholder, minLength, maxLength |
| email | Email input (format validated) | placeholder |
| password | Password input | placeholder |
| int / number | Number input | min, max, step |
| textArea | Textarea | rows, height, maxLength |
| date | Date picker (DD/MM/YYYY) | placeholder |
| datetime | Date + time picker | placeholder |
| time | Time picker (HH:mm) | placeholder |
| year | Year picker | minYear, maxYear |
| select | Native <select> | options, placeholder, multiple |
| multipleDropdown | Semantic UI multi-select | options, placeholder |
| search | Semantic UI Search | options, isLoading, resultRenderer |
| toggle | Toggle switch | — |
| checkbox | Single checkbox | onOptionChange |
| checkboxGroup | Group of checkboxes | options, grouped, onOptionChange |
| radioGroup | Stacked radio buttons | options, onOptionChange |
| radioGroupInline | Inline radio buttons | options, onOptionChange |
| json | JSON textarea editor | datatype, allowModeSwitch, storeAsString, rows |
| accordion | Collapsible section | sections, defaultOpen, activeAllIndex |
| component | Arbitrary JSX embed | component |
| label | Section heading | — |
| hr / separator | Horizontal rule | — |
Options format
The select, multipleDropdown, checkboxGroup, and radio types all accept options in any of these shapes — the component resolves them automatically:
{ value: 'pt', text: 'Portugal' } // most common
{ id: 'admin', label: 'Admin' }
{ name: 'read', label: 'Read' }Field Schema Reference
Every item in the fields array is a plain object. The properties below apply across all (or most) field types.
Common properties
| Property | Type | Default | Description |
|---|---|---|---|
| name | string | — | Required. Unique key; used as the form data key. |
| type | string | 'text' | Field type (see table above). |
| label / displayName | string \| node | — | Label shown above the field. |
| value | any | — | Initial (or controlled) value. |
| required | bool | false | Adds * indicator and enforces non-empty on submit. |
| disabled | bool | false | Disables the input. |
| placeholder | string | displayName | Input placeholder text. |
| tooltip | string | — | Tooltip shown next to the label. |
| showHelp | bool | false | Show ⓘ help icon. |
| helpText | string | — | Text shown in the help icon tooltip. |
| errorMessage | string | auto | Custom message shown on validation failure. |
| validate | function | — | (value, formData) => string \| null. Return a string to show as error. |
| onChange | function | — | (value, formData) => void. Per-field change callback (in addition to onFieldChange). |
| hide | bool \| function | false | (formData, field) => bool. Hides the field dynamically. |
| inline | bool | false | Renders this field side-by-side with adjacent inline fields. |
| group | string | — | Groups fields under a named section heading. Groups render in alphabetical order, ungrouped fields last. |
Style properties (all optional)
| Property | Type | Description |
|---|---|---|
| style / mainDivStyle | object | Style applied to the outer wrapper <div>. |
| labelStyle | object | Style applied to the <label> element. |
| spanStyle | object | Style applied to the required * span. |
| inputStyle | object | Style applied to the input element. |
| tooltipStyle | object | Style applied to the tooltip span. |
| className | string | Extra CSS class for text/email/password inputs. |
Type-specific properties
int / number
{ name: 'score', type: 'number', min: 0, max: 100, step: 0.5, value: '' }textArea
{ name: 'bio', type: 'textArea', rows: 4, height: '120px', maxLength: 300, value: '' }select
{
name: 'country', type: 'select',
options: [{ value: 'PT', text: 'Portugal' }],
multiple: false, // set true for native multi-select
value: '',
}checkboxGroup
{
name: 'perms', type: 'checkboxGroup',
value: ['read'], // array of selected names
options: [
{ name: 'read', label: 'Read' },
{ name: 'write', label: 'Write' },
{ name: 'delete', label: 'Delete' },
],
grouped: false, // true renders a visual group border
}json
{
name: 'config', type: 'json',
datatype: 'object', // 'string' | 'number' | 'object'
allowModeSwitch: true, // toggle between single / multiple (array) mode
storeAsString: false, // keep raw string in formData instead of parsed value
rows: 6,
value: '',
}accordion
{
name: 'advanced', type: 'accordion',
defaultOpen: false,
activeAllIndex: false, // open all sections by default
sections: [
{ title: 'Config', icon: '⚙️', content: <p>…</p> },
{ title: 'Danger zone', icon: '⚠️', content: <p>…</p> },
],
}component
{
name: 'avatar', type: 'component',
displayName: 'Avatar preview',
component: <MyAvatarWidget />,
}GenericForm Props
| Prop | Type | Default | Description |
|---|---|---|---|
| fields | array | [] | Required. Array of field schema objects. |
| title | string \| node | — | Form heading. |
| titleIcon | node | — | Icon rendered before the title. |
| titleStyle | object | — | Style for the title element. |
| onSubmit | async function | — | (formData) => void. Called after successful validation. |
| handleSubmit | function | — | Legacy: receives the native submit event instead of formData. |
| onCancel | function | — | Called when the Cancel button is clicked. |
| onDataChange | function | — | (formData) => void. Fired on every field change with the full snapshot. |
| onFieldChange | function | — | ({ name, value, option? }) => void. Fired per field change. |
| validateOnChange | bool | true | Validate each field as the user types. |
| validateOnSubmit | bool | true | Validate all fields before calling onSubmit. |
| submitText | string | 'Submit' | Label on the submit button. |
| cancelText | string | 'Cancel' | Label on the cancel button. |
| showSubmit | bool | true | Show the submit button. |
| showCancel | bool | true | Show the cancel button. |
| haveSaveButton | bool | true | Alias for showSubmit. |
| submitButton | object | {} | { className, style, icon } overrides for the submit button. |
| cancelButton | object | {} | { className, style, icon } overrides for the cancel button. |
| customActions | array | [] | Extra buttons in the actions bar (see below). |
| splitForm | bool | false | Render fields in a two-column layout. |
| splitField | string | — | Field name at which to split left/right columns. |
| splitGroup | bool | false | Split at the group boundary instead of a specific field. |
| enableModal | bool | false | Wrap the form in a React Bootstrap <Modal>. |
| modalShow | bool | false | Controls modal visibility. |
| modalSize | string | 'lg' | Bootstrap modal size ('sm', 'lg', 'xl'). |
| disableDefaultContainer | bool | false | Remove the default card/shadow wrapper <div>. |
| containerStyle | object | {} | Style for the outermost container. |
| formStyle | object | — | Style for the <form> element. |
| fieldsStyle | object | — | Style for each fields section wrapper. |
| actionsDivStyle | object | — | Style for the actions bar wrapper. |
| actionsStyle | object | — | Style applied to every action button. |
| handleSearchChange | function | — | Passed directly to the search field type. |
| handleResultSelect | function | — | Passed directly to the search field type. |
| resultRenderer | function | — | Custom result item renderer for the search type. |
customActions
Inject additional buttons into the actions bar:
<GenericForm
fields={fields}
customActions={[
{
label: 'Reset',
icon: <ResetIcon />, // optional
className: 'ufg-button-secondary',
style: {},
disabled: false,
onClick: () => resetMyForm(),
},
]}
/>Validation
Validation runs on change (if validateOnChange) and on submit (if validateOnSubmit). Built-in rules cover the most common cases:
| Rule | Prop | Example |
|---|---|---|
| Required | required: true | Non-empty value |
| Email format | type: 'email' | Must match [email protected] |
| Min length | minLength: 3 | At least 3 characters |
| Max length | maxLength: 20 | At most 20 characters |
| Numeric min | min: 0 | Value ≥ 0 |
| Numeric max | max: 100 | Value ≤ 100 |
| Custom | validate function | Any logic |
Custom validation:
{
name: 'website', type: 'text', value: '',
validate: (value, formData) => {
if (value && !value.startsWith('https://')) return 'Must start with https://';
return null; // no error
},
}Cross-field validation — the second argument is the live form data object:
{
name: 'notifyEmail', type: 'email', value: '',
hide: (fd) => !fd.notify,
validate: (value, fd) =>
fd.notify && !value ? 'Required when notifications are enabled' : null,
}Override the default error message with errorMessage:
{ name: 'name', type: 'text', required: true, errorMessage: 'Please enter your full name.' }Layout & Grouping
Inline fields
Set inline: true on consecutive fields to render them side-by-side:
{ name: 'firstName', type: 'text', label: 'First name', inline: true, value: '' },
{ name: 'lastName', type: 'text', label: 'Last name', inline: true, value: '' },Groups
Assign a group string to organise fields under labelled sections. Groups are sorted alphabetically; ungrouped fields appear last.
{ name: 'email', type: 'email', group: 'Contact', value: '' },
{ name: 'phone', type: 'text', group: 'Contact', value: '' },
{ name: 'country', type: 'select', group: 'Address', value: '', options: [...] },Two-column split
<GenericForm
fields={fields}
splitForm
splitField="address" // everything up to and including 'address' goes left
/>Use splitGroup to split at a group boundary rather than a specific field.
External State Control
GenericForm manages its own internal state, but every field's value prop is watched. When a value changes, the form merges only that field — which means you can drive specific fields from a parent useState without re-initialising the whole form.
The recommended pattern for full external control uses onDataChange:
const [formState, setFormState] = useState({
firstName: '', lastName: '', country: '', city: '',
});
// Auto-fill city when country changes
const handleFieldChange = ({ name, value }) => {
if (name === 'country') {
setFormState(prev => ({ ...prev, city: CITY_BY_COUNTRY[value] ?? '' }));
}
};
const fields = [
{ name: 'firstName', type: 'text', label: 'First name', value: formState.firstName },
{ name: 'lastName', type: 'text', label: 'Last name', value: formState.lastName },
{ name: 'country', type: 'select', label: 'Country', value: formState.country, options: COUNTRIES },
{ name: 'city', type: 'text', label: 'City', value: formState.city, disabled: true },
];
<GenericForm
fields={fields}
onDataChange={setFormState}
onFieldChange={handleFieldChange}
onSubmit={(data) => console.log('Saved:', data)}
/>You can also write directly to formState from outside the form (e.g. a "Load profile" button) — the form will detect the changed value props and update only the affected fields.
Modal Mode
Wrap the form in a React Bootstrap modal with no extra markup:
const [show, setShow] = useState(false);
<GenericForm
title="Edit User"
fields={fields}
enableModal
modalShow={show}
modalSize="lg"
onSubmit={(data) => { saveUser(data); setShow(false); }}
onCancel={() => setShow(false)}
/>Note: Import React Bootstrap's CSS in your app root if you haven't already:
import 'bootstrap/dist/css/bootstrap.min.css';
Theming (CSS Variables)
Override any of the following CSS custom properties to match your design system:
:root {
--ufg-primary: #1d4ed8; /* submit button, active toggle */
--ufg-primary-dark: #1e40af;
--ufg-primary-darker: #1e3a8a;
--ufg-error: #dc2626; /* validation error text */
--ufg-border: #cbd5e1; /* input borders */
--ufg-bg: #ffffff; /* card background */
--ufg-surface: #fafafa; /* secondary surfaces */
--ufg-text: #111111; /* primary text */
--ufg-text-secondary: #6b7280; /* placeholder, help text */
--ufg-radius: 10px; /* input border radius */
--ufg-shadow: 0 10px 30px rgba(0, 0, 0, 0.12);
}Demo Components
Two reference implementations are bundled and exported:
import GenericForm, { Demo, Demo2 } from '@insticc/genericform';
// Every field type, conditional visibility, validation, custom actions:
<Demo />
// Full external state management with live formState sidebar:
<Demo2 />Demo covers all 18 field types, conditional field visibility via hide, cross-field validation, inline layout, accordion, and custom action buttons.
Demo2 demonstrates the onDataChange + onFieldChange pattern for parent-driven state, including dependent field auto-fill (country → city) and a live field-change event log.
License
ISC
