@connect-soft/form-generator 1.1.0-alpha3 → 1.1.0-alpha5

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
@@ -266,6 +266,113 @@ const fields = [
266
266
 
267
267
  ---
268
268
 
269
+ ## Custom Layouts
270
+
271
+ For full control over form layout, pass a render function as `children`:
272
+
273
+ ```typescript
274
+ <FormGenerator
275
+ fields={[
276
+ { type: 'text', name: 'email', label: 'Email' },
277
+ { type: 'password', name: 'password', label: 'Password' },
278
+ ] as const}
279
+ title="Login"
280
+ onSubmit={handleSubmit}
281
+ >
282
+ {({ fields, buttons, title }) => (
283
+ <div className="login-form">
284
+ <h1>{title}</h1>
285
+ <div className="field-row">{fields.email}</div>
286
+ <div className="field-row">{fields.password}</div>
287
+ <div className="actions">{buttons.submit}</div>
288
+ </div>
289
+ )}
290
+ </FormGenerator>
291
+ ```
292
+
293
+ ### Render Props API
294
+
295
+ The render function receives:
296
+
297
+ | Property | Type | Description |
298
+ |----------|------|-------------|
299
+ | `fields` | `TemplateFields` | Pre-rendered fields (see below) |
300
+ | `layouts` | `TemplateLayouts` | Pre-rendered named layouts |
301
+ | `buttons` | `{ submit, reset? }` | Pre-rendered buttons |
302
+ | `title` | `string` | Form title prop |
303
+ | `description` | `string` | Form description prop |
304
+ | `form` | `UseFormReturn` | react-hook-form instance |
305
+ | `isSubmitting` | `boolean` | Form submission state |
306
+ | `isValid` | `boolean` | Form validity state |
307
+ | `isDirty` | `boolean` | Form dirty state |
308
+ | `renderField` | `function` | Manual field renderer |
309
+ | `renderLayout` | `function` | Manual layout renderer |
310
+
311
+ ### Fields Object
312
+
313
+ Access fields by name or use helper methods:
314
+
315
+ ```typescript
316
+ {({ fields }) => (
317
+ <div>
318
+ {/* Access individual fields */}
319
+ {fields.email}
320
+ {fields.password}
321
+
322
+ {/* Render all fields */}
323
+ {fields.all}
324
+
325
+ {/* Render only fields not yet accessed */}
326
+ {fields.remaining}
327
+
328
+ {/* Check if field exists */}
329
+ {fields.has('email') && fields.email}
330
+
331
+ {/* Get all field names */}
332
+ {fields.names.map(name => <div key={name}>{fields[name]}</div>)}
333
+
334
+ {/* Render specific fields */}
335
+ {fields.render('email', 'password')}
336
+ </div>
337
+ )}
338
+ ```
339
+
340
+ ### Mixed Layout Example
341
+
342
+ Highlight specific fields while rendering the rest normally:
343
+
344
+ ```typescript
345
+ <FormGenerator fields={fieldDefinitions} onSubmit={handleSubmit}>
346
+ {({ fields, buttons }) => (
347
+ <div>
348
+ <div className="highlighted">{fields.email}</div>
349
+ <div className="other-fields">{fields.remaining}</div>
350
+ {buttons.submit}
351
+ </div>
352
+ )}
353
+ </FormGenerator>
354
+ ```
355
+
356
+ ### Form State Access
357
+
358
+ Use form state for conditional rendering:
359
+
360
+ ```typescript
361
+ <FormGenerator fields={fieldDefinitions} onSubmit={handleSubmit}>
362
+ {({ fields, buttons, isSubmitting, isValid, isDirty }) => (
363
+ <div>
364
+ {fields.all}
365
+ <button type="submit" disabled={isSubmitting || !isValid}>
366
+ {isSubmitting ? 'Saving...' : 'Submit'}
367
+ </button>
368
+ {isDirty && <span>You have unsaved changes</span>}
369
+ </div>
370
+ )}
371
+ </FormGenerator>
372
+ ```
373
+
374
+ ---
375
+
269
376
  ## TypeScript Type Inference
270
377
 
271
378
  Get full type inference from field definitions:
@@ -378,6 +485,11 @@ function MyForm() {
378
485
  | `submitText` | `string` | `'Submit'` | Submit button text |
379
486
  | `disabled` | `boolean` | `false` | Disable entire form |
380
487
  | `mode` | `'onChange' \| 'onBlur' \| 'onSubmit' \| 'onTouched' \| 'all'` | `'onChange'` | Validation trigger mode |
488
+ | `children` | `TemplateRenderFn` | - | Render function for custom layout |
489
+ | `title` | `string` | - | Form title (available in render props) |
490
+ | `description` | `string` | - | Form description (available in render props) |
491
+ | `showReset` | `boolean` | `false` | Include reset button in `buttons.reset` |
492
+ | `resetText` | `string` | `'Reset'` | Reset button text |
381
493
 
382
494
  ### Field Base Properties
383
495
 
package/dist/index.js CHANGED
@@ -3,8 +3,8 @@
3
3
  var jsxRuntime = require('react/jsx-runtime');
4
4
  var React = require('react');
5
5
  var n$1 = require('zod/v4/core');
6
- var zod = require('zod');
7
6
  var compilerRuntime = require('react/compiler-runtime');
7
+ var zod = require('zod');
8
8
 
9
9
  function _interopNamespaceDefault(e) {
10
10
  var n = Object.create(null);
@@ -3130,6 +3130,105 @@ function clearAllRegistries() {
3130
3130
  clearLayoutRegistry();
3131
3131
  }
3132
3132
 
3133
+ const RESERVED_PROPS = new Set(['all', 'remaining', 'names', 'has', 'render']);
3134
+ function createTemplateFields(fieldEntries) {
3135
+ const accessedFields = new Set();
3136
+ const handler = {
3137
+ get(_, prop) {
3138
+ if (typeof prop === 'symbol') {
3139
+ return undefined;
3140
+ }
3141
+ if (prop === 'all') {
3142
+ return Array.from(fieldEntries.values()).map(entry => entry.element);
3143
+ }
3144
+ if (prop === 'remaining') {
3145
+ return Array.from(fieldEntries.entries()).filter(([name]) => !accessedFields.has(name)).map(([, entry]) => entry.element);
3146
+ }
3147
+ if (prop === 'names') {
3148
+ return Array.from(fieldEntries.keys());
3149
+ }
3150
+ if (prop === 'has') {
3151
+ return name => fieldEntries.has(name);
3152
+ }
3153
+ if (prop === 'render') {
3154
+ return (...names) => {
3155
+ return names.filter(name => fieldEntries.has(name)).map(name => {
3156
+ accessedFields.add(name);
3157
+ return fieldEntries.get(name).element;
3158
+ });
3159
+ };
3160
+ }
3161
+ if (fieldEntries.has(prop)) {
3162
+ accessedFields.add(prop);
3163
+ return fieldEntries.get(prop).element;
3164
+ }
3165
+ return undefined;
3166
+ },
3167
+ has(_, prop) {
3168
+ if (typeof prop === 'symbol') {
3169
+ return false;
3170
+ }
3171
+ return RESERVED_PROPS.has(prop) || fieldEntries.has(prop);
3172
+ },
3173
+ ownKeys() {
3174
+ return [...RESERVED_PROPS, ...fieldEntries.keys()];
3175
+ },
3176
+ getOwnPropertyDescriptor(_, prop) {
3177
+ if (typeof prop === 'symbol') {
3178
+ return undefined;
3179
+ }
3180
+ if (RESERVED_PROPS.has(prop) || fieldEntries.has(prop)) {
3181
+ return {
3182
+ configurable: true,
3183
+ enumerable: true,
3184
+ value: this.get(_, prop, {})
3185
+ };
3186
+ }
3187
+ return undefined;
3188
+ }
3189
+ };
3190
+ return new Proxy({}, handler);
3191
+ }
3192
+ function createTemplateLayouts(layoutEntries) {
3193
+ const handler = {
3194
+ get(_, prop) {
3195
+ if (typeof prop === 'symbol') {
3196
+ return undefined;
3197
+ }
3198
+ if (prop === 'all') {
3199
+ return Array.from(layoutEntries.values()).map(entry => entry.element);
3200
+ }
3201
+ if (layoutEntries.has(prop)) {
3202
+ return layoutEntries.get(prop).element;
3203
+ }
3204
+ return undefined;
3205
+ },
3206
+ has(_, prop) {
3207
+ if (typeof prop === 'symbol') {
3208
+ return false;
3209
+ }
3210
+ return prop === 'all' || layoutEntries.has(prop);
3211
+ },
3212
+ ownKeys() {
3213
+ return ['all', ...layoutEntries.keys()];
3214
+ },
3215
+ getOwnPropertyDescriptor(_, prop) {
3216
+ if (typeof prop === 'symbol') {
3217
+ return undefined;
3218
+ }
3219
+ if (prop === 'all' || layoutEntries.has(prop)) {
3220
+ return {
3221
+ configurable: true,
3222
+ enumerable: true,
3223
+ value: this.get(_, prop, {})
3224
+ };
3225
+ }
3226
+ return undefined;
3227
+ }
3228
+ };
3229
+ return new Proxy({}, handler);
3230
+ }
3231
+
3133
3232
  const FieldRenderer = /*#__PURE__*/React.memo(t0 => {
3134
3233
  const $ = compilerRuntime.c(5);
3135
3234
  const {
@@ -3140,6 +3239,10 @@ const FieldRenderer = /*#__PURE__*/React.memo(t0 => {
3140
3239
  if (field.hidden) {
3141
3240
  return null;
3142
3241
  }
3242
+ if (!field.name) {
3243
+ console.error(`Field is missing required "name" property. Field type: "${field.type}". ` + "Every field must have a unique name for form state management.");
3244
+ return null;
3245
+ }
3143
3246
  let t1;
3144
3247
  let t2;
3145
3248
  if ($[0] !== field || $[1] !== form || $[2] !== namePrefix) {
@@ -3375,8 +3478,9 @@ const defaultValidators = {
3375
3478
  tel: stringValidator,
3376
3479
  number: field => {
3377
3480
  let schema = zod.z.coerce.number();
3378
- if (field.min !== undefined) schema = schema.min(field.min);
3379
- if (field.max !== undefined) schema = schema.max(field.max);
3481
+ const numberField = field;
3482
+ if (numberField.min !== undefined) schema = schema.min(numberField.min);
3483
+ if (numberField.max !== undefined) schema = schema.max(numberField.max);
3380
3484
  return schema;
3381
3485
  },
3382
3486
  checkbox: () => zod.z.coerce.boolean(),
@@ -3462,6 +3566,38 @@ function getItemKey(item, index) {
3462
3566
  }
3463
3567
  return item.name;
3464
3568
  }
3569
+ function extractFields(items) {
3570
+ const fields = [];
3571
+ for (const item of items) {
3572
+ if (isLayoutBlock(item)) {
3573
+ let childItems = [];
3574
+ if (item.type === 'columns') {
3575
+ for (const column of item.columns) {
3576
+ childItems.push(...column.children);
3577
+ }
3578
+ } else if (item.type === 'section') {
3579
+ childItems = item.children;
3580
+ }
3581
+ fields.push(...extractFields(childItems));
3582
+ } else {
3583
+ fields.push(item);
3584
+ }
3585
+ }
3586
+ return fields;
3587
+ }
3588
+ function extractLayouts(items) {
3589
+ const layouts = [];
3590
+ items.forEach((item, index) => {
3591
+ if (isLayoutBlock(item)) {
3592
+ layouts.push({
3593
+ layout: item,
3594
+ index
3595
+ });
3596
+ }
3597
+ });
3598
+ return layouts;
3599
+ }
3600
+
3465
3601
  function hasSchema(props) {
3466
3602
  return props.schema !== undefined;
3467
3603
  }
@@ -3473,6 +3609,11 @@ function FormGeneratorImpl(props) {
3473
3609
  submitText = 'Submit',
3474
3610
  disabled,
3475
3611
  mode = 'onChange',
3612
+ title,
3613
+ description,
3614
+ showReset = false,
3615
+ resetText = 'Reset',
3616
+ children,
3476
3617
  ref
3477
3618
  } = props;
3478
3619
  const userSchema = hasSchema(props) ? props.schema : undefined;
@@ -3496,6 +3637,9 @@ function FormGeneratorImpl(props) {
3496
3637
  const handleSubmit = form.handleSubmit(async data => {
3497
3638
  await onSubmit(data);
3498
3639
  });
3640
+ const handleReset = React.useCallback(() => {
3641
+ form.reset(mergedDefaultValues);
3642
+ }, [form, mergedDefaultValues]);
3499
3643
  React.useImperativeHandle(ref, () => ({
3500
3644
  setValues: values => {
3501
3645
  Object.entries(values).forEach(([key, value]) => {
@@ -3520,26 +3664,124 @@ function FormGeneratorImpl(props) {
3520
3664
  setError: (name, error) => form.setError(name, error),
3521
3665
  isValid: () => form.formState.isValid,
3522
3666
  isDirty: () => form.formState.isDirty,
3523
- form
3667
+ form: form
3524
3668
  }), [form, handleSubmit, mergedDefaultValues]);
3525
3669
  const SubmitButton = getFormComponent('SubmitButton');
3670
+ const fieldEntries = React.useMemo(() => {
3671
+ const entries = new Map();
3672
+ const allFields = extractFields(fields);
3673
+ allFields.forEach((field, index) => {
3674
+ if (!field.hidden) {
3675
+ const element = jsxRuntime.jsx(FieldRenderer, {
3676
+ field: field
3677
+ }, field.name || `field-${index}`);
3678
+ entries.set(field.name, {
3679
+ field,
3680
+ element,
3681
+ accessed: false
3682
+ });
3683
+ }
3684
+ });
3685
+ return entries;
3686
+ }, [fields]);
3687
+ const layoutEntries = React.useMemo(() => {
3688
+ const entries_0 = new Map();
3689
+ const allLayouts = extractLayouts(fields);
3690
+ allLayouts.forEach(({
3691
+ layout,
3692
+ index: index_0
3693
+ }) => {
3694
+ if (!layout.hidden) {
3695
+ const key_0 = layout.name || `layout-${layout.type}-${index_0}`;
3696
+ const element_0 = jsxRuntime.jsx(FormItemRenderer, {
3697
+ item: layout
3698
+ }, key_0);
3699
+ if (layout.name) {
3700
+ entries_0.set(layout.name, {
3701
+ layout,
3702
+ element: element_0
3703
+ });
3704
+ }
3705
+ }
3706
+ });
3707
+ return entries_0;
3708
+ }, [fields]);
3709
+ const templateFields = React.useMemo(() => createTemplateFields(fieldEntries), [fieldEntries]);
3710
+ const templateLayouts = React.useMemo(() => createTemplateLayouts(layoutEntries), [layoutEntries]);
3711
+ const buttons = React.useMemo(() => {
3712
+ const result = {
3713
+ submit: jsxRuntime.jsx(SubmitButton, {
3714
+ disabled: disabled || form.formState.isSubmitting,
3715
+ isSubmitting: form.formState.isSubmitting,
3716
+ children: submitText
3717
+ }, "submit")
3718
+ };
3719
+ if (showReset) {
3720
+ result.reset = jsxRuntime.jsx("button", {
3721
+ type: "button",
3722
+ onClick: handleReset,
3723
+ disabled: disabled,
3724
+ children: resetText
3725
+ }, "reset");
3726
+ }
3727
+ return result;
3728
+ }, [SubmitButton, disabled, form.formState.isSubmitting, submitText, showReset, resetText, handleReset]);
3729
+ const renderField = React.useCallback((field_0, options) => {
3730
+ return jsxRuntime.jsx(FieldRenderer, {
3731
+ field: field_0,
3732
+ namePrefix: options === null || options === void 0 ? void 0 : options.namePrefix
3733
+ });
3734
+ }, []);
3735
+ const renderLayout = React.useCallback((layout_0, options_0) => {
3736
+ return jsxRuntime.jsx(FormItemRenderer, {
3737
+ item: layout_0,
3738
+ namePrefix: options_0 === null || options_0 === void 0 ? void 0 : options_0.namePrefix
3739
+ });
3740
+ }, []);
3741
+ if (!children) {
3742
+ return jsxRuntime.jsx(FormProvider, {
3743
+ ...form,
3744
+ children: jsxRuntime.jsxs("form", {
3745
+ ref: formRef,
3746
+ onSubmit: handleSubmit,
3747
+ className: className,
3748
+ children: [fields.map((item, index_1) => jsxRuntime.jsx(FormItemRenderer, {
3749
+ item: item
3750
+ }, getItemKey(item, index_1))), jsxRuntime.jsx(SubmitButton, {
3751
+ disabled: disabled || form.formState.isSubmitting,
3752
+ isSubmitting: form.formState.isSubmitting,
3753
+ children: submitText
3754
+ })]
3755
+ })
3756
+ });
3757
+ }
3758
+ const renderProps = {
3759
+ fields: templateFields,
3760
+ layouts: templateLayouts,
3761
+ buttons,
3762
+ title,
3763
+ description,
3764
+ form: form,
3765
+ isSubmitting: form.formState.isSubmitting,
3766
+ isValid: form.formState.isValid,
3767
+ isDirty: form.formState.isDirty,
3768
+ renderField,
3769
+ renderLayout
3770
+ };
3771
+ const renderFn = children;
3526
3772
  return jsxRuntime.jsx(FormProvider, {
3527
3773
  ...form,
3528
- children: jsxRuntime.jsxs("form", {
3774
+ children: jsxRuntime.jsx("form", {
3529
3775
  ref: formRef,
3530
3776
  onSubmit: handleSubmit,
3531
3777
  className: className,
3532
- children: [fields.map((item, index) => jsxRuntime.jsx(FormItemRenderer, {
3533
- item: item
3534
- }, getItemKey(item, index))), jsxRuntime.jsx(SubmitButton, {
3535
- disabled: disabled || form.formState.isSubmitting,
3536
- isSubmitting: form.formState.isSubmitting,
3537
- children: submitText
3538
- })]
3778
+ children: renderFn(renderProps)
3539
3779
  })
3540
3780
  });
3541
3781
  }
3542
- const FormGenerator = FormGeneratorImpl;
3782
+ function FormGenerator(props) {
3783
+ return FormGeneratorImpl(props);
3784
+ }
3543
3785
 
3544
3786
  Object.defineProperty(exports, "z", {
3545
3787
  enumerable: true,