dynamic-modal 1.1.23 → 1.1.25

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,203 +1,253 @@
1
1
  # dynamic-modal
2
2
 
3
- `dynamic-modal` is a TypeScript library for creating reusable modals in React and Next.js applications. It uses JSON objects to configure the modal structure, eliminating the need to write HTML. This approach simplifies modal creation and customization, allowing you to open and close modals using a custom hook.
3
+ `dynamic-modal` is a React library for building configurable modals from JSON.
4
+ Instead of hand-writing a modal UI every time, you describe fields, actions, and
5
+ conditional behavior in a config object and open it through a hook.
4
6
 
5
- ## Requirements
7
+ It is designed for projects that want:
6
8
 
7
- To use `dynamic-modal` properly, ensure you have the following dependencies installed:
9
+ - reusable modal definitions
10
+ - dynamic forms inside modals
11
+ - conditional rendering with `renderIf`
12
+ - conditional enabling with `enableIf`
13
+ - dependent remote options with `liveData`
14
+ - full UI customization through your own design system components
8
15
 
9
- - React
16
+ ## Compatibility
10
17
 
11
- Additionally, `dynamic-modal` is compatible with **Next.js**.
18
+ According to `package.json`, this library is compatible with:
12
19
 
13
- ## Installation
20
+ - `react`: `^18.0.0 || ^19.0.0`
21
+ - `react-dom`: `^18.0.0 || ^19.0.0`
22
+ - `react-hook-form`: `^7.54.2`
23
+
24
+ The library itself is currently built with:
25
+
26
+ - `react`: `^19.0.0`
27
+ - `react-dom`: `^19.0.0`
28
+ - `react-hook-form`: `^7.54.2`
14
29
 
15
- Install the library via npm:
30
+ It works well in React apps and in Next.js projects that support client
31
+ components.
32
+
33
+ ## Installation
16
34
 
17
35
  ```bash
18
36
  npm install dynamic-modal
19
37
  ```
20
38
 
21
- ## Setup for Next.js (14 or 15)
22
- Define your components for use inside modal, create file and configure all modal components. Here´s an example using HeroUI (previously NextUI):
39
+ If your project does not already include the required peers, install them too:
23
40
 
