dynamic-modal 1.1.23 → 1.1.24
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 +574 -142
- 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 +21 -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 +61 -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
|
-
|
|
181
|
+
## 2. Add the provider and portal
|
|
123
182
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
import { ModalComponents } from "@data/modal-component/modal-component"
|
|
183
|
+
Wrap your app with `ComponentState` and add a portal target with the id
|
|
184
|
+
`modal-portal`.
|
|
127
185
|
|
|
128
|
-
|
|
129
|
-
return (
|
|
130
|
-
<ComponentState components={ModalComponents}>
|
|
131
|
-
<Component {...pageProps} />
|
|
132
|
-
</ComponentState>
|
|
133
|
-
)
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export default Provider
|
|
186
|
+
### Next.js App Router
|
|
137
187
|
|
|
138
|
-
```
|
|
139
|
-
|
|
188
|
+
```tsx
|
|
189
|
+
import type { ReactNode } from 'react';
|
|
140
190
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
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
|
|
184
225
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
import { Button } from '@nextui-org/react'
|
|
188
|
-
//Create your modal, import and use
|
|
189
|
-
import testModal from '@modal-config/test'
|
|
226
|
+
Use `useModalHandler` to open the modal and render `DynamicModal` once in your
|
|
227
|
+
page or component tree.
|
|
190
228
|
|
|
191
|
-
|
|
192
|
-
|
|
229
|
+
```tsx
|
|
230
|
+
'use client';
|
|
231
|
+
|
|
232
|
+
import { DynamicModal, useModalHandler } from 'dynamic-modal';
|
|
233
|
+
import { Button } from '@heroui/react';
|
|
234
|
+
import simpleModal from './modal-config/simple-modal';
|
|
235
|
+
|
|
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,394 @@ 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
|
+
## Advanced conditions with async actions
|
|
445
|
+
|
|
446
|
+
`renderIf` and `enableIf` can also use async logic instead of static value maps.
|
|
447
|
+
This is useful if the decision depends on the backend or on custom business
|
|
448
|
+
rules.
|
|
449
|
+
|
|
450
|
+
Example:
|
|
451
|
+
|
|
452
|
+
```ts
|
|
453
|
+
renderIf: {
|
|
454
|
+
condition: ['customerId'],
|
|
455
|
+
action: async (customerId, formData) => {
|
|
456
|
+
const response = await fetch(`/api/customers/${customerId}/can-edit`);
|
|
457
|
+
const data = await response.json();
|
|
458
|
+
return data.allowed;
|
|
459
|
+
},
|
|
460
|
+
}
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
The same shape works for `enableIf`.
|
|
464
|
+
|
|
465
|
+
## Examples by use case
|
|
466
|
+
|
|
467
|
+
### 1. Basic modal
|
|
468
|
+
|
|
469
|
+
Use this when you just need a standard modal with fixed fields.
|
|
470
|
+
|
|
471
|
+
```ts
|
|
472
|
+
fields: [
|
|
473
|
+
{
|
|
474
|
+
elementType: 'input',
|
|
475
|
+
label: 'Name',
|
|
476
|
+
name: 'name',
|
|
477
|
+
validation: { required: true, message: 'Required' },
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
elementType: 'textarea',
|
|
481
|
+
label: 'Description',
|
|
482
|
+
name: 'description',
|
|
483
|
+
validation: { required: false },
|
|
484
|
+
},
|
|
485
|
+
];
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
### 2. Render fields depending on a select
|
|
489
|
+
|
|
490
|
+
Use `renderIf` for mutually exclusive sections.
|
|
491
|
+
|
|
492
|
+
```ts
|
|
493
|
+
fields: [
|
|
494
|
+
{
|
|
495
|
+
elementType: 'select',
|
|
496
|
+
label: 'Mode',
|
|
497
|
+
name: 'mode',
|
|
498
|
+
defaultValue: 'email',
|
|
499
|
+
options: [
|
|
500
|
+
{ id: 'email', name: 'Email' },
|
|
501
|
+
{ id: 'sms', name: 'SMS' },
|
|
502
|
+
],
|
|
503
|
+
validation: { required: true, message: 'Select a mode' },
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
elementType: 'input',
|
|
507
|
+
label: 'Email',
|
|
508
|
+
name: 'email',
|
|
509
|
+
validation: { required: true, message: 'Write an email' },
|
|
510
|
+
renderIf: { mode: ['email'] },
|
|
511
|
+
},
|
|
512
|
+
{
|
|
513
|
+
elementType: 'input',
|
|
514
|
+
label: 'Phone',
|
|
515
|
+
name: 'phone',
|
|
516
|
+
validation: { required: true, message: 'Write a phone' },
|
|
517
|
+
renderIf: { mode: ['sms'] },
|
|
518
|
+
},
|
|
519
|
+
];
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
### 3. Keep the field visible but disabled
|
|
523
|
+
|
|
524
|
+
Use `enableIf` if the user should see the field before it becomes available.
|
|
525
|
+
|
|
526
|
+
```ts
|
|
527
|
+
{
|
|
528
|
+
elementType: 'input',
|
|
529
|
+
label: 'Approval note',
|
|
530
|
+
name: 'approvalNote',
|
|
531
|
+
validation: { required: false },
|
|
532
|
+
enableIf: {
|
|
533
|
+
status: ['approved'],
|
|
534
|
+
},
|
|
209
535
|
}
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
### 4. Load options from another field
|
|
210
539
|
|
|
211
|
-
|
|
540
|
+
Use `liveData` for dependent selects.
|
|
212
541
|
|
|
542
|
+
```ts
|
|
543
|
+
fields: [
|
|
544
|
+
{
|
|
545
|
+
elementType: 'select',
|
|
546
|
+
label: 'Type',
|
|
547
|
+
name: 'typeId',
|
|
548
|
+
options: props.typeList,
|
|
549
|
+
validation: {
|
|
550
|
+
required: true,
|
|
551
|
+
message: 'Please select a valid type',
|
|
552
|
+
},
|
|
553
|
+
},
|
|
554
|
+
{
|
|
555
|
+
elementType: 'select',
|
|
556
|
+
label: 'Options',
|
|
557
|
+
name: 'optionId',
|
|
558
|
+
options: [],
|
|
559
|
+
validation: {
|
|
560
|
+
required: true,
|
|
561
|
+
message: 'Please select a valid option',
|
|
562
|
+
},
|
|
563
|
+
liveData: {
|
|
564
|
+
condition: ['typeId'],
|
|
565
|
+
action: props.optionReadAction,
|
|
566
|
+
},
|
|
567
|
+
},
|
|
568
|
+
];
|
|
213
569
|
```
|
|
214
570
|
|
|
215
|
-
|
|
216
|
-
|
|
571
|
+
### 5. Reserve data that should travel with the result
|
|
572
|
+
|
|
573
|
+
Use `reservedData` when you want to preserve contextual information without
|
|
574
|
+
showing it in the modal.
|
|
575
|
+
|
|
576
|
+
```ts
|
|
577
|
+
reservedData: {
|
|
578
|
+
customerId: props.customerId,
|
|
579
|
+
source: 'customer-profile',
|
|
580
|
+
}
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
That data will be merged into the object returned by `out`.
|
|
584
|
+
|
|
585
|
+
## Configuration reference
|
|
586
|
+
|
|
587
|
+
### Modal-level config
|
|
588
|
+
|
|
589
|
+
Common properties of `IModalConfigProps`:
|
|
590
|
+
|
|
591
|
+
- `title`: modal title
|
|
592
|
+
- `fields`: list of modal elements
|
|
593
|
+
- `out`: callback invoked on submit
|
|
594
|
+
- `reservedData`: extra data merged into the result
|
|
595
|
+
- `onClose`: callback when the modal closes
|
|
596
|
+
- `style`: styles for the modal container
|
|
597
|
+
- `overFlowBody`: body height/overflow control
|
|
598
|
+
- `minHeightBody`: minimum body height
|
|
599
|
+
- `useSubmit`: if `false`, action button uses manual validation mode
|
|
600
|
+
- `useBlur`: enables backdrop blur style
|
|
601
|
+
- `actions.action`: main action button props
|
|
602
|
+
- `actions.cancel`: optional cancel button props
|
|
603
|
+
- `actions.containerStyle`: style for the action buttons container
|
|
604
|
+
|
|
605
|
+
### Common field properties
|
|
606
|
+
|
|
607
|
+
Most form fields share:
|
|
608
|
+
|
|
609
|
+
- `name`
|
|
610
|
+
- `label`
|
|
611
|
+
- `placeholder`
|
|
612
|
+
- `defaultValue`
|
|
613
|
+
- `style`
|
|
614
|
+
- `disabled`
|
|
615
|
+
- `validation`
|
|
616
|
+
- `renderIf`
|
|
617
|
+
- `enableIf`
|
|
618
|
+
|
|
619
|
+
Validation supports:
|
|
620
|
+
|
|
621
|
+
- `required`
|
|
622
|
+
- `message`
|
|
623
|
+
- `regex`
|
|
624
|
+
- `maxLength`
|
|
625
|
+
- `minLength`
|
|
626
|
+
- `min`
|
|
627
|
+
- `max`
|
|
628
|
+
|
|
629
|
+
## Notes and recommendations
|
|
630
|
+
|
|
631
|
+
- Render `DynamicModal` only once per screen or page branch when possible.
|
|
632
|
+
- Prefer stable `name` values because they are used to manage form state.
|
|
633
|
+
- Use `renderIf` for hidden sections and `enableIf` for visible-but-locked
|
|
634
|
+
sections.
|
|
635
|
+
- Keep `liveData` actions fast and deterministic when possible.
|
|
636
|
+
- If your custom UI components use different event contracts, adapt them inside
|
|
637
|
+
`ComponentState` rather than changing modal configs.
|
|
638
|
+
|
|
639
|
+
## Repository examples
|
|
640
|
+
|
|
641
|
+
This repository includes working examples in:
|
|
642
|
+
|
|
643
|
+
- `examples/simple.ts`
|
|
644
|
+
- `examples/render-if.ts`
|
|
645
|
+
- `examples/enable-if.ts`
|
|
646
|
+
- `examples/live-data.ts`
|
|
647
|
+
|
|
648
|
+
These are useful starting points for building your own modal catalog.
|