@insticc/genericform 2.1.2 → 2.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,500 +1,500 @@
1
- # @insticc/genericform
2
-
3
- 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.
4
-
5
- ---
6
-
7
- ## Table of Contents
8
-
9
- - [@insticc/genericform](#insticcgenericform)
10
- - [Table of Contents](#table-of-contents)
11
- - [Installation](#installation)
12
- - [Peer Dependencies](#peer-dependencies)
13
- - [Quick Start](#quick-start)
14
- - [Field Types](#field-types)
15
- - [Options format](#options-format)
16
- - [Field Schema Reference](#field-schema-reference)
17
- - [Common properties](#common-properties)
18
- - [Style properties (all optional)](#style-properties-all-optional)
19
- - [Type-specific properties](#type-specific-properties)
20
- - [GenericForm Props](#genericform-props)
21
- - [`customActions`](#customactions)
22
- - [Validation](#validation)
23
- - [Layout \& Grouping](#layout--grouping)
24
- - [Inline fields](#inline-fields)
25
- - [Groups](#groups)
26
- - [Two-column split](#two-column-split)
27
- - [External State Control](#external-state-control)
28
- - [Modal Mode](#modal-mode)
29
- - [Theming (CSS Variables)](#theming-css-variables)
30
- - [Demo Components](#demo-components)
31
- - [License](#license)
32
-
33
- ---
34
-
35
- ## Installation
36
-
37
- ```bash
38
- npm install @insticc/genericform
39
- ```
40
-
41
- Import the component and its stylesheet:
42
-
43
- ```jsx
44
- import GenericForm from '@insticc/genericform';
45
- ```
46
-
47
- ---
48
-
49
- ## Peer Dependencies
50
-
51
- The following must be installed in your project:
52
-
53
- | Package | Version |
54
- |---|---|
55
- | `react` | ≥ 17 |
56
- | `react-dom` | ≥ 17 |
57
- | `react-bootstrap` | ≥ 2 |
58
- | `semantic-ui-react` | ≥ 2 |
59
- | `react-datetime` | ≥ 3 |
60
-
61
- ---
62
-
63
- ## Quick Start
64
-
65
- ```jsx
66
- import { useState } from 'react';
67
- import GenericForm from '@insticc/genericform';
68
- import '@insticc/genericform/dist/index.css';
69
-
70
- const MyForm = () => {
71
- const [result, setResult] = useState(null);
72
-
73
- const fields = [
74
- { name: 'firstName', type: 'text', label: 'First Name', required: true, value: '' },
75
- { name: 'lastName', type: 'text', label: 'Last Name', required: true, value: '' },
76
- { name: 'email', type: 'email', label: 'Email', required: true, value: '' },
77
- { name: 'age', type: 'int', label: 'Age', min: 0, max: 120, value: '' },
78
- {
79
- name: 'country', type: 'select', label: 'Country', value: '',
80
- options: [
81
- { value: 'PT', text: 'Portugal' },
82
- { value: 'ES', text: 'Spain' },
83
- { value: 'FR', text: 'France' },
84
- ],
85
- },
86
- ];
87
-
88
- return (
89
- <GenericForm
90
- title="New User"
91
- fields={fields}
92
- submitText="Save"
93
- onSubmit={(data) => setResult(data)}
94
- onCancel={() => setResult(null)}
95
- />
96
- );
97
- };
98
- ```
99
-
100
- The `onSubmit` callback receives the complete form data object keyed by each field's `name`.
101
-
102
- ---
103
-
104
- ## Field Types
105
-
106
- | `type` value | Rendered as | Key extra props |
107
- |---|---|---|
108
- | `text` / `string` | Text input | `placeholder`, `minLength`, `maxLength` |
109
- | `email` | Email input (format validated) | `placeholder` |
110
- | `password` | Password input | `placeholder` |
111
- | `int` / `number` | Number input | `min`, `max`, `step` |
112
- | `textArea` | Textarea | `rows`, `height`, `maxLength` |
113
- | `date` | Date picker (DD/MM/YYYY) | `placeholder` |
114
- | `datetime` | Date + time picker | `placeholder` |
115
- | `time` | Time picker (HH:mm) | `placeholder` |
116
- | `year` | Year picker | `minYear`, `maxYear` |
117
- | `select` | Native `<select>` | `options`, `placeholder`, `multiple` |
118
- | `multipleDropdown` | Semantic UI multi-select | `options`, `placeholder` |
119
- | `search` | Semantic UI Search | `options`, `isLoading`, `resultRenderer` |
120
- | `toggle` | Toggle switch | — |
121
- | `checkbox` | Single checkbox | `onOptionChange` |
122
- | `checkboxGroup` | Group of checkboxes | `options`, `grouped`, `onOptionChange` |
123
- | `radioGroup` | Stacked radio buttons | `options`, `onOptionChange` |
124
- | `radioGroupInline` | Inline radio buttons | `options`, `onOptionChange` |
125
- | `json` | JSON textarea editor | `datatype`, `allowModeSwitch`, `storeAsString`, `rows` |
126
- | `accordion` | Collapsible section | `sections`, `defaultOpen`, `activeAllIndex` |
127
- | `component` | Arbitrary JSX embed | `component` |
128
- | `label` | Section heading | — |
129
- | `hr` / `separator` | Horizontal rule | — |
130
-
131
- ### Options format
132
-
133
- The `select`, `multipleDropdown`, `checkboxGroup`, and radio types all accept options in any of these shapes — the component resolves them automatically:
134
-
135
- ```js
136
- { value: 'pt', text: 'Portugal' } // most common
137
- { id: 'admin', label: 'Admin' }
138
- { name: 'read', label: 'Read' }
139
- ```
140
-
141
- ---
142
-
143
- ## Field Schema Reference
144
-
145
- Every item in the `fields` array is a plain object. The properties below apply across all (or most) field types.
146
-
147
- ### Common properties
148
-
149
- | Property | Type | Default | Description |
150
- |---|---|---|---|
151
- | `name` | `string` | — | **Required.** Unique key; used as the form data key. |
152
- | `type` | `string` | `'text'` | Field type (see table above). |
153
- | `label` / `displayName` | `string \| node` | — | Label shown above the field. |
154
- | `value` | `any` | — | Initial (or controlled) value. |
155
- | `required` | `bool` | `false` | Adds `*` indicator and enforces non-empty on submit. |
156
- | `disabled` | `bool` | `false` | Disables the input. |
157
- | `placeholder` | `string` | `displayName` | Input placeholder text. |
158
- | `tooltip` | `string` | — | Tooltip shown next to the label. |
159
- | `showHelp` | `bool` | `false` | Show ⓘ help icon. |
160
- | `helpText` | `string` | — | Text shown in the help icon tooltip. |
161
- | `errorMessage` | `string` | auto | Custom message shown on validation failure. |
162
- | `validate` | `function` | — | `(value, formData) => string \| null`. Return a string to show as error. |
163
- | `onChange` | `function` | — | `(value, formData) => void`. Per-field change callback (in addition to `onFieldChange`). |
164
- | `hide` | `bool \| function` | `false` | `(formData, field) => bool`. Hides the field dynamically. |
165
- | `inline` | `bool` | `false` | Renders this field side-by-side with adjacent `inline` fields. |
166
- | `group` | `string` | — | Groups fields under a named section heading. Groups render in alphabetical order, ungrouped fields last. |
167
-
168
- ### Style properties (all optional)
169
-
170
- | Property | Type | Description |
171
- |---|---|---|
172
- | `style` / `mainDivStyle` | `object` | Style applied to the outer wrapper `<div>`. |
173
- | `labelStyle` | `object` | Style applied to the `<label>` element. |
174
- | `spanStyle` | `object` | Style applied to the required `*` span. |
175
- | `inputStyle` | `object` | Style applied to the input element. |
176
- | `tooltipStyle` | `object` | Style applied to the tooltip span. |
177
- | `className` | `string` | Extra CSS class for text/email/password inputs. |
178
-
179
- ### Type-specific properties
180
-
181
- **`int` / `number`**
182
- ```js
183
- { name: 'score', type: 'number', min: 0, max: 100, step: 0.5, value: '' }
184
- ```
185
-
186
- **`textArea`**
187
- ```js
188
- { name: 'bio', type: 'textArea', rows: 4, height: '120px', maxLength: 300, value: '' }
189
- ```
190
-
191
- **`select`**
192
- ```js
193
- {
194
- name: 'country', type: 'select',
195
- options: [{ value: 'PT', text: 'Portugal' }],
196
- multiple: false, // set true for native multi-select
197
- value: '',
198
- }
199
- ```
200
-
201
- **`checkboxGroup`**
202
- ```js
203
- {
204
- name: 'perms', type: 'checkboxGroup',
205
- value: ['read'], // array of selected names
206
- options: [
207
- { name: 'read', label: 'Read' },
208
- { name: 'write', label: 'Write' },
209
- { name: 'delete', label: 'Delete' },
210
- ],
211
- grouped: false, // true renders a visual group border
212
- }
213
- ```
214
-
215
- **`json`**
216
- ```js
217
- {
218
- name: 'config', type: 'json',
219
- datatype: 'object', // 'string' | 'number' | 'object'
220
- allowModeSwitch: true, // toggle between single / multiple (array) mode
221
- storeAsString: false, // keep raw string in formData instead of parsed value
222
- rows: 6,
223
- value: '',
224
- }
225
- ```
226
-
227
- **`accordion`**
228
- ```js
229
- {
230
- name: 'advanced', type: 'accordion',
231
- defaultOpen: false,
232
- activeAllIndex: false, // open all sections by default
233
- sections: [
234
- { title: 'Config', icon: '⚙️', content: <p>…</p> },
235
- { title: 'Danger zone', icon: '⚠️', content: <p>…</p> },
236
- ],
237
- }
238
- ```
239
-
240
- **`component`**
241
- ```js
242
- {
243
- name: 'avatar', type: 'component',
244
- displayName: 'Avatar preview',
245
- component: <MyAvatarWidget />,
246
- }
247
- ```
248
-
249
- ---
250
-
251
- ## GenericForm Props
252
-
253
- | Prop | Type | Default | Description |
254
- |---|---|---|---|
255
- | `fields` | `array` | `[]` | **Required.** Array of field schema objects. |
256
- | `title` | `string \| node` | — | Form heading. |
257
- | `titleIcon` | `node` | — | Icon rendered before the title. |
258
- | `titleStyle` | `object` | — | Style for the title element. |
259
- | `onSubmit` | `async function` | — | `(formData) => void`. Called after successful validation. |
260
- | `handleSubmit` | `function` | — | Legacy: receives the native submit event instead of formData. |
261
- | `onCancel` | `function` | — | Called when the Cancel button is clicked. |
262
- | `onDataChange` | `function` | — | `(formData) => void`. Fired on every field change with the full snapshot. |
263
- | `onFieldChange` | `function` | — | `({ name, value, option? }) => void`. Fired per field change. |
264
- | `validateOnChange` | `bool` | `true` | Validate each field as the user types. |
265
- | `validateOnSubmit` | `bool` | `true` | Validate all fields before calling `onSubmit`. |
266
- | `submitText` | `string` | `'Submit'` | Label on the submit button. |
267
- | `cancelText` | `string` | `'Cancel'` | Label on the cancel button. |
268
- | `showSubmit` | `bool` | `true` | Show the submit button. |
269
- | `showCancel` | `bool` | `true` | Show the cancel button. |
270
- | `haveSaveButton` | `bool` | `true` | Alias for `showSubmit`. |
271
- | `submitButton` | `object` | `{}` | `{ className, style, icon }` overrides for the submit button. |
272
- | `cancelButton` | `object` | `{}` | `{ className, style, icon }` overrides for the cancel button. |
273
- | `customActions` | `array` | `[]` | Extra buttons in the actions bar (see below). |
274
- | `splitForm` | `bool` | `false` | Render fields in a two-column layout. |
275
- | `splitField` | `string` | — | Field `name` at which to split left/right columns. |
276
- | `splitGroup` | `bool` | `false` | Split at the group boundary instead of a specific field. |
277
- | `enableModal` | `bool` | `false` | Wrap the form in a React Bootstrap `<Modal>`. |
278
- | `modalShow` | `bool` | `false` | Controls modal visibility. |
279
- | `modalSize` | `string` | `'lg'` | Bootstrap modal size (`'sm'`, `'lg'`, `'xl'`). |
280
- | `disableDefaultContainer` | `bool` | `false` | Remove the default card/shadow wrapper `<div>`. |
281
- | `containerStyle` | `object` | `{}` | Style for the outermost container. |
282
- | `formStyle` | `object` | — | Style for the `<form>` element. |
283
- | `fieldsStyle` | `object` | — | Style for each fields section wrapper. |
284
- | `actionsDivStyle` | `object` | — | Style for the actions bar wrapper. |
285
- | `actionsStyle` | `object` | — | Style applied to every action button. |
286
- | `handleSearchChange` | `function` | — | Passed directly to the `search` field type. |
287
- | `handleResultSelect` | `function` | — | Passed directly to the `search` field type. |
288
- | `resultRenderer` | `function` | — | Custom result item renderer for the `search` type. |
289
-
290
- ### `customActions`
291
-
292
- Inject additional buttons into the actions bar:
293
-
294
- ```jsx
295
- <GenericForm
296
- fields={fields}
297
- customActions={[
298
- {
299
- label: 'Reset',
300
- icon: <ResetIcon />, // optional
301
- className: 'ufg-button-secondary',
302
- style: {},
303
- disabled: false,
304
- onClick: () => resetMyForm(),
305
- },
306
- ]}
307
- />
308
- ```
309
-
310
- ---
311
-
312
- ## Validation
313
-
314
- Validation runs on change (if `validateOnChange`) and on submit (if `validateOnSubmit`). Built-in rules cover the most common cases:
315
-
316
- | Rule | Prop | Example |
317
- |---|---|---|
318
- | Required | `required: true` | Non-empty value |
319
- | Email format | `type: 'email'` | Must match `x@x.x` |
320
- | Min length | `minLength: 3` | At least 3 characters |
321
- | Max length | `maxLength: 20` | At most 20 characters |
322
- | Numeric min | `min: 0` | Value ≥ 0 |
323
- | Numeric max | `max: 100` | Value ≤ 100 |
324
- | Custom | `validate` function | Any logic |
325
-
326
- **Custom validation:**
327
-
328
- ```js
329
- {
330
- name: 'website', type: 'text', value: '',
331
- validate: (value, formData) => {
332
- if (value && !value.startsWith('https://')) return 'Must start with https://';
333
- return null; // no error
334
- },
335
- }
336
- ```
337
-
338
- **Cross-field validation** — the second argument is the live form data object:
339
-
340
- ```js
341
- {
342
- name: 'notifyEmail', type: 'email', value: '',
343
- hide: (fd) => !fd.notify,
344
- validate: (value, fd) =>
345
- fd.notify && !value ? 'Required when notifications are enabled' : null,
346
- }
347
- ```
348
-
349
- Override the default error message with `errorMessage`:
350
-
351
- ```js
352
- { name: 'name', type: 'text', required: true, errorMessage: 'Please enter your full name.' }
353
- ```
354
-
355
- ---
356
-
357
- ## Layout & Grouping
358
-
359
- ### Inline fields
360
-
361
- Set `inline: true` on consecutive fields to render them side-by-side:
362
-
363
- ```js
364
- { name: 'firstName', type: 'text', label: 'First name', inline: true, value: '' },
365
- { name: 'lastName', type: 'text', label: 'Last name', inline: true, value: '' },
366
- ```
367
-
368
- ### Groups
369
-
370
- Assign a `group` string to organise fields under labelled sections. Groups are sorted alphabetically; ungrouped fields appear last.
371
-
372
- ```js
373
- { name: 'email', type: 'email', group: 'Contact', value: '' },
374
- { name: 'phone', type: 'text', group: 'Contact', value: '' },
375
- { name: 'country', type: 'select', group: 'Address', value: '', options: [...] },
376
- ```
377
-
378
- ### Two-column split
379
-
380
- ```jsx
381
- <GenericForm
382
- fields={fields}
383
- splitForm
384
- splitField="address" // everything up to and including 'address' goes left
385
- />
386
- ```
387
-
388
- Use `splitGroup` to split at a group boundary rather than a specific field.
389
-
390
- ---
391
-
392
- ## External State Control
393
-
394
- `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.
395
-
396
- The recommended pattern for full external control uses `onDataChange`:
397
-
398
- ```jsx
399
- const [formState, setFormState] = useState({
400
- firstName: '', lastName: '', country: '', city: '',
401
- });
402
-
403
- // Auto-fill city when country changes
404
- const handleFieldChange = ({ name, value }) => {
405
- if (name === 'country') {
406
- setFormState(prev => ({ ...prev, city: CITY_BY_COUNTRY[value] ?? '' }));
407
- }
408
- };
409
-
410
- const fields = [
411
- { name: 'firstName', type: 'text', label: 'First name', value: formState.firstName },
412
- { name: 'lastName', type: 'text', label: 'Last name', value: formState.lastName },
413
- { name: 'country', type: 'select', label: 'Country', value: formState.country, options: COUNTRIES },
414
- { name: 'city', type: 'text', label: 'City', value: formState.city, disabled: true },
415
- ];
416
-
417
- <GenericForm
418
- fields={fields}
419
- onDataChange={setFormState}
420
- onFieldChange={handleFieldChange}
421
- onSubmit={(data) => console.log('Saved:', data)}
422
- />
423
- ```
424
-
425
- 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.
426
-
427
- ---
428
-
429
- ## Modal Mode
430
-
431
- Wrap the form in a React Bootstrap modal with no extra markup:
432
-
433
- ```jsx
434
- const [show, setShow] = useState(false);
435
-
436
- <GenericForm
437
- title="Edit User"
438
- fields={fields}
439
- enableModal
440
- modalShow={show}
441
- modalSize="lg"
442
- onSubmit={(data) => { saveUser(data); setShow(false); }}
443
- onCancel={() => setShow(false)}
444
- />
445
- ```
446
-
447
- > **Note:** Import React Bootstrap's CSS in your app root if you haven't already:
448
- > ```js
449
- > import 'bootstrap/dist/css/bootstrap.min.css';
450
- > ```
451
-
452
- ---
453
-
454
- ## Theming (CSS Variables)
455
-
456
- Override any of the following CSS custom properties to match your design system:
457
-
458
- ```css
459
- :root {
460
- --ufg-primary: #1d4ed8; /* submit button, active toggle */
461
- --ufg-primary-dark: #1e40af;
462
- --ufg-primary-darker: #1e3a8a;
463
- --ufg-error: #dc2626; /* validation error text */
464
- --ufg-border: #cbd5e1; /* input borders */
465
- --ufg-bg: #ffffff; /* card background */
466
- --ufg-surface: #fafafa; /* secondary surfaces */
467
- --ufg-text: #111111; /* primary text */
468
- --ufg-text-secondary: #6b7280; /* placeholder, help text */
469
- --ufg-radius: 10px; /* input border radius */
470
- --ufg-shadow: 0 10px 30px rgba(0, 0, 0, 0.12);
471
- }
472
- ```
473
-
474
- ---
475
-
476
- ## Demo Components
477
-
478
- Two reference implementations are bundled and exported:
479
-
480
- ```jsx
481
- import GenericForm, { Demo, Demo2 } from '@insticc/genericform';
482
-
483
- // Every field type, conditional visibility, validation, custom actions:
484
- <Demo />
485
-
486
- // Full external state management with live formState sidebar:
487
- <Demo2 />
488
- ```
489
-
490
- `Demo` covers all 18 field types, conditional field visibility via `hide`, cross-field validation, inline layout, accordion, and custom action buttons.
491
-
492
- `Demo2` demonstrates the `onDataChange` + `onFieldChange` pattern for parent-driven state, including dependent field auto-fill (country → city) and a live field-change event log.
493
-
494
- ---
495
-
496
- ## License
497
-
498
- ISC
499
-
1
+ # @insticc/genericform
2
+
3
+ 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.
4
+
5
+ ---
6
+
7
+ ## Table of Contents
8
+
9
+ - [@insticc/genericform](#insticcgenericform)
10
+ - [Table of Contents](#table-of-contents)
11
+ - [Installation](#installation)
12
+ - [Peer Dependencies](#peer-dependencies)
13
+ - [Quick Start](#quick-start)
14
+ - [Field Types](#field-types)
15
+ - [Options format](#options-format)
16
+ - [Field Schema Reference](#field-schema-reference)
17
+ - [Common properties](#common-properties)
18
+ - [Style properties (all optional)](#style-properties-all-optional)
19
+ - [Type-specific properties](#type-specific-properties)
20
+ - [GenericForm Props](#genericform-props)
21
+ - [`customActions`](#customactions)
22
+ - [Validation](#validation)
23
+ - [Layout \& Grouping](#layout--grouping)
24
+ - [Inline fields](#inline-fields)
25
+ - [Groups](#groups)
26
+ - [Two-column split](#two-column-split)
27
+ - [External State Control](#external-state-control)
28
+ - [Modal Mode](#modal-mode)
29
+ - [Theming (CSS Variables)](#theming-css-variables)
30
+ - [Demo Components](#demo-components)
31
+ - [License](#license)
32
+
33
+ ---
34
+
35
+ ## Installation
36
+
37
+ ```bash
38
+ npm install @insticc/genericform
39
+ ```
40
+
41
+ Import the component and its stylesheet:
42
+
43
+ ```jsx
44
+ import GenericForm from '@insticc/genericform';
45
+ ```
46
+
47
+ ---
48
+
49
+ ## Peer Dependencies
50
+
51
+ The following must be installed in your project:
52
+
53
+ | Package | Version |
54
+ |---|---|
55
+ | `react` | ≥ 17 |
56
+ | `react-dom` | ≥ 17 |
57
+ | `react-bootstrap` | ≥ 2 |
58
+ | `semantic-ui-react` | ≥ 2 |
59
+ | `react-datetime` | ≥ 3 |
60
+
61
+ ---
62
+
63
+ ## Quick Start
64
+
65
+ ```jsx
66
+ import { useState } from 'react';
67
+ import GenericForm from '@insticc/genericform';
68
+ import '@insticc/genericform/dist/index.css';
69
+
70
+ const MyForm = () => {
71
+ const [result, setResult] = useState(null);
72
+
73
+ const fields = [
74
+ { name: 'firstName', type: 'text', label: 'First Name', required: true, value: '' },
75
+ { name: 'lastName', type: 'text', label: 'Last Name', required: true, value: '' },
76
+ { name: 'email', type: 'email', label: 'Email', required: true, value: '' },
77
+ { name: 'age', type: 'int', label: 'Age', min: 0, max: 120, value: '' },
78
+ {
79
+ name: 'country', type: 'select', label: 'Country', value: '',
80
+ options: [
81
+ { value: 'PT', text: 'Portugal' },
82
+ { value: 'ES', text: 'Spain' },
83
+ { value: 'FR', text: 'France' },
84
+ ],
85
+ },
86
+ ];
87
+
88
+ return (
89
+ <GenericForm
90
+ title="New User"
91
+ fields={fields}
92
+ submitText="Save"
93
+ onSubmit={(data) => setResult(data)}
94
+ onCancel={() => setResult(null)}
95
+ />
96
+ );
97
+ };
98
+ ```
99
+
100
+ The `onSubmit` callback receives the complete form data object keyed by each field's `name`.
101
+
102
+ ---
103
+
104
+ ## Field Types
105
+
106
+ | `type` value | Rendered as | Key extra props |
107
+ |---|---|---|
108
+ | `text` / `string` | Text input | `placeholder`, `minLength`, `maxLength` |
109
+ | `email` | Email input (format validated) | `placeholder` |
110
+ | `password` | Password input | `placeholder` |
111
+ | `int` / `number` | Number input | `min`, `max`, `step` |
112
+ | `textArea` | Textarea | `rows`, `height`, `maxLength` |
113
+ | `date` | Date picker (DD/MM/YYYY) | `placeholder` |
114
+ | `datetime` | Date + time picker | `placeholder` |
115
+ | `time` | Time picker (HH:mm) | `placeholder` |
116
+ | `year` | Year picker | `minYear`, `maxYear` |
117
+ | `select` | Native `<select>` | `options`, `placeholder`, `multiple` |
118
+ | `multipleDropdown` | Semantic UI multi-select | `options`, `placeholder` |
119
+ | `search` | Semantic UI Search | `options`, `isLoading`, `resultRenderer` |
120
+ | `toggle` | Toggle switch | — |
121
+ | `checkbox` | Single checkbox | `onOptionChange` |
122
+ | `checkboxGroup` | Group of checkboxes | `options`, `grouped`, `onOptionChange` |
123
+ | `radioGroup` | Stacked radio buttons | `options`, `onOptionChange` |
124
+ | `radioGroupInline` | Inline radio buttons | `options`, `onOptionChange` |
125
+ | `json` | JSON textarea editor | `datatype`, `allowModeSwitch`, `storeAsString`, `rows` |
126
+ | `accordion` | Collapsible section | `sections`, `defaultOpen`, `activeAllIndex` |
127
+ | `component` | Arbitrary JSX embed | `component` |
128
+ | `label` | Section heading | — |
129
+ | `hr` / `separator` | Horizontal rule | — |
130
+
131
+ ### Options format
132
+
133
+ The `select`, `multipleDropdown`, `checkboxGroup`, and radio types all accept options in any of these shapes — the component resolves them automatically:
134
+
135
+ ```js
136
+ { value: 'pt', text: 'Portugal' } // most common
137
+ { id: 'admin', label: 'Admin' }
138
+ { name: 'read', label: 'Read' }
139
+ ```
140
+
141
+ ---
142
+
143
+ ## Field Schema Reference
144
+
145
+ Every item in the `fields` array is a plain object. The properties below apply across all (or most) field types.
146
+
147
+ ### Common properties
148
+
149
+ | Property | Type | Default | Description |
150
+ |---|---|---|---|
151
+ | `name` | `string` | — | **Required.** Unique key; used as the form data key. |
152
+ | `type` | `string` | `'text'` | Field type (see table above). |
153
+ | `label` / `displayName` | `string \| node` | — | Label shown above the field. |
154
+ | `value` | `any` | — | Initial (or controlled) value. |
155
+ | `required` | `bool` | `false` | Adds `*` indicator and enforces non-empty on submit. |
156
+ | `disabled` | `bool` | `false` | Disables the input. |
157
+ | `placeholder` | `string` | `displayName` | Input placeholder text. |
158
+ | `tooltip` | `string` | — | Tooltip shown next to the label. |
159
+ | `showHelp` | `bool` | `false` | Show ⓘ help icon. |
160
+ | `helpText` | `string` | — | Text shown in the help icon tooltip. |
161
+ | `errorMessage` | `string` | auto | Custom message shown on validation failure. |
162
+ | `validate` | `function` | — | `(value, formData) => string \| null`. Return a string to show as error. |
163
+ | `onChange` | `function` | — | `(value, formData) => void`. Per-field change callback (in addition to `onFieldChange`). |
164
+ | `hide` | `bool \| function` | `false` | `(formData, field) => bool`. Hides the field dynamically. |
165
+ | `inline` | `bool` | `false` | Renders this field side-by-side with adjacent `inline` fields. |
166
+ | `group` | `string` | — | Groups fields under a named section heading. Groups render in alphabetical order, ungrouped fields last. |
167
+
168
+ ### Style properties (all optional)
169
+
170
+ | Property | Type | Description |
171
+ |---|---|---|
172
+ | `style` / `mainDivStyle` | `object` | Style applied to the outer wrapper `<div>`. |
173
+ | `labelStyle` | `object` | Style applied to the `<label>` element. |
174
+ | `spanStyle` | `object` | Style applied to the required `*` span. |
175
+ | `inputStyle` | `object` | Style applied to the input element. |
176
+ | `tooltipStyle` | `object` | Style applied to the tooltip span. |
177
+ | `className` | `string` | Extra CSS class for text/email/password inputs. |
178
+
179
+ ### Type-specific properties
180
+
181
+ **`int` / `number`**
182
+ ```js
183
+ { name: 'score', type: 'number', min: 0, max: 100, step: 0.5, value: '' }
184
+ ```
185
+
186
+ **`textArea`**
187
+ ```js
188
+ { name: 'bio', type: 'textArea', rows: 4, height: '120px', maxLength: 300, value: '' }
189
+ ```
190
+
191
+ **`select`**
192
+ ```js
193
+ {
194
+ name: 'country', type: 'select',
195
+ options: [{ value: 'PT', text: 'Portugal' }],
196
+ multiple: false, // set true for native multi-select
197
+ value: '',
198
+ }
199
+ ```
200
+
201
+ **`checkboxGroup`**
202
+ ```js
203
+ {
204
+ name: 'perms', type: 'checkboxGroup',
205
+ value: ['read'], // array of selected names
206
+ options: [
207
+ { name: 'read', label: 'Read' },
208
+ { name: 'write', label: 'Write' },
209
+ { name: 'delete', label: 'Delete' },
210
+ ],
211
+ grouped: false, // true renders a visual group border
212
+ }
213
+ ```
214
+
215
+ **`json`**
216
+ ```js
217
+ {
218
+ name: 'config', type: 'json',
219
+ datatype: 'object', // 'string' | 'number' | 'object'
220
+ allowModeSwitch: true, // toggle between single / multiple (array) mode
221
+ storeAsString: false, // keep raw string in formData instead of parsed value
222
+ rows: 6,
223
+ value: '',
224
+ }
225
+ ```
226
+
227
+ **`accordion`**
228
+ ```js
229
+ {
230
+ name: 'advanced', type: 'accordion',
231
+ defaultOpen: false,
232
+ activeAllIndex: false, // open all sections by default
233
+ sections: [
234
+ { title: 'Config', icon: '⚙️', content: <p>…</p> },
235
+ { title: 'Danger zone', icon: '⚠️', content: <p>…</p> },
236
+ ],
237
+ }
238
+ ```
239
+
240
+ **`component`**
241
+ ```js
242
+ {
243
+ name: 'avatar', type: 'component',
244
+ displayName: 'Avatar preview',
245
+ component: <MyAvatarWidget />,
246
+ }
247
+ ```
248
+
249
+ ---
250
+
251
+ ## GenericForm Props
252
+
253
+ | Prop | Type | Default | Description |
254
+ |---|---|---|---|
255
+ | `fields` | `array` | `[]` | **Required.** Array of field schema objects. |
256
+ | `title` | `string \| node` | — | Form heading. |
257
+ | `titleIcon` | `node` | — | Icon rendered before the title. |
258
+ | `titleStyle` | `object` | — | Style for the title element. |
259
+ | `onSubmit` | `async function` | — | `(formData) => void`. Called after successful validation. |
260
+ | `handleSubmit` | `function` | — | Legacy: receives the native submit event instead of formData. |
261
+ | `onCancel` | `function` | — | Called when the Cancel button is clicked. |
262
+ | `onDataChange` | `function` | — | `(formData) => void`. Fired on every field change with the full snapshot. |
263
+ | `onFieldChange` | `function` | — | `({ name, value, option? }) => void`. Fired per field change. |
264
+ | `validateOnChange` | `bool` | `true` | Validate each field as the user types. |
265
+ | `validateOnSubmit` | `bool` | `true` | Validate all fields before calling `onSubmit`. |
266
+ | `submitText` | `string` | `'Submit'` | Label on the submit button. |
267
+ | `cancelText` | `string` | `'Cancel'` | Label on the cancel button. |
268
+ | `showSubmit` | `bool` | `true` | Show the submit button. |
269
+ | `showCancel` | `bool` | `true` | Show the cancel button. |
270
+ | `haveSaveButton` | `bool` | `true` | Alias for `showSubmit`. |
271
+ | `submitButton` | `object` | `{}` | `{ className, style, icon }` overrides for the submit button. |
272
+ | `cancelButton` | `object` | `{}` | `{ className, style, icon }` overrides for the cancel button. |
273
+ | `customActions` | `array` | `[]` | Extra buttons in the actions bar (see below). |
274
+ | `splitForm` | `bool` | `false` | Render fields in a two-column layout. |
275
+ | `splitField` | `string` | — | Field `name` at which to split left/right columns. |
276
+ | `splitGroup` | `bool` | `false` | Split at the group boundary instead of a specific field. |
277
+ | `enableModal` | `bool` | `false` | Wrap the form in a React Bootstrap `<Modal>`. |
278
+ | `modalShow` | `bool` | `false` | Controls modal visibility. |
279
+ | `modalSize` | `string` | `'lg'` | Bootstrap modal size (`'sm'`, `'lg'`, `'xl'`). |
280
+ | `disableDefaultContainer` | `bool` | `false` | Remove the default card/shadow wrapper `<div>`. |
281
+ | `containerStyle` | `object` | `{}` | Style for the outermost container. |
282
+ | `formStyle` | `object` | — | Style for the `<form>` element. |
283
+ | `fieldsStyle` | `object` | — | Style for each fields section wrapper. |
284
+ | `actionsDivStyle` | `object` | — | Style for the actions bar wrapper. |
285
+ | `actionsStyle` | `object` | — | Style applied to every action button. |
286
+ | `handleSearchChange` | `function` | — | Passed directly to the `search` field type. |
287
+ | `handleResultSelect` | `function` | — | Passed directly to the `search` field type. |
288
+ | `resultRenderer` | `function` | — | Custom result item renderer for the `search` type. |
289
+
290
+ ### `customActions`
291
+
292
+ Inject additional buttons into the actions bar:
293
+
294
+ ```jsx
295
+ <GenericForm
296
+ fields={fields}
297
+ customActions={[
298
+ {
299
+ label: 'Reset',
300
+ icon: <ResetIcon />, // optional
301
+ className: 'ufg-button-secondary',
302
+ style: {},
303
+ disabled: false,
304
+ onClick: () => resetMyForm(),
305
+ },
306
+ ]}
307
+ />
308
+ ```
309
+
310
+ ---
311
+
312
+ ## Validation
313
+
314
+ Validation runs on change (if `validateOnChange`) and on submit (if `validateOnSubmit`). Built-in rules cover the most common cases:
315
+
316
+ | Rule | Prop | Example |
317
+ |---|---|---|
318
+ | Required | `required: true` | Non-empty value |
319
+ | Email format | `type: 'email'` | Must match `x@x.x` |
320
+ | Min length | `minLength: 3` | At least 3 characters |
321
+ | Max length | `maxLength: 20` | At most 20 characters |
322
+ | Numeric min | `min: 0` | Value ≥ 0 |
323
+ | Numeric max | `max: 100` | Value ≤ 100 |
324
+ | Custom | `validate` function | Any logic |
325
+
326
+ **Custom validation:**
327
+
328
+ ```js
329
+ {
330
+ name: 'website', type: 'text', value: '',
331
+ validate: (value, formData) => {
332
+ if (value && !value.startsWith('https://')) return 'Must start with https://';
333
+ return null; // no error
334
+ },
335
+ }
336
+ ```
337
+
338
+ **Cross-field validation** — the second argument is the live form data object:
339
+
340
+ ```js
341
+ {
342
+ name: 'notifyEmail', type: 'email', value: '',
343
+ hide: (fd) => !fd.notify,
344
+ validate: (value, fd) =>
345
+ fd.notify && !value ? 'Required when notifications are enabled' : null,
346
+ }
347
+ ```
348
+
349
+ Override the default error message with `errorMessage`:
350
+
351
+ ```js
352
+ { name: 'name', type: 'text', required: true, errorMessage: 'Please enter your full name.' }
353
+ ```
354
+
355
+ ---
356
+
357
+ ## Layout & Grouping
358
+
359
+ ### Inline fields
360
+
361
+ Set `inline: true` on consecutive fields to render them side-by-side:
362
+
363
+ ```js
364
+ { name: 'firstName', type: 'text', label: 'First name', inline: true, value: '' },
365
+ { name: 'lastName', type: 'text', label: 'Last name', inline: true, value: '' },
366
+ ```
367
+
368
+ ### Groups
369
+
370
+ Assign a `group` string to organise fields under labelled sections. Groups are sorted alphabetically; ungrouped fields appear last.
371
+
372
+ ```js
373
+ { name: 'email', type: 'email', group: 'Contact', value: '' },
374
+ { name: 'phone', type: 'text', group: 'Contact', value: '' },
375
+ { name: 'country', type: 'select', group: 'Address', value: '', options: [...] },
376
+ ```
377
+
378
+ ### Two-column split
379
+
380
+ ```jsx
381
+ <GenericForm
382
+ fields={fields}
383
+ splitForm
384
+ splitField="address" // everything up to and including 'address' goes left
385
+ />
386
+ ```
387
+
388
+ Use `splitGroup` to split at a group boundary rather than a specific field.
389
+
390
+ ---
391
+
392
+ ## External State Control
393
+
394
+ `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.
395
+
396
+ The recommended pattern for full external control uses `onDataChange`:
397
+
398
+ ```jsx
399
+ const [formState, setFormState] = useState({
400
+ firstName: '', lastName: '', country: '', city: '',
401
+ });
402
+
403
+ // Auto-fill city when country changes
404
+ const handleFieldChange = ({ name, value }) => {
405
+ if (name === 'country') {
406
+ setFormState(prev => ({ ...prev, city: CITY_BY_COUNTRY[value] ?? '' }));
407
+ }
408
+ };
409
+
410
+ const fields = [
411
+ { name: 'firstName', type: 'text', label: 'First name', value: formState.firstName },
412
+ { name: 'lastName', type: 'text', label: 'Last name', value: formState.lastName },
413
+ { name: 'country', type: 'select', label: 'Country', value: formState.country, options: COUNTRIES },
414
+ { name: 'city', type: 'text', label: 'City', value: formState.city, disabled: true },
415
+ ];
416
+
417
+ <GenericForm
418
+ fields={fields}
419
+ onDataChange={setFormState}
420
+ onFieldChange={handleFieldChange}
421
+ onSubmit={(data) => console.log('Saved:', data)}
422
+ />
423
+ ```
424
+
425
+ 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.
426
+
427
+ ---
428
+
429
+ ## Modal Mode
430
+
431
+ Wrap the form in a React Bootstrap modal with no extra markup:
432
+
433
+ ```jsx
434
+ const [show, setShow] = useState(false);
435
+
436
+ <GenericForm
437
+ title="Edit User"
438
+ fields={fields}
439
+ enableModal
440
+ modalShow={show}
441
+ modalSize="lg"
442
+ onSubmit={(data) => { saveUser(data); setShow(false); }}
443
+ onCancel={() => setShow(false)}
444
+ />
445
+ ```
446
+
447
+ > **Note:** Import React Bootstrap's CSS in your app root if you haven't already:
448
+ > ```js
449
+ > import 'bootstrap/dist/css/bootstrap.min.css';
450
+ > ```
451
+
452
+ ---
453
+
454
+ ## Theming (CSS Variables)
455
+
456
+ Override any of the following CSS custom properties to match your design system:
457
+
458
+ ```css
459
+ :root {
460
+ --ufg-primary: #1d4ed8; /* submit button, active toggle */
461
+ --ufg-primary-dark: #1e40af;
462
+ --ufg-primary-darker: #1e3a8a;
463
+ --ufg-error: #dc2626; /* validation error text */
464
+ --ufg-border: #cbd5e1; /* input borders */
465
+ --ufg-bg: #ffffff; /* card background */
466
+ --ufg-surface: #fafafa; /* secondary surfaces */
467
+ --ufg-text: #111111; /* primary text */
468
+ --ufg-text-secondary: #6b7280; /* placeholder, help text */
469
+ --ufg-radius: 10px; /* input border radius */
470
+ --ufg-shadow: 0 10px 30px rgba(0, 0, 0, 0.12);
471
+ }
472
+ ```
473
+
474
+ ---
475
+
476
+ ## Demo Components
477
+
478
+ Two reference implementations are bundled and exported:
479
+
480
+ ```jsx
481
+ import GenericForm, { Demo, Demo2 } from '@insticc/genericform';
482
+
483
+ // Every field type, conditional visibility, validation, custom actions:
484
+ <Demo />
485
+
486
+ // Full external state management with live formState sidebar:
487
+ <Demo2 />
488
+ ```
489
+
490
+ `Demo` covers all 18 field types, conditional field visibility via `hide`, cross-field validation, inline layout, accordion, and custom action buttons.
491
+
492
+ `Demo2` demonstrates the `onDataChange` + `onFieldChange` pattern for parent-driven state, including dependent field auto-fill (country → city) and a live field-change event log.
493
+
494
+ ---
495
+
496
+ ## License
497
+
498
+ ISC
499
+
500
500
  ---