24
- ```jsx
25
- 'use client'
26
- import { Autocomplete, AutocompleteItem, Button, Input, Select, SelectItem, Switch, Textarea } from "@heroui/react"
27
- import { IComponentState } from "dynamic-modal/src/interfaces/component-state"
41
+ ```bash
42
+ npm install react react-dom react-hook-form
43
+ ```
28
44
 
29
- export const ModalComponents: IComponentState = {
30
- ModalButtonCancel: (props) => {
31
- return (
32
- <Button
33
- {...props}
34
- color={props.color as "default" | "primary" | "secondary" | "success" | "warning" | "danger" | undefined}
35
- variant={'bordered'}
36
- >
37
- {props.text}
38
- </Button>
39
- )
40
- },
41
- ModalButtonAction: (props) => {
42
- return (
43
- <Button
44
- {...props}
45
- color={props.color as "default" | "primary" | "secondary" | "success" | "warning" | "danger" | undefined}
46
- variant={'solid'}
47
- >
48
- {props.text}
49
- </Button>)
50
- },
51
- Button: ({ text, ...props }) => {
52
- return (
53
- <Button
54
- {...props}
55
- color={props.color as "default" | "primary" | "secondary" | "success" | "warning" | "danger" | undefined}
56
- variant={props.variant as "flat" | "bordered" | "solid" | "light" | "faded" | "shadow" | "ghost" | undefined}
57
- >
58
- {text}
59
- </Button>)
60
- },
61
- Input: ({ invalid, error, disabled, onChange, ...props }) => {
62
- return (
63
- <Input
64
- {...props}
65
- onValueChange={onChange}
66
- errorMessage={error?.message}
67
- isInvalid={invalid}
68
- isDisabled={disabled}
69
- />
70
- )
71
- },
72
- Select: ({ options, invalid, error, isMulti, isSearch, disabled, onChange, value, ...props }) => {
73
- return (
74
- !isSearch ?
75
- <Select
45
+ ## Exports
46
+
47
+ The package exposes:
48
+
49
+ - `DynamicModal`
50
+ - `useModalHandler`
51
+ - `ComponentState`
52
+ - `ComponentStateContext`
53
+ - `IComponentState`
54
+ - `IModalConfigLoader`
55
+ - `IModalConfigProps`
56
+ - `IModalRenderCondition`
57
+ - `IModalField`
58
+ - `IModalLiveDataCondition`
59
+ - `IOption`
60
+
61
+ ## Mental model
62
+
63
+ You use the library in 4 steps:
64
+
65
+ 1. Define the UI components the modal should use in your app.
66
+ 2. Wrap your app with `ComponentState`.
67
+ 3. Render `DynamicModal` and control it with `useModalHandler`.
68
+ 4. Build modal configs as plain objects and open them when needed.
69
+
70
+ ## 1. Provide your own components
71
+
72
+ `dynamic-modal` does not force a UI kit on you.
73
+ You provide your own inputs, selects, buttons, toggles, and textarea components
74
+ through `ComponentState`, so the modal matches your app visually.
75
+
76
+ Example:
77
+
78
+ ```tsx
79
+ 'use client';
80
+
81
+ import { ReactNode } from 'react';
82
+ import {
83
+ Autocomplete,
84
+ AutocompleteItem,
85
+ Button,
86
+ Input,
87
+ Select,
88
+ SelectItem,
89
+ Switch,
90
+ Textarea,
91
+ } from '@heroui/react';
92
+ import type { IComponentState } from 'dynamic-modal';
93
+
94
+ export const modalComponents: IComponentState = {
95
+ ModalButtonCancel: ({ text, color, ...props }) => (
96
+ <Button {...props} color={color as any} variant="bordered">
97
+ {text}
98
+ </Button>
99
+ ),
100
+ ModalButtonAction: ({ text, color, ...props }) => (
101
+ <Button {...props} color={color as any} variant="solid">
102
+ {text}
103
+ </Button>
104
+ ),
105
+ Button: ({ text, color, variant, ...props }) => (
106
+ <Button {...props} color={color as any} variant={variant as any}>
107
+ {text}
108
+ </Button>
109
+ ),
110
+ Input: ({ invalid, error, disabled, onChange, value, ...props }) => (
111
+ <Input
112
+ {...props}
113
+ value={value ?? ''}
114
+ onValueChange={onChange}
115
+ errorMessage={error?.message}
116
+ isInvalid={invalid}
117
+ isDisabled={disabled}
118
+ />
119
+ ),
120
+ Select: ({
121
+ options,
122
+ invalid,
123
+ error,
124
+ isMulti,
125
+ isSearch,
126
+ disabled,
127
+ onChange,
128
+ value,
129
+ ...props
130
+ }) =>
131
+ isSearch ? (
132
+ <Autocomplete
76
133
  {...props}
77
- selectedKeys={isMulti ? value : [value]}
78
- onSelectionChange={onChange}
79
- selectionMode={isMulti ? 'multiple' : 'single'}
80
- errorMessage={error?.message}
81
- isInvalid={invalid}
82
- isDisabled={disabled}
83
- >
84
- {options.map((option) => (
85
- <SelectItem key={option.id}>{option.name}</SelectItem>
86
- ))}
87
- </Select> :
88
- <Autocomplete
89
- {...props}
90
134
  selectedKey={value}
91
- onSelectionChange={onChange}
135
+ onSelectionChange={onChange as any}
92
136
  errorMessage={error?.message}
93
137
  isInvalid={invalid}
94
138
  isDisabled={disabled}
95
- >
139
+ >
96
140
  {options.map((item) => (
97
141
  <AutocompleteItem key={item.id}>{item.name}</AutocompleteItem>
98
142
  ))}
99
143
  </Autocomplete>
100
- )
101
- },
102
- Textarea: ({ invalid, error, disabled, ...props }) => {
103
- return (
104
- <Textarea
144
+ ) : (
145
+ <Select
105
146
  {...props}
147
+ selectedKeys={isMulti ? (value ?? []) : value ? [value] : []}
148
+ onSelectionChange={onChange as any}
149
+ selectionMode={isMulti ? 'multiple' : 'single'}
106
150
  errorMessage={error?.message}
107
151
  isInvalid={invalid}
108
152
  isDisabled={disabled}
109
- />
110
- )
111
- },
112
- Toggle: ({ value, onChange, label, invalid, ...props }) => {
113
- return(
114
- <Switch {...props} isSelected={value} onValueChange={onChange}>
115
- {label}
116
- </Switch>
117
- )
118
- }
153
+ >
154
+ {options.map((option) => (
155
+ <SelectItem key={option.id}>{option.name}</SelectItem>
156
+ ))}
157
+ </Select>
158
+ ),
159
+ Textarea: ({ invalid, error, disabled, value, onChange, ...props }) => (
160
+ <Textarea
161
+ {...props}
162
+ value={value ?? ''}
163
+ onValueChange={onChange}
164
+ errorMessage={error?.message}
165
+ isInvalid={invalid}
166
+ isDisabled={disabled}
167
+ />
168
+ ),
169
+ Toggle: ({ value, onChange, label, ...props }) => (
170
+ <Switch {...props} isSelected={!!value} onValueChange={onChange}>
171
+ {label}
172
+ </Switch>
173
+ ),
174
+ };
175
+
176
+ export function ModalProvider({ children }: { children: ReactNode }) {
177
+ return <ComponentState components={modalComponents}>{children}</ComponentState>;
119
178
  }
