@connect-soft/form-generator 1.1.0-alpha9 → 1.1.1

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
@@ -13,7 +13,7 @@
13
13
  - **Type-Safe**: Full TypeScript inference for form values and field types
14
14
  - **Field Type Checking**: Compile-time validation of `field.type` with autocomplete
15
15
  - **Extensible Types**: Add custom field types via module augmentation
16
- - **Imperative API**: Control form via ref (`setValues`, `reset`, `submit`, etc.)
16
+ - **Imperative API**: Control form via ref (`setValues`, `reset`, `setDefaultValues`, `submit`, etc.)
17
17
  - **Flexible**: Register custom field components with a simple API
18
18
  - **Validation**: Built-in Zod validation support
19
19
  - **Array Fields**: Repeatable field groups with `useFieldArray` integration
@@ -524,6 +524,69 @@ Highlight specific fields while rendering the rest normally:
524
524
  </FormGenerator>
525
525
  ```
526
526
 
527
+ ### Hook-Based Field Access with useTemplateField
528
+
529
+ When you need to access fields from **child components** (not inline in the render function), use the `useTemplateField` hook. It returns the pre-rendered field element and marks it as accessed, so it won't appear in remaining fields.
530
+
531
+ ```typescript
532
+ import { FormGenerator, useTemplateField, RemainingFields } from '@connect-soft/form-generator';
533
+
534
+ // Child component that claims a field
535
+ function EmailSection() {
536
+ const email = useTemplateField('email');
537
+ return <div className="highlighted">{email}</div>;
538
+ }
539
+
540
+ function PasswordSection() {
541
+ const password = useTemplateField('password');
542
+ return <div className="special">{password}</div>;
543
+ }
544
+
545
+ <FormGenerator fields={fieldDefinitions} onSubmit={handleSubmit}>
546
+ {({ buttons }) => (
547
+ <div>
548
+ <EmailSection />
549
+ <PasswordSection />
550
+ <div className="other-fields">
551
+ <RemainingFields />
552
+ </div>
553
+ {buttons.submit}
554
+ </div>
555
+ )}
556
+ </FormGenerator>
557
+ ```
558
+
559
+ **Why not just use `fields.remaining` with hooks?** The render function runs before child components render, so `fields.remaining` is evaluated before any hooks in child components mark fields as accessed. `<RemainingFields />` is a component that evaluates at its own render time — after sibling components above it have already claimed their fields.
560
+
561
+ You can freely mix proxy access in the render function with hook access in child components:
562
+
563
+ ```typescript
564
+ <FormGenerator fields={fieldDefinitions} onSubmit={handleSubmit}>
565
+ {({ fields, buttons }) => (
566
+ <div>
567
+ {/* Proxy access in the render function */}
568
+ <div className="hero">{fields.email}</div>
569
+ {/* Hook access in a child component */}
570
+ <PasswordSection />
571
+ {/* RemainingFields excludes both email and password */}
572
+ <RemainingFields />
573
+ {buttons.submit}
574
+ </div>
575
+ )}
576
+ </FormGenerator>
577
+ ```
578
+
579
+ #### useTemplateField
580
+
581
+ | Parameter | Type | Description |
582
+ |-----------|------|-------------|
583
+ | `name` | `string` | Field name to retrieve |
584
+ | **Returns** | `ReactElement \| undefined` | The pre-rendered field element, or `undefined` if not found |
585
+
586
+ #### RemainingFields
587
+
588
+ A component that renders all fields not yet accessed via `useTemplateField` or the fields proxy. Must be used within a `FormGenerator` custom layout.
589
+
527
590
  ### Form State Access
528
591
 
529
592
  Use form state for conditional rendering:
@@ -751,8 +814,138 @@ const fields = [
751
814
  | Schema | No (infers from fields) | Required (Zod) |
752
815
  | Field name checking | Inferred from fields | Enforced at compile-time |
753
816
  | Type inference | From field definitions | From Zod schema |
817
+ | Constraint detection | No | Yes (automatic) |
754
818
  | Use case | Quick prototyping | Production apps |
755
819
 
820
+ ### Automatic Schema Constraint Detection
821
+
822
+ `StrictFormGenerator` automatically extracts constraints from your Zod schema and propagates them to field components. This means you don't need to duplicate constraints in both your schema and field definitions.
823
+
824
+ #### Supported Constraints
825
+
826
+ **Number fields** (`z.number()`):
827
+ | Zod Method | Field Property | Example |
828
+ |------------|----------------|---------|
829
+ | `.min(n)` | `min` | `z.number().min(0)` → `{ min: 0 }` |
830
+ | `.max(n)` | `max` | `z.number().max(100)` → `{ max: 100 }` |
831
+ | `.int()` | `step: 1` | `z.number().int()` → `{ step: 1 }` |
832
+ | `.multipleOf(n)` | `step` | `z.number().multipleOf(0.01)` → `{ step: 0.01 }` |
833
+ | `.positive()` | `min` | `z.number().positive()` → `{ min: 0 }` (exclusive) |
834
+ | `.nonnegative()` | `min: 0` | `z.number().nonnegative()` → `{ min: 0 }` |
835
+
836
+ **String fields** (`z.string()`):
837
+ | Zod Method | Field Property | Example |
838
+ |------------|----------------|---------|
839
+ | `.min(n)` | `minLength` | `z.string().min(3)` → `{ minLength: 3 }` |
840
+ | `.max(n)` | `maxLength` | `z.string().max(100)` → `{ maxLength: 100 }` |
841
+ | `.length(n)` | `minLength` + `maxLength` | `z.string().length(6)` → `{ minLength: 6, maxLength: 6 }` |
842
+ | `.regex(pattern)` | `pattern` | `z.string().regex(/^[A-Z]+$/)` → `{ pattern: '^[A-Z]+$' }` |
843
+
844
+ **Date fields** (`z.date()`):
845
+ | Zod Method | Field Property | Example |
846
+ |------------|----------------|---------|
847
+ | `.min(date)` | `min` (ISO string) | `z.date().min(new Date('2020-01-01'))` → `{ min: '2020-01-01' }` |
848
+ | `.max(date)` | `max` (ISO string) | `z.date().max(new Date('2030-12-31'))` → `{ max: '2030-12-31' }` |
849
+
850
+ #### Example
851
+
852
+ ```typescript
853
+ import { StrictFormGenerator } from '@connect-soft/form-generator';
854
+ import { z } from 'zod';
855
+
856
+ const userSchema = z.object({
857
+ username: z.string().min(3).max(20).regex(/^[a-z0-9_]+$/),
858
+ age: z.number().int().min(18).max(120),
859
+ price: z.number().multipleOf(0.01).min(0),
860
+ birthDate: z.date().min(new Date('1900-01-01')).max(new Date()),
861
+ });
862
+
863
+ // No need to specify min/max/minLength/maxLength in fields!
864
+ // They are automatically extracted from the schema
865
+ <StrictFormGenerator
866
+ schema={userSchema}
867
+ fields={[
868
+ { type: 'text', name: 'username', label: 'Username' },
869
+ { type: 'number', name: 'age', label: 'Age' },
870
+ { type: 'number', name: 'price', label: 'Price' },
871
+ { type: 'date', name: 'birthDate', label: 'Birth Date' },
872
+ ]}
873
+ onSubmit={handleSubmit}
874
+ />
875
+
876
+ // Field components receive:
877
+ // username: { minLength: 3, maxLength: 20, pattern: '^[a-z0-9_]+$', required: true }
878
+ // age: { min: 18, max: 120, step: 1, required: true }
879
+ // price: { min: 0, step: 0.01, required: true }
880
+ // birthDate: { min: '1900-01-01', max: '2026-02-02', required: true }
881
+ ```
882
+
883
+ #### Using Constraints in Field Components
884
+
885
+ Your registered field components can use these constraints directly:
886
+
887
+ ```typescript
888
+ registerField('number', ({ field, formField, fieldState }) => (
889
+ <div>
890
+ <label>{field.label}</label>
891
+ <input
892
+ type="number"
893
+ {...formField}
894
+ min={field.min}
895
+ max={field.max}
896
+ step={field.step}
897
+ />
898
+ {fieldState.error && <span>{fieldState.error.message}</span>}
899
+ </div>
900
+ ));
901
+
902
+ registerField('text', ({ field, formField, fieldState }) => (
903
+ <div>
904
+ <label>{field.label}</label>
905
+ <input
906
+ type="text"
907
+ {...formField}
908
+ minLength={field.minLength}
909
+ maxLength={field.maxLength}
910
+ pattern={field.pattern}
911
+ />
912
+ {fieldState.error && <span>{fieldState.error.message}</span>}
913
+ </div>
914
+ ));
915
+ ```
916
+
917
+ #### Manual Constraint Merging
918
+
919
+ You can also manually merge constraints using the utility functions:
920
+
921
+ ```typescript
922
+ import { mergeSchemaConstraints, analyzeSchema } from '@connect-soft/form-generator';
923
+
924
+ const schema = z.object({
925
+ age: z.number().min(0).max(120),
926
+ name: z.string().min(1).max(100),
927
+ });
928
+
929
+ // Analyze schema to get field info
930
+ const fieldInfo = analyzeSchema(schema);
931
+ // => [
932
+ // { name: 'age', type: 'number', required: true, min: 0, max: 120 },
933
+ // { name: 'name', type: 'string', required: true, minLength: 1, maxLength: 100 }
934
+ // ]
935
+
936
+ // Or merge constraints into existing fields
937
+ const fields = [
938
+ { type: 'number', name: 'age', label: 'Age' },
939
+ { type: 'text', name: 'name', label: 'Name' },
940
+ ];
941
+
942
+ const fieldsWithConstraints = mergeSchemaConstraints(schema, fields);
943
+ // => [
944
+ // { type: 'number', name: 'age', label: 'Age', required: true, min: 0, max: 120 },
945
+ // { type: 'text', name: 'name', label: 'Name', required: true, minLength: 1, maxLength: 100 }
946
+ // ]
947
+ ```
948
+
756
949
  ### Available Helpers
757
950
 
758
951
  | Helper | Description |
@@ -830,6 +1023,23 @@ function MyForm() {
830
1023
  });
831
1024
  };
832
1025
 
1026
+ // Set values without marking the form as dirty
1027
+ const handleLoadData = () => {
1028
+ formRef.current?.setValues(
1029
+ { email: 'loaded@api.com', age: 30 },
1030
+ { shouldDirty: false }
1031
+ );
1032
+ };
1033
+
1034
+ // Set values and make them the new baseline for isDirty
1035
+ const handleSetDefaults = () => {
1036
+ formRef.current?.setDefaultValues({
1037
+ email: 'new-default@example.com',
1038
+ age: 25,
1039
+ });
1040
+ // isDirty is now false — these are the new default values
1041
+ };
1042
+
833
1043
  return (
834
1044
  <>
835
1045
  <FormGenerator
@@ -840,6 +1050,8 @@ function MyForm() {
840
1050
  <button type="button" onClick={handleExternalSubmit}>Submit Externally</button>
841
1051
  <button type="button" onClick={handleReset}>Reset Form</button>
842
1052
  <button type="button" onClick={handleSetValues}>Set Values</button>
1053
+ <button type="button" onClick={handleLoadData}>Load Data</button>
1054
+ <button type="button" onClick={handleSetDefaults}>Set Defaults</button>
843
1055
  </>
844
1056
  );
845
1057
  }
@@ -849,9 +1061,10 @@ function MyForm() {
849
1061
 
850
1062
  | Method | Description |
851
1063
  |--------|-------------|
852
- | `setValues(values)` | Set form values (partial update) |
1064
+ | `setValues(values, options?)` | Set form values (partial update). Options: `{ shouldDirty?, shouldValidate? }` |
853
1065
  | `getValues()` | Get current form values |
854
1066
  | `reset(values?)` | Reset to default or provided values |
1067
+ | `setDefaultValues(values)` | Set values and make them the new baseline for `isDirty` |
855
1068
  | `submit()` | Programmatically submit the form |
856
1069
  | `clearErrors()` | Clear all validation errors |
857
1070
  | `setError(name, error)` | Set error for a specific field |
@@ -859,6 +1072,24 @@ function MyForm() {
859
1072
  | `isDirty()` | Check if form has unsaved changes |
860
1073
  | `form` | Access underlying react-hook-form instance |
861
1074
 
1075
+ ### SetValuesOptions
1076
+
1077
+ | Option | Type | Default | Description |
1078
+ |--------|------|---------|-------------|
1079
+ | `shouldDirty` | `boolean` | `true` | Whether setting the value marks the field as dirty |
1080
+ | `shouldValidate` | `boolean` | `true` | Whether to trigger validation after setting values |
1081
+
1082
+ ### setDefaultValues vs reset vs setValues
1083
+
1084
+ | Method | Sets values | Resets dirty state | New dirty baseline |
1085
+ |--------|-------------|--------------------|--------------------|
1086
+ | `setValues(v)` | Yes | No (marks dirty) | No |
1087
+ | `setValues(v, { shouldDirty: false })` | Yes | No (keeps current) | No |
1088
+ | `reset(v)` | Yes | Yes | Yes (merges with original defaults) |
1089
+ | `setDefaultValues(v)` | Yes | Yes | Yes (merges with original defaults) |
1090
+
1091
+ Use `setDefaultValues` when loading data from an API and you want the loaded values to become the "clean" baseline (e.g., editing an existing record). Use `setValues` with `{ shouldDirty: false }` when you want to set a value without affecting dirty tracking at all.
1092
+
862
1093
  ### Watching Form Values
863
1094
 
864
1095
  Detect when field values change from the parent component:
@@ -954,6 +1185,44 @@ function ValueWatcher() {
954
1185
  | `validation` | `ZodType` | Zod validation schema |
955
1186
  | `className` | `string` | CSS class for field wrapper |
956
1187
 
1188
+ ### Field Type-Specific Properties
1189
+
1190
+ **Text fields** (`text`, `email`, `password`, `tel`, `url`, `search`):
1191
+ | Property | Type | Description |
1192
+ |----------|------|-------------|
1193
+ | `placeholder` | `string` | Placeholder text |
1194
+ | `minLength` | `number` | Minimum character length |
1195
+ | `maxLength` | `number` | Maximum character length |
1196
+ | `pattern` | `string` | HTML5 validation pattern |
1197
+
1198
+ **Number fields** (`number`, `range`):
1199
+ | Property | Type | Description |
1200
+ |----------|------|-------------|
1201
+ | `placeholder` | `string` | Placeholder text |
1202
+ | `min` | `number` | Minimum value |
1203
+ | `max` | `number` | Maximum value |
1204
+ | `step` | `number` | Step increment |
1205
+
1206
+ **Date fields** (`date`, `datetime`, `datetime-local`):
1207
+ | Property | Type | Description |
1208
+ |----------|------|-------------|
1209
+ | `min` | `string` | Minimum date (ISO format: YYYY-MM-DD) |
1210
+ | `max` | `string` | Maximum date (ISO format: YYYY-MM-DD) |
1211
+
1212
+ ### Schema Analysis Utilities
1213
+
1214
+ | Function | Description |
1215
+ |----------|-------------|
1216
+ | `analyzeSchema(schema)` | Extract detailed field info from Zod schema |
1217
+ | `mergeSchemaConstraints(schema, fields)` | Merge schema constraints into field definitions |
1218
+ | `mergeSchemaRequirements(schema, fields)` | Merge only required status (legacy) |
1219
+ | `getNumberConstraints(schema)` | Extract min/max/step from number schema |
1220
+ | `getStringConstraints(schema)` | Extract minLength/maxLength/pattern from string schema |
1221
+ | `getDateConstraints(schema)` | Extract min/max dates from date schema |
1222
+ | `isSchemaRequired(schema)` | Check if schema field is required |
1223
+ | `unwrapSchema(schema)` | Unwrap optional/nullable wrappers |
1224
+ | `getSchemaTypeName(schema)` | Get base type name (string, number, etc.) |
1225
+
957
1226
  ---
958
1227
 
959
1228
  ## Links