react-form-dto 1.0.4 → 1.0.6

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
@@ -18,14 +18,14 @@ It is designed for **schema-driven UIs**, backend-configured workflows, admin pa
18
18
  Most form libraries solve **state management**.
19
19
  React Form DTO solves **form architecture**.
20
20
 
21
- It operates at a higher abstraction level where **layout, validation, rendering, and behavior** are defined in a single schema.
21
+ It operates at a higher abstraction level where **layout, validation, rendering, and behavior** are defined in a single schema — with a react-hook-form–style hook API to manage state and submission.
22
22
 
23
23
  ### Use this library when:
24
24
 
25
25
  - Forms are generated from backend schemas or configuration APIs
26
26
  - UI logic must be reused across multiple applications
27
27
  - Forms are large, dynamic, or conditional
28
- - You need imperative control (wizards, modals, async flows)
28
+ - You need reactive watches, field arrays, or context-driven child components
29
29
  - Your design system is based on Material UI
30
30
 
31
31
  ### Key Advantages
@@ -33,7 +33,7 @@ It operates at a higher abstraction level where **layout, validation, rendering,
33
33
  - 📄 **DTO-first design** – define forms entirely in JSON or TypeScript
34
34
  - 🎨 **Material UI v7 native** – accessibility and consistency by default
35
35
  - 🧱 **Composable structure** – Form → Section → Field
36
- - 🎯 **Imperative API** – programmatic control via refs
36
+ - 🪝 **Hook-based API** – `useFormDTO`, `useForm`, `useWatch`, `useFieldArray`, `useFormContext`
37
37
  - 🔀 **Conditional rendering** – dynamic visibility and logic
38
38
  - 🧩 **Extensible renderers** – plug in custom components
39
39
  - 🛡️ **Strong TypeScript typing** – safe, predictable APIs
@@ -48,12 +48,12 @@ It operates at a higher abstraction level where **layout, validation, rendering,
48
48
  |------|---------------|----------------|--------|
49
49
  | Schema / DTO driven | ✅ Native | ❌ Manual | ❌ Manual |
50
50
  | MUI-first | ✅ Yes | ⚠️ Partial | ⚠️ Partial |
51
- | Imperative API | ✅ First-class | ⚠️ Limited | ⚠️ Limited |
51
+ | Hook-based API | ✅ First-class | Yes | ⚠️ Partial |
52
52
  | Large dynamic forms | ✅ Excellent | ⚠️ Medium | ❌ Poor |
53
53
  | Boilerplate | ✅ Minimal | ❌ High | ❌ High |
54
54
 
55
55
  > **Note:** React Form DTO is **not a replacement** for React Hook Form.
56
- > It is a **higher-level abstraction** for schema-driven UI generation.
56
+ > It is a **higher-level abstraction** for schema-driven UI generation, with a familiar hook API on top.
57
57
 
58
58
  ---
59
59
 
@@ -103,188 +103,216 @@ See [CHANGELOG.md](./.github/CHANGELOG.md) for detailed version history.
103
103
 
104
104
  ### DTO as Source of Truth
105
105
 
106
- All structure, layout, validation, and behavior live in a single schema.
106
+ All structure, layout, validation, and behavior live in a single schema object. Define it once — the library handles the rest.
107
107
 
108
- ### Stateless Rendering
108
+ ### Hook-based State Management
109
109
 
110
- The UI is derived entirely from the DTO and internal state.
110
+ `useFormDTO` bridges the schema into a react-hook-form–style context. Any component inside `<FormProvider>` can read values, watch fields, and access errors without prop-drilling.
111
111
 
112
- ### Imperative Escape Hatch
112
+ ### Renderer Isolation
113
113
 
114
- Refs enable workflows that declarative-only approaches struggle with.
114
+ Field logic is decoupled from presentation, enabling full customization via the `renderers` prop.
115
115
 
116
- ### Renderer Isolation
116
+ ### Imperative Escape Hatch
117
117
 
118
- Field logic is decoupled from presentation, enabling customization.
118
+ A ref-based API (`FormBuilderHandle`) is available for scenarios that need programmatic control outside the React tree.
119
+
120
+ ---
119
121
 
120
122
  ## Quick Start
121
123
 