120
179
  ```
121
180
 
122
- In the main provider of your React application, import your modal components (defined previously) and wrap your app with the `ComponentState` component to ensure `dynamic-modal` functions properly. Here’s an example:
123
-
124
- ```jsx
125
- import { ComponentState } from 'dynamic-modal'
126
- import { ModalComponents } from "@data/modal-component/modal-component"
127
-
128
- function Provider({ children }: Readonly<{ children: ReactNode }>) {
129
- return (
130
- <ComponentState components={ModalComponents}>
131
- <Component {...pageProps} />
132
- </ComponentState>
133
- )
134
- }
181
+ ## 2. Add the provider and portal
135
182
 
136
- export default Provider
183
+ Wrap your app with `ComponentState` and add a portal target with the id
184
+ `modal-portal`.
137
185
 
138
- ```
139
- In the root layout define `portal` for modal (this component use react portal)
186
+ ### Next.js App Router
140
187
 
141
- ```jsx
142
- //imports...
188
+ ```tsx
189
+ import type { ReactNode } from 'react';
143
190
 
144
- export default function RootLayout ({
145
- children
146
- }: Readonly<{
147
- children: ReactNode
148
- }>) {
191
+ export default function RootLayout({
192
+ children,
193
+ }: Readonly<{ children: ReactNode }>) {
149
194
  return (
150
195
  <html lang="en">
151
- <body className={inter.className}>
152
- <Provider>
153
- {children}
154
- </Provider>
155
- <div id='modal-portal'/>
196
+ <body>
197
+ <ModalProvider>{children}</ModalProvider>
198
+ <div id="modal-portal" />
156
199
  </body>
157
200
  </html>
158
- )
201
+ );
159
202
  }
160
203
  ```
161
204
 
162
- ## Setup for Next.js 13 and old
163
- Edit file named `_document.tsx` and define `portal` for modal (this component use react portal)
205
+ ### Next.js Pages Router
164
206
 
