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 +642 -142
- package/dist/components/make-select/make-select.js +5 -2
- package/dist/src/components/make-description/make-description.js +1 -1
- package/dist/src/components/make-input/make-input.js +16 -27
- package/dist/src/components/make-select/make-select.js +23 -40
- package/dist/src/components/make-table/make-table.js +48 -62
- package/dist/src/components/make-textarea/make-textarea.js +16 -27
- package/dist/src/components/make-toggle/make-toggle.js +16 -27
- package/dist/src/components/portal/portal.js +27 -16
- package/dist/src/hooks/use-conditional-field.d.ts +28 -0
- package/dist/src/hooks/use-conditional-field.js +62 -0
- package/dist/src/modal.js +57 -39
- package/dist/src/util/general/general.util.js +8 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,203 +1,253 @@
|
|
|
1
1
|
# dynamic-modal
|
|
2
2
|
|
|
3
|
-
`dynamic-modal` is a
|
|
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
|
-
|
|
7
|
+
It is designed for projects that want:
|
|
6
8
|
|
|
7
|
-
|
|
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
|
-
|
|
16
|
+
## Compatibility
|
|
10
17
|
|
|
11
|
-
|
|
18
|
+
According to `package.json`, this library is compatible with:
|
|
12
19
|
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
```
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
```
|
|
142
|
-
|
|
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
|
|
152
|
-
<
|
|
153
|
-
|
|
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
|
-
|
|
163
|
-
Edit file named `_document.tsx` and define `portal` for modal (this component use react portal)
|
|
205
|
+
### Next.js Pages Router
|
|
164
206
|
|
|
165
|
-
```
|
|
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=
|
|
216
|
+
<div id="modal-portal" />
|
|
175
217
|
<NextScript />
|
|
176
218
|
</body>
|
|
177
219
|
</Html>
|
|
178
|
-
)
|
|
220
|
+
);
|
|
179
221
|
}
|
|
180
222
|
```
|
|
181
223
|
|
|
182
|
-
##
|
|
183
|
-
|
|
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
|
-
|
|
186
|
-
import {
|
|
187
|
-
import
|
|
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
|
|
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(
|
|
199
|
-
|
|
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
|
-
|
|
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
|
-
|
|
216
|
-
|
|
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.
|