122
- ```tsx
123
- import { FormBuilder, type FormBuilderHandle } from 'react-form-dto';
124
- import { useRef } from 'react';
125
- import { myFormDTO } from './myFormDTO';
126
-
127
- const formRef = useRef<FormBuilderHandle>(null);
128
-
129
- const handleSubmit = (e) => {
130
- e.preventDefault();
131
- if (!formRef.current) return;
132
- const values = formRef.current.getValues();
133
- const errors = formRef.current.validateAll();
134
- formRef.current.handleChange?.("firstName", "NewName");
135
- const firstNameErrors = formRef.current.validateField("firstName");
136
- const allErrors = formRef.current.getErrors();
137
- };
124
+ ### 1 — Define a FormDTO
125
+
126
+ ```ts
127
+ import type { FormDTO } from 'react-form-dto';
138
128
 
139
- return (
140
- <form onSubmit={handleSubmit}>
141
- <FormBuilder ref={formRef} dto={myFormDTO} />
142
- <button type="submit">Submit</button>
143
- </form>
144
- );
129
+ const profileForm: FormDTO = {
130
+ title: "User Profile",
131
+ sections: [
132
+ {
133
+ id: "personal",
134
+ heading: "Personal Information",
135
+ fields: [
136
+ {
137
+ id: "firstName",
138
+ type: "text",
139
+ label: "First Name",
140
+ layout: { cols: 6 },
141
+ validations: { required: "First name is required" },
142
+ },
143
+ {
144
+ id: "lastName",
145
+ type: "text",
146
+ label: "Last Name",
147
+ layout: { cols: 6 },
148
+ validations: { required: "Last name is required" },
149
+ },
150
+ {
151
+ id: "email",
152
+ type: "email",
153
+ label: "Email",
154
+ validations: {
155
+ required: "Email is required",
156
+ pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
157
+ },
158
+ },
159
+ ],
160
+ },
161
+ ],
162
+ };
145
163
  ```
146
164
 
147
- ---
165
+ ### 2 — Render with FormProvider
148
166
 
149
- ## useFormBuilderController Controlled, Auto‑Rendered Forms
167
+ Pass the DTO to `useFormDTO`, wrap in `<FormProvider>`, and render with `<FormBuilder>`. `form.handleSubmit` validates all fields before calling your callback.
150
168
 
151
- `useFormBuilderController` is a hook that wraps `FormBuilder` and gives you:
169
+ ```tsx
170
+ import { FormBuilder, FormProvider, useFormDTO } from 'react-form-dto';
171
+ import { profileForm } from './profileForm';
152
172
 
153
- - An auto-rendering `Form` component (no manual mapping of sections/fields)
154
- - An imperative controller API for values, errors, and validation
173
+ export function ProfilePage() {
174
+ const form = useFormDTO(profileForm);
155
175
 
156
- It combines the convenience of `FormBuilder` with the programmatic control of `useFormBuilder`.
176
+ const onSubmit = (data: Record<string, any>) => {
177
+ console.log('Submitted:', data);
178
+ };
157
179
 
158
- ### When to Use
180
+ return (
181
+ <FormProvider value={form}>
182
+ <form onSubmit={form.handleSubmit(onSubmit)}>
183
+ <FormBuilder dto={profileForm} />
184
+ <button type="submit">Submit</button>
185
+ </form>
186
+ </FormProvider>
187
+ );
188
+ }
189
+ ```
190
+
191
+ `useFormDTO` automatically extracts default values and wires all `validations` rules from the DTO into the form context. Errors appear after a field is touched — not on initial load.
159
192
 
160
- - You want the form UI to be generated entirely from a DTO
161
- - You need to:
162
- - Run validation on submit
163
- - Read values/errors from outside the form
164
- - Prefill fields programmatically
165
- - Plug in custom field renderers
193
+ ### 3 Read state from child components
166
194
 
167
- ### Basic Example
195
+ Any component inside `<FormProvider>` can subscribe to form state:
168
196
 
169
197
  ```tsx
170
- import { useFormBuilderController } from 'react-form-dto';
171
- import { myFormDTO } from './myFormDTO';
198
+ import { useWatch, useFormContext } from 'react-form-dto';
172
199
 