165
- ```jsx
166
- import { Html, Head, Main, NextScript } from 'next/document'
207
+ ```tsx
208
+ import { Html, Head, Main, NextScript } from 'next/document';
167
209
 
168
- export default function Document () {
210
+ export default function Document() {
169
211
  return (
170
212
  <Html>
171
213
  <Head />
172
214
  <body>
173
215
  <Main />
174
- <div id='modal-portal'/>
216
+ <div id="modal-portal" />
175
217
  <NextScript />
176
218
  </body>
177
219
  </Html>
178
- )
220
+ );
179
221
  }
180
222
  ```
181
223
 
182
- ## Usage
183
- To control the modal’s open and close states, use the `useModalHandler` custom hook and call `openModal` whenever you need to display the modal.
224
+ ## 3. Render and control the modal
225
+
226
+ Use `useModalHandler` to open the modal and render `DynamicModal` once in your
227
+ page or component tree.
228
+
229
+ ```tsx
230
+ 'use client';
184
231
 
185
- ```jsx
186
- import { useModalHandler, DynamicModal } from 'dynamic-modal'
187
- import { Button } from '@nextui-org/react'
188
- //Create your modal, import and use
189
- import testModal from '@modal-config/test'
232
+ import { DynamicModal, useModalHandler } from 'dynamic-modal';
233
+ import { Button } from '@heroui/react';
234
+ import simpleModal from './modal-config/simple-modal';
190
235
 
191
- function ExampleComponent() {
192
- const { openModal, modalProps } = useModalHandler()
236
+ export default function ExamplePage() {
237
+ const { openModal, modalProps } = useModalHandler();
193
238
 
194
239
  return (
195
240
  <>
196
241
  <Button
197
242
  onClick={() => {
198
- openModal(testModal.default({}, (data) => {
199
- console.log('modal data', data)
200
- }))
243
+ openModal(
244
+ simpleModal.default(
245
+ { reserved: 'abc', input1: 'Initial value', store: false },
246
+ (data) => {
247
+ console.log('modal result', data);
248
+ },
249
+ ),
250
+ );
201
251
  }}
202
252
  >
203
253
  Open modal
@@ -205,12 +255,462 @@ function ExampleComponent() {
205
255
 
206
256
  <DynamicModal {...modalProps} />
207
257
  </>
208
- )
258
+ );
259
+ }
260
+ ```
261
+
262
+ ## 4. Create modal configs
263
+
264
+ The recommended pattern is to define modal configs with `IModalConfigLoader`.
265
+ This lets you:
266
+
267
+ - receive input props
268
+ - return a typed modal config
269
+ - receive typed modal output in the `action` callback
270
+
271
+ Basic example:
272
+
273
+ ```ts
274
+ import type { IModalConfigLoader } from 'dynamic-modal';
275
+
276
+ type IncomingProps = {
277
+ reserved: string;
278
+ input1: string;
279
+ store?: boolean;
280
+ clear?: boolean;
281
+ };
282
+
283
+ type ResultProps = IncomingProps;
284
+
285
+ const simpleModal: {
286
+ default: IModalConfigLoader<IncomingProps, ResultProps>;
287
+ } = {
288
+ default: (props, action) => ({
289
+ reservedData: {
290
+ reserved: props.reserved,
291
+ },
292
+ title: 'Basic modal',
293
+ style: {
294
+ width: '500px',
295
+ },
296
+ fields: [
297
+ {
298
+ elementType: 'input',
299
+ label: 'Input 1',
300
+ name: 'input1',
301
+ defaultValue: props.input1,
302
+ validation: {
303
+ required: true,
304
+ message: 'This field is required',
305
+ },
306
+ },
307
+ {
308
+ elementType: 'group',
309
+ groups: [
310
+ {
311
+ elementType: 'toggle',
312
+ label: 'Store',
313
+ name: 'store',
314
+ defaultValue: `${props.store ?? false}`,
315
+ style: { width: '50%' },
316
+ validation: {
317
+ required: false,
318
+ },
319
+ },
320
+ {
321
+ elementType: 'toggle',
322
+ label: 'Clear',
323
+ name: 'clear',
324
+ defaultValue: `${props.clear ?? false}`,
325
+ style: { width: '50%' },
326
+ validation: {
327
+ required: false,
328
+ },
329
+ },
330
+ ],
331
+ },
332
+ ],
333
+ out: action,
334
+ actions: {
335
+ action: { text: 'Save', color: 'primary' },
336
+ cancel: { text: 'Cancel', color: 'danger' },
337
+ },
338
+ }),
339
+ };
340
+
341
+ export default simpleModal;
342
+ ```
343
+
344
+ ## Supported field types
345
+
346
+ You can build modal UIs with these field types:
347
+
348
+ - `input`
349
+ - `select`
350
+ - `textarea`
351
+ - `toggle`
352
+ - `text`
353
+ - `upload`
354
+ - `custom-upload`
355
+ - `watcher`
356
+ - `button`
357
+ - `table`
358
+ - `group`
359
+
360
+ `group` lets you place multiple fields in the same row.
361
+
362
+ ## Conditional behavior
363
+
364
+ One of the main strengths of the library is dynamic behavior based on form
365
+ state.
366
+
367
+ ### `renderIf`
368
+
369
+ Use `renderIf` when a field should appear only if another field matches one or
370
+ more values.
371
+
372
+ ```ts
373
+ {
374
+ elementType: 'input',
375
+ label: 'Company name',
376
+ name: 'companyName',
377
+ validation: {
378
+ required: true,
379
+ message: 'Write a company name',
380
+ },
381
+ renderIf: {
382
+ personType: ['company'],
383
+ },
384
+ }
385
+ ```
386
+
387
+ You can also use `'*'` as a wildcard:
388
+
389
+ ```ts
390
+ renderIf: {
391
+ personType: ['*'],
392
+ }
393
+ ```
394
+
395
+ ### `enableIf`
396
+
397
+ Use `enableIf` when a field should stay visible but only become editable if a
398
+ condition is met.
399
+
400
+ ```ts
401
+ {
402
+ elementType: 'input',
403
+ label: 'Discount code',
404
+ name: 'discountCode',
405
+ validation: {
406
+ required: false,
407
+ },
408
+ enableIf: {
409
+ hasDiscount: ['true'],
410
+ },
411
+ }
412
+ ```
413
+
414
+ ### `liveData`
415
+
416
+ Use `liveData` when one field depends on another and must fetch options
417
+ dynamically.
418
+
419
+ ```ts
420
+ {
421
+ elementType: 'select',
422
+ label: 'City',
423
+ name: 'cityId',
424
+ options: [],
425
+ validation: {
426
+ required: true,
427
+ message: 'Select a city',
428
+ },
429
+ liveData: {
430
+ condition: ['countryId'],
431
+ action: async (countryId, formData) => {
432
+ const response = await fetch(`/api/cities?countryId=${countryId}`);
433
+ const data = await response.json();
434
+
435
+ return data.map((city: { id: string; name: string }) => ({
436
+ id: city.id,
437
+ name: city.name,
438
+ }));
439
+ },
440
+ },
441
+ }
442
+ ```
443
+
444
+ ### `watcher`
445
+
446
+ Use `watcher` when you want to display a derived read-only value built from
447
+ other fields in the same modal.
448
+
449
+ `watcher` listens to the fields listed in `watchList`, joins their current
450
+ values, and renders the result using your custom `Input` component in disabled
451
+ mode.
452
+
453
+ Example:
454
+
455
+ ```ts
456
+ {
457
+ elementType: 'watcher',
458
+ label: 'Full name preview',
459
+ watchList: ['firstName', 'middleName', 'lastName'],
460
+ style: {
461
+ width: '100%',
462
+ },
463
+ }
464
+ ```
465
+
466
+ Typical use cases:
467
+
468
+ - preview a full name from multiple inputs
469
+ - build a quick summary field for the user
470
+ - show a composed display value without storing it as a real form field
471
+
472
+ ## Advanced conditions with async actions
473
+
474
+ `renderIf` and `enableIf` can also use async logic instead of static value maps.
475
+ This is useful if the decision depends on the backend or on custom business
476
+ rules.
477
+
478
+ Example:
479
+
480
+ ```ts
481
+ renderIf: {
482
+ condition: ['customerId'],
483
+ action: async (customerId, formData) => {
484
+ const response = await fetch(`/api/customers/${customerId}/can-edit`);
485
+ const data = await response.json();
486
+ return data.allowed;
487
+ },
209
488
  }
489
+ ```
490
+
491
+ The same shape works for `enableIf`.
492
+
493
+ ## Examples by use case
210
494
 
211
- export default ExampleComponent
495
+ ### 1. Basic modal
212
496
 
497
+ Use this when you just need a standard modal with fixed fields.
498
+
499
+ ```ts
500
+ fields: [
501
+ {
502
+ elementType: 'input',
503
+ label: 'Name',
504
+ name: 'name',
505
+ validation: { required: true, message: 'Required' },
506
+ },
507
+ {
508
+ elementType: 'textarea',
509
+ label: 'Description',
510
+ name: 'description',
511
+ validation: { required: false },
512
+ },
513
+ ];
514
+ ```
515
+
516
+ ### 2. Render fields depending on a select
517
+
518
+ Use `renderIf` for mutually exclusive sections.
519
+
520
+ ```ts
521
+ fields: [
522
+ {
523
+ elementType: 'select',
524
+ label: 'Mode',
525
+ name: 'mode',
526
+ defaultValue: 'email',
527
+ options: [
528
+ { id: 'email', name: 'Email' },
529
+ { id: 'sms', name: 'SMS' },
530
+ ],
531
+ validation: { required: true, message: 'Select a mode' },
532
+ },
533
+ {
534
+ elementType: 'input',
535
+ label: 'Email',
536
+ name: 'email',
537
+ validation: { required: true, message: 'Write an email' },
538
+ renderIf: { mode: ['email'] },
539
+ },
540
+ {
541
+ elementType: 'input',
542
+ label: 'Phone',
543
+ name: 'phone',
544
+ validation: { required: true, message: 'Write a phone' },
545
+ renderIf: { mode: ['sms'] },
546
+ },
547
+ ];
213
548
  ```