173
- function MyFormWithController() {
174
- const formController = useFormBuilderController({
175
- dto: myFormDTO,
176
- locale: 'en',
177
- });
200
+ function LiveGreeting() {
201
+ const firstName = useWatch('firstName');
202
+ return <p>Hello, {firstName || 'stranger'}!</p>;
203
+ }
178
204
 
179
- const handleSubmit = (e: React.FormEvent) => {
180
- e.preventDefault();
205
+ function SubmitButton() {
206
+ const { errors } = useFormContext();
207
+ const hasErrors = Object.values(errors).some(Boolean);
208
+ return <button type="submit" disabled={hasErrors}>Submit</button>;
209
+ }
210
+ ```
211
+
212
+ ---
181
213
 
182
- const errors = formController.validateAll();
183
- const hasErrors = Object.values(errors).some(
184
- (err) => err && err.length > 0,
185
- );
214
+ ## Hooks
186
215
 
187
- if (hasErrors) {
188
- console.log('Validation errors:', errors);
189
- return;
190
- }
216
+ | Hook | Description |
217
+ |------|-------------|
218
+ | `useFormDTO(dto, options?)` | Creates a form instance bound to a `FormDTO`. Extracts defaults and wires all validations automatically. |
219
+ | `useForm(options?)` | Lower-level hook for plain (non-DTO) forms with a react-hook-form–style API. |
220
+ | `useFormContext()` | Reads the form instance from the nearest `<FormProvider>`. Throws if no provider found. |
221
+ | `useOptionalFormContext()` | Like `useFormContext` but returns `undefined` instead of throwing. |
222
+ | `useWatch(name)` | Reactively reads one or more field values. Re-renders on change. |
223
+ | `useFieldArray({ name })` | Manages an array field — `fields`, `append`, `prepend`, `remove`, `swap`, `insert`. |
224
+ | `useFormBuilderController(props)` | Wraps `FormBuilder` in a ref-based controller. Returns a `Form` component + imperative API. |
225
+ | `useFormBuilder(dto)` | Low-level hook that manages raw form state without a provider. |
191
226
 
192
- console.log('Form submitted:', formController.getValues());
193
- };
227
+ ---
194
228
 
195
- return (
196
- <form onSubmit={handleSubmit}>
197
- {/* Auto-renders the full form from the DTO */}
198
- <formController.Form />
199
- <button type="submit">Submit</button>
200
- </form>
201
- );
229
+ ## Validation
230
+
231
+ Validation rules live on each field's `validations` object and are run automatically by `handleSubmit` or on demand via `trigger()`.
232
+
233
+ ```ts
234
+ {
235
+ id: "email",
236
+ type: "email",
237
+ label: "Email",
238
+ validations: {
239
+ required: "Email is required",
240
+ pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
241
+ minLength: 5,
242
+ maxLength: 100,
243
+ },
202
244
  }
203
245
  ```
204
246
 
205
- ### API Overview
247
+ ### Built-in rules
248
+
249
+ | Rule | Type | Description |
250
+ |------|------|-------------|
251
+ | `required` | `boolean \| string` | Field must not be empty. |
252
+ | `minLength` | `number` | Minimum string length. |
253
+ | `maxLength` | `number` | Maximum string length. |
254
+ | `min` | `number` | Minimum numeric value. |
255
+ | `max` | `number` | Maximum numeric value. |
256
+ | `pattern` | `RegExp` | Must match regex. |
257
+ | `validate` | `(value, allValues) => string \| null` | Custom function. |
258
+
259
+ ### Validate programmatically
206
260
 
207
261
  ```ts
208
- const controller = useFormBuilderController({
209
- dto: myFormDTO,
210
- locale: 'en',
211
- renderers: {
212
- // optional: override field types
213
- text: MyTextField,
214
- },
215
- handleChangeCallback: (id, value) => {
216
- // optional: side effects on every field change
217
- console.log(id, value);
218
- },
219
- });
262
+ const isValid = form.trigger(); // validate all fields
263
+ const emailOk = form.trigger('email'); // validate one field
264
+ ```
220
265
 
221
- // Reading and validating
222
- controller.getValues(); // Record<string, any>
223
- controller.getErrors(); // Record<string, string | null>
224
- controller.validateAll(); // Record<string, string[] | null>
225
- controller.validateField('id'); // string[]
266
+ ### Inject server errors
226
267
 
227
- // Programmatic updates
228
- controller.handleChange('firstName', 'Jane');
268
+ ```ts
269
+ const onSubmit = async (data) => {
270
+ try {
271
+ await api.save(data);
272
+ } catch {
273
+ form.setError('email', 'This email is already taken');
274
+ }
275
+ };
229
276
  ```
230
277
 
231
- ### Example with Prefill and Custom Renderers
278
+ ---
279
+
280
+ ## useFormBuilderController — Quick drop-in
281
+
282
+ For cases where you want the full form rendered from a DTO with an external imperative API and no `FormProvider` setup:
232
283
 
233
284
  ```tsx
234
285
  import { useFormBuilderController } from 'react-form-dto';
235
- import { myFormDTO } from './myFormDTO';
236
- import { TextInput, SelectInput } from './customFields';
237
-
238
- const renderers = {
239
- text: TextInput,
240
- select: SelectInput,
241
- };
286
+ import { profileForm } from './profileForm';
242
287
 
243
- function AdvancedForm() {
244
- const formController = useFormBuilderController({
245
- dto: myFormDTO,
246
- locale: 'en',
247
- renderers,
248
- handleChangeCallback: (id, value) => {
249
- // sync with external store / analytics, etc.
250
- console.log('Changed:', id, value);
251
- },
252
- });
288
+ function MyForm() {
289
+ const ctrl = useFormBuilderController({ dto: profileForm, locale: 'en' });
253
290
 
254
- const prefill = () => {
255
- formController.handleChange('firstName', 'John');
256
- formController.handleChange('lastName', 'Doe');
291
+ const handleSubmit = (e: React.FormEvent) => {
292
+ e.preventDefault();
293
+ const errors = ctrl.validateAll();
294
+ if (Object.values(errors).every((e) => !e?.length)) {
295
+ console.log(ctrl.getValues());
296
+ }
257
297
  };
258
298
 
259
299
  return (
260
- <>
261
- <button type="button" onClick={prefill}>
262
- Prefill
263
- </button>
264
-
265
- <form
266
- onSubmit={(e) => {
267
- e.preventDefault();
268
- const errors = formController.validateAll();
269
- const hasErrors = Object.values(errors).some(
270
- (err) => err && err.length > 0,
271
- );
272
- if (!hasErrors) {
273
- console.log('Values:', formController.getValues());
274
- }
275
- }}
276
- >
277
- <formController.Form />
278
- <button type="submit">Submit</button>
279
- </form>
280
- </>
300
+ <form onSubmit={handleSubmit}>
301
+ <ctrl.Form />
302
+ <button type="submit">Submit</button>
303
+ </form>
281
304
  );
282
305
  }
283
306
  ```
284
307
 
285
- > For the full hook reference, see
286
- > **Docs → Hooks → `useFormBuilderController`**:
287
- > `docs/api/hooks/useFormBuilderController.md`
308
+ | Method | Returns | Description |
309
+ |--------|---------|-------------|
310
+ | `getValues()` | `Record<string, any>` | All current field values. |
311
+ | `getErrors()` | `Record<string, string \| null>` | Current error state. |
312
+ | `validateAll()` | `Record<string, string[]>` | Validates every field. |
313
+ | `validateField(id)` | `string[]` | Validates one field. |
314
+ | `handleChange(id, val)` | `void` | Programmatically set a field value. |
315
+ | `Form` | `() => JSX.Element` | Auto-rendered form component. |
288
316
 
289
317
  ---
290
318
 
@@ -292,19 +320,19 @@ function AdvancedForm() {
292
320
 
293
321
  ![Form Example](./example.png)
294
322
 
295
- The form in the image above is generated from this DTO.
323
+ The form in the image above is generated from this DTO:
296
324
 
297
325
  ```tsx
298
326
  const profileForm: FormDTO = {
299
327
  title: "User Profile",
300
328
  description: "Fill out your personal information",
301
- layout: { cols: 12, gap: "1rem" }, // global form layout
329
+ layout: { cols: 12, gap: "1rem" },
302
330
  sections: [
303
331
  {
304
332
  id: "personal",
305
333
  heading: "Personal Information",
306
334
  description: "Basic details about you",
307
- layout: { cols: 12, gap: "1rem" }, // section layout
335
+ layout: { cols: 12, gap: "1rem" },
308
336
  fields: [
309
337
  {
310
338
  id: "title",
@@ -343,20 +371,12 @@ const profileForm: FormDTO = {
343
371
  type: "multi-autocomplete",
344
372
  label: "Skills",
345
373
  placeholder: "Select your skills",
346
- options: [
347
- "React",
348
- "TypeScript",
349
- "Node.js",
350
- "GraphQL",
351
- "Docker",
352
- ],
374
+ options: ["React", "TypeScript", "Node.js", "GraphQL", "Docker"],
353
375
  layout: { cols: 12 },
354
376
  validations: {
355
377
  required: "Select at least one skill",
356
378
  validate: (val: string[]) =>
357
- val && val.length < 2
358
- ? "Pick at least 2 skills"
359
- : null,
379
+ val && val.length < 2 ? "Pick at least 2 skills" : null,
360
380
  },
361
381
  },
362
382
  ],
@@ -418,11 +438,7 @@ const profileForm: FormDTO = {
418
438
 
419
439
  ## 🎭 Conditional Visibility with `visibleWhen`
420
440
 
421
- React Form DTO supports dynamic field visibility based on the values of other fields in the form. This is achieved through the `visibleWhen` property, which allows you to define simple conditions or complex logical expressions.
422
-
423
- ### Basic Usage
424
-
425
- Show a field only when another field has a specific value:
441
+ React Form DTO supports dynamic field visibility based on the values of other fields. Define simple conditions or complex AND/OR groups:
426
442
 
427
443
  ```tsx
428
444
  {
@@ -436,189 +452,61 @@ Show a field only when another field has a specific value:
436
452
  }
437
453
  ```
438
454
 
439
- For detail documentation, please visit [Docs](https://shakir-afridi.github.io/react-form-dto/docs/api/visibleWhen.html)
455
+ For full documentation see [Docs → visibleWhen](https://shakir-afridi.github.io/react-form-dto/docs/api/visibleWhen.html).
440
456
 
441
457
  ---
442
458
 
443
459
  ## 🌍 Internationalization (I18n)
444
460
 
445
- React Form DTO has built-in support for multi-language forms through `I18String` and `I18nOption` types.
446
-
447
- ### Using I18String
448
-
449
- Any text property (`label`, `placeholder`, `title`, `description`, validation messages) can be either a plain string or a locale map:
461
+ Any text property (`label`, `placeholder`, `title`, `description`, validation messages) can be a plain string or a locale map:
450
462
 
451
463
  ```tsx
452
- // Simple string (single language)
453
- {
454
- label: "First Name"
455
- }
456
-
457
- // Multi-language support
458
464
  {
459
465
  label: {
460
466
  en: "First Name",
461
467
  fr: "Prénom",
462
- es: "Nombre",
463
468
  de: "Vorname"
464
- }
465
- }
466
- ```
467
-
468
- ### Using I18nOption for Selections
469
-
470
- For select, autocomplete, and multi-autocomplete fields, you can use `I18nOption` objects to provide translatable labels while maintaining stable values:
471
-
472
- ```tsx
473
- {
474
- id: "country",
475
- type: "select",
476
- label: { en: "Country", fr: "Pays" },
477
- options: [
478
- {
479
- value: "us",
480
- label: { en: "United States", fr: "États-Unis" }
481
- },
482
- {
483
- value: "fr",
484
- label: { en: "France", fr: "France" }
485
- },
486
- {
487
- value: "de",
488
- label: { en: "Germany", fr: "Allemagne" }
469
+ },
470
+ validations: {
471
+ required: {
472
+ en: "First name is required",
473
+ fr: "Le prénom est obligatoire"
489
474
  }
490
- ]
475
+ }
491
476
  }
492
477
  ```
493
478
 
494
- **Backward Compatibility:** Simple string arrays still work:
495
-
496
- ```tsx
497
- options: ["USA", "France", "Germany"] // Still supported
498
- ```
499
-
500
- ### I18n Validation Messages
479
+ Pass `locale` to `useFormDTO` to resolve messages:
501
480
 
502
- Validation messages also support I18n:
503
-
504
- ```tsx
505
- validations: {
506
- required: {
507
- en: "This field is required",
508
- fr: "Ce champ est obligatoire",
509
- es: "Este campo es obligatorio"
510
- },
511
- validate: (value) => value.length < 2
512
- ? {
513
- en: "Minimum 2 characters",
514
- fr: "Minimum 2 caractères"
515
- }
516
- : null
517
- }
481
+ ```ts
482
+ const form = useFormDTO(myDTO, { locale: 'fr' });
518
483
  ```
519
484
 
520
- ### Complete I18n Example
485
+ For select/autocomplete fields, use `I18nOption` objects for translatable option labels:
521
486
 
522
487
  ```tsx
523
- const multilingualForm: FormDTO = {
524
- title: {
525
- en: "User Registration",
526
- fr: "Inscription de l'utilisateur",
527
- es: "Registro de Usuario"
528
- },
529
- description: {
530
- en: "Please fill in your details",
531
- fr: "Veuillez remplir vos coordonnées",
532
- es: "Por favor complete sus datos"
533
- },
534
- sections: [
535
- {
536
- id: "personal",
537
- heading: {
538
- en: "Personal Information",
539
- fr: "Informations personnelles",
540
- es: "Información Personal"
541
- },
542
- fields: [
543
- {
544
- id: "title",
545
- type: "select",
546
- label: { en: "Title", fr: "Titre", es: "Título" },
547
- options: [
548
- { value: "mr", label: { en: "Mr", fr: "M.", es: "Sr." } },
549
- { value: "ms", label: { en: "Ms", fr: "Mme", es: "Sra." } },
550
- { value: "dr", label: { en: "Dr", fr: "Dr", es: "Dr." } }
551
- ]
552
- },
553
- {
554
- id: "firstName",
555
- type: "text",
556
- label: { en: "First Name", fr: "Prénom", es: "Nombre" },
557
- placeholder: {
558
- en: "Enter your first name",
559
- fr: "Entrez votre prénom",
560
- es: "Ingrese su nombre"
561
- },
562
- validations: {
563
- required: {
564
- en: "First name is required",
565
- fr: "Le prénom est obligatoire",
566
- es: "El nombre es obligatorio"
567
- }
568
- }
569
- }
570
- ]
571
- }
572
- ]
573
- };
488
+ options: [
489
+ { value: "us", label: { en: "United States", fr: "États-Unis" } },
490
+ { value: "de", label: { en: "Germany", fr: "Allemagne" } }
491
+ ]
574
492
  ```
575
493
 
576
- > **Note:** The library provides the I18n structure. You'll need to implement locale selection and text resolution in your application layer.
577
-
578
494
  ---
579
495
 
580
496
  ## Custom Field Renderers
581
497
 
582
- Override any default renderer by supplying your own component:
583
-
584
- ```ts
585
- {
586
- id: "salary",
587
- type: "number",
588
- label: "Salary",
589
- renderer: CurrencyInput
590
- }
591
- ```
592
-
593
- This makes the library extensible without modifying core logic.
498
+ Override any default renderer by supplying your own component via the `renderers` prop:
594
499
 
595
- ---
596
-
597
- ## Validation API
598
-
599
- Validation is handled through the `useFormBuilder` hook and the imperative form handle.
600
-
601
- ```ts
602
- const {
603
- handleChange, // Function to update a field value: (id, value) => void
604
- validateAll, // Function to validate all fields: () => Record<string, string[]>
605
- getValues, // Function to get all current form values: () => Record<string, any>
606
- getErrors, // Function to get all current form errors: () => Record<string, string | null>
607
- validateField, // Function to validate a specific field: (id) => string[]
608
- } = useFormBuilder(myFormDTO);
500
+ ```tsx
501
+ <FormBuilder dto={myDTO} renderers={{ text: MyTextField, select: MySelect }} />
609
502
  ```
610
503
 
611
- ### Validation Rules
504
+ Or pass them through `useFormBuilderController`:
612
505
 
613
506
  ```ts
614
- validations: {
615
- required: "This field is required",
616
- validate: (value) => value.length < 2 ? "Minimum 2 characters" : null
617
- }
507
+ const ctrl = useFormBuilderController({ dto: myDTO, locale: 'en', renderers: { text: MyTextField } });
618
508
  ```
619
509
 
620
- > Recommendation: Standardize validation return values to `string[]` for predictable handling in large applications.
621
-
622
510
  ---
623
511
 
624
512
  ## Real-World Enterprise Usage
@@ -678,11 +566,11 @@ This enables:
678
566
  Contributions are welcome and encouraged.
679
567
 
680
568
  1. Fork the repository
681
- 2. Create a feature branch
569
+ 2. Create a feature branch
682
570
  `git checkout -b feature/my-feature`
683
- 3. Commit your changes
571
+ 3. Commit your changes
684
572
  `git commit -m "Add my feature"`
685
- 4. Push to the branch
573
+ 4. Push to the branch
686
574
  `git push origin feature/my-feature`
687
575
  5. Open a Pull Request
688
576
 
@@ -696,4 +584,4 @@ MIT
696
584
 
697
585
  ---
698
586
 
699
- **React Form DTO — Schema-first forms for Material UI**
587
+ ## React Form DTO — Schema-first forms for Material UI