214
549
 
215
- ## Examples
216
- The examples folder in the repository contains different configuration modes to help you customize your modal.
550
+ ### 3. Keep the field visible but disabled
551
+
552
+ Use `enableIf` if the user should see the field before it becomes available.
553
+
554
+ ```ts
555
+ {
556
+ elementType: 'input',
557
+ label: 'Approval note',
558
+ name: 'approvalNote',
559
+ validation: { required: false },
560
+ enableIf: {
561
+ status: ['approved'],
562
+ },
563
+ }
564
+ ```
565
+
566
+ ### 4. Load options from another field
567
+
568
+ Use `liveData` for dependent selects.
569
+
570
+ ```ts
571
+ fields: [
572
+ {
573
+ elementType: 'select',
574
+ label: 'Type',
575
+ name: 'typeId',
576
+ options: props.typeList,
577
+ validation: {
578
+ required: true,
579
+ message: 'Please select a valid type',
580
+ },
581
+ },
582
+ {
583
+ elementType: 'select',
584
+ label: 'Options',
585
+ name: 'optionId',
586
+ options: [],
587
+ validation: {
588
+ required: true,
589
+ message: 'Please select a valid option',
590
+ },
591
+ liveData: {
592
+ condition: ['typeId'],
593
+ action: props.optionReadAction,
594
+ },
595
+ },
596
+ ];
597
+ ```
598
+
599
+ ### 5. Reserve data that should travel with the result
600
+
601
+ Use `reservedData` when you want to preserve contextual information without
602
+ showing it in the modal.
603
+
604
+ ```ts
605
+ reservedData: {
606
+ customerId: props.customerId,
607
+ source: 'customer-profile',
608
+ }
609
+ ```
610
+
611
+ That data will be merged into the object returned by `out`.
612
+
613
+ ### 6. Compose a read-only value with `watcher`
614
+
615
+ Use `watcher` when you want the modal to display a value derived from multiple
616
+ fields while the user types.
617
+
618
+ ```ts
619
+ fields: [
620
+ {
621
+ elementType: 'input',
622
+ label: 'First name',
623
+ name: 'firstName',
624
+ validation: { required: true, message: 'Required' },
625
+ },
626
+ {
627
+ elementType: 'input',
628
+ label: 'Last name',
629
+ name: 'lastName',
630
+ validation: { required: true, message: 'Required' },
631
+ },
632
+ {
633
+ elementType: 'watcher',
634
+ label: 'Preview',
635
+ watchList: ['firstName', 'lastName'],
636
+ style: { width: '100%' },
637
+ },
638
+ ];
639
+ ```
640
+
641
+ Important notes:
642
+
643
+ - `watcher` is display-only
644
+ - it does not submit its own value in the modal result
645
+ - it is useful for previews, concatenations, and human-readable summaries
646
+
647
+ ## Configuration reference
648
+
649
+ ### Modal-level config
650
+
651
+ Common properties of `IModalConfigProps`:
652
+
653
+ - `title`: modal title
654
+ - `fields`: list of modal elements
655
+ - `out`: callback invoked on submit
656
+ - `reservedData`: extra data merged into the result
657
+ - `onClose`: callback when the modal closes
658
+ - `style`: styles for the modal container
659
+ - `overFlowBody`: body height/overflow control
660
+ - `minHeightBody`: minimum body height
661
+ - `useSubmit`: if `false`, action button uses manual validation mode
662
+ - `useBlur`: enables backdrop blur style
663
+ - `actions.action`: main action button props
664
+ - `actions.cancel`: optional cancel button props
665
+ - `actions.containerStyle`: style for the action buttons container
666
+
667
+ ### Common field properties
668
+
669
+ Most form fields share:
670
+
671
+ - `name`
672
+ - `label`
673
+ - `placeholder`
674
+ - `defaultValue`
675
+ - `style`
676
+ - `disabled`
677
+ - `validation`
678
+ - `renderIf`
679
+ - `enableIf`
680
+
681
+ `watcher` uses:
682
+
683
+ - `label`
684
+ - `style`
685
+ - `watchList`
686
+
687
+ Validation supports:
688
+
689
+ - `required`
690
+ - `message`
691
+ - `regex`
692
+ - `maxLength`
693
+ - `minLength`
694
+ - `min`
695
+ - `max`
696
+
697
+ ## Notes and recommendations
698
+
699
+ - Render `DynamicModal` only once per screen or page branch when possible.
700
+ - Prefer stable `name` values because they are used to manage form state.
701
+ - Use `renderIf` for hidden sections and `enableIf` for visible-but-locked
702
+ sections.
703
+ - Keep `liveData` actions fast and deterministic when possible.
704
+ - If your custom UI components use different event contracts, adapt them inside
705
+ `ComponentState` rather than changing modal configs.
706
+
707
+ ## Repository examples
708
+
709
+ This repository includes working examples in:
710
+
711
+ - `examples/simple.ts`
712
+ - `examples/render-if.ts`
713
+ - `examples/enable-if.ts`
714
+ - `examples/live-data.ts`
715
+
716
+ These are useful starting points for building your own modal catalog.