@react-typed-forms/schemas 1.0.0-dev.17 → 1.0.0-dev.18
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/.rush/temp/operation/build/state.json +1 -1
- package/.rush/temp/shrinkwrap-deps.json +5 -1
- package/lib/controlRender.d.ts +126 -100
- package/lib/hooks.d.ts +10 -9
- package/lib/index.d.ts +7 -5
- package/lib/index.js +1003 -89
- package/lib/index.js.map +1 -1
- package/lib/renderers.d.ts +153 -0
- package/lib/schemaBuilder.d.ts +87 -87
- package/lib/tailwind.d.ts +2 -0
- package/lib/types.d.ts +272 -234
- package/lib/util.d.ts +6 -3
- package/package.json +9 -5
- package/src/controlRender.tsx +183 -176
- package/src/hooks.tsx +425 -0
- package/src/index.ts +2 -0
- package/src/renderers.tsx +855 -0
- package/src/tailwind.tsx +21 -0
- package/src/types.ts +56 -9
- package/src/util.ts +5 -1
- package/src/hooks.ts +0 -173
|
@@ -0,0 +1,855 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
CSSProperties,
|
|
3
|
+
Fragment,
|
|
4
|
+
ReactElement,
|
|
5
|
+
ReactNode,
|
|
6
|
+
useMemo,
|
|
7
|
+
useState,
|
|
8
|
+
} from "react";
|
|
9
|
+
import {
|
|
10
|
+
ActionRendererProps,
|
|
11
|
+
AdornmentProps,
|
|
12
|
+
AdornmentRenderer,
|
|
13
|
+
ArrayRendererProps,
|
|
14
|
+
controlTitle,
|
|
15
|
+
DataRendererProps,
|
|
16
|
+
DisplayRendererProps,
|
|
17
|
+
FormRenderer,
|
|
18
|
+
GroupRendererProps,
|
|
19
|
+
LabelRendererProps,
|
|
20
|
+
Visibility,
|
|
21
|
+
} from "./controlRender";
|
|
22
|
+
import clsx from "clsx";
|
|
23
|
+
import {
|
|
24
|
+
AdornmentPlacement,
|
|
25
|
+
ControlDefinition,
|
|
26
|
+
DataRenderType,
|
|
27
|
+
DisplayDataType,
|
|
28
|
+
FieldOption,
|
|
29
|
+
FieldType,
|
|
30
|
+
GridRenderer,
|
|
31
|
+
HtmlDisplay,
|
|
32
|
+
isGridRenderer,
|
|
33
|
+
TextDisplay,
|
|
34
|
+
} from "./types";
|
|
35
|
+
import { Control, Fcheckbox, formControlProps } from "@react-typed-forms/core";
|
|
36
|
+
import { hasOptions } from "./util";
|
|
37
|
+
|
|
38
|
+
export interface DefaultRenderers {
|
|
39
|
+
data: DataRendererRegistration;
|
|
40
|
+
label: LabelRendererRegistration;
|
|
41
|
+
action: ActionRendererRegistration;
|
|
42
|
+
array: ArrayRendererRegistration;
|
|
43
|
+
group: GroupRendererRegistration;
|
|
44
|
+
display: DisplayRendererRegistration;
|
|
45
|
+
visibility: VisibilityRendererRegistration;
|
|
46
|
+
adornment: AdornmentRendererRegistration;
|
|
47
|
+
}
|
|
48
|
+
export interface DataRendererRegistration {
|
|
49
|
+
type: "data";
|
|
50
|
+
schemaType?: string | string[];
|
|
51
|
+
renderType?: string | string[];
|
|
52
|
+
options?: boolean;
|
|
53
|
+
collection?: boolean;
|
|
54
|
+
match?: (props: DataRendererProps) => boolean;
|
|
55
|
+
render: (
|
|
56
|
+
props: DataRendererProps,
|
|
57
|
+
defaultLabel: (label?: Partial<LabelRendererProps>) => LabelRendererProps,
|
|
58
|
+
renderers: FormRenderer,
|
|
59
|
+
) => ReactElement;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface LabelRendererRegistration {
|
|
63
|
+
type: "label";
|
|
64
|
+
render: (
|
|
65
|
+
labelProps: LabelRendererProps,
|
|
66
|
+
elem: ReactElement,
|
|
67
|
+
renderers: FormRenderer,
|
|
68
|
+
) => ReactElement;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface ActionRendererRegistration {
|
|
72
|
+
type: "action";
|
|
73
|
+
render: (props: ActionRendererProps, renderers: FormRenderer) => ReactElement;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface ArrayRendererRegistration {
|
|
77
|
+
type: "array";
|
|
78
|
+
render: (props: ArrayRendererProps, renderers: FormRenderer) => ReactElement;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface GroupRendererRegistration {
|
|
82
|
+
type: "group";
|
|
83
|
+
render: (
|
|
84
|
+
props: GroupRendererProps,
|
|
85
|
+
defaultLabel: (label?: Partial<LabelRendererProps>) => LabelRendererProps,
|
|
86
|
+
renderers: FormRenderer,
|
|
87
|
+
) => ReactElement;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface DisplayRendererRegistration {
|
|
91
|
+
type: "display";
|
|
92
|
+
render: (
|
|
93
|
+
props: DisplayRendererProps,
|
|
94
|
+
renderers: FormRenderer,
|
|
95
|
+
) => ReactElement;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface VisibilityRendererRegistration {
|
|
99
|
+
type: "visibility";
|
|
100
|
+
render: (visible: Visibility, elem: ReactElement) => ReactElement;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface AdornmentRendererRegistration {
|
|
104
|
+
type: "adornment";
|
|
105
|
+
adornmentType?: string | string[];
|
|
106
|
+
render: (props: AdornmentProps) => AdornmentRenderer;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export type AnyRendererRegistration =
|
|
110
|
+
| DataRendererRegistration
|
|
111
|
+
| GroupRendererRegistration
|
|
112
|
+
| DisplayRendererRegistration
|
|
113
|
+
| ActionRendererRegistration
|
|
114
|
+
| LabelRendererRegistration
|
|
115
|
+
| ArrayRendererRegistration
|
|
116
|
+
| AdornmentRendererRegistration
|
|
117
|
+
| VisibilityRendererRegistration;
|
|
118
|
+
|
|
119
|
+
export function createFormRenderer(
|
|
120
|
+
customRenderers: AnyRendererRegistration[] = [],
|
|
121
|
+
defaultRenderers: DefaultRenderers = createClassStyledRenderers(),
|
|
122
|
+
): FormRenderer {
|
|
123
|
+
const dataRegistrations = customRenderers.filter(isDataRegistration);
|
|
124
|
+
const adornmentRegistrations = customRenderers.filter(
|
|
125
|
+
isAdornmentRegistration,
|
|
126
|
+
);
|
|
127
|
+
const labelRenderer =
|
|
128
|
+
customRenderers.find(isLabelRegistration) ?? defaultRenderers.label;
|
|
129
|
+
const renderVisibility = (
|
|
130
|
+
customRenderers.find(isVisibilityRegistration) ??
|
|
131
|
+
defaultRenderers.visibility
|
|
132
|
+
).render;
|
|
133
|
+
|
|
134
|
+
const formRenderers = {
|
|
135
|
+
renderAction,
|
|
136
|
+
renderData,
|
|
137
|
+
renderGroup,
|
|
138
|
+
renderDisplay,
|
|
139
|
+
renderLabel,
|
|
140
|
+
renderArray,
|
|
141
|
+
renderVisibility,
|
|
142
|
+
renderAdornment,
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
function renderAdornment(props: AdornmentProps): AdornmentRenderer {
|
|
146
|
+
const renderer =
|
|
147
|
+
adornmentRegistrations.find((x) =>
|
|
148
|
+
isOneOf(x.adornmentType, props.definition.type),
|
|
149
|
+
) ?? defaultRenderers.adornment;
|
|
150
|
+
return renderer.render(props);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function renderArray(props: ArrayRendererProps) {
|
|
154
|
+
return defaultRenderers.array.render(props, formRenderers);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function renderLabel(props: LabelRendererProps, elem: ReactElement) {
|
|
158
|
+
return labelRenderer.render(props, elem, formRenderers);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function withAdornments(
|
|
162
|
+
definition: ControlDefinition,
|
|
163
|
+
adornments?: AdornmentRenderer[],
|
|
164
|
+
): [
|
|
165
|
+
AdornmentRenderer[],
|
|
166
|
+
(placement: AdornmentPlacement) => ReactElement,
|
|
167
|
+
(elem: ReactElement) => ReactElement,
|
|
168
|
+
] {
|
|
169
|
+
const rAdornments = adornments
|
|
170
|
+
? adornments
|
|
171
|
+
: definition.adornments?.map((x, i) =>
|
|
172
|
+
renderAdornment({ definition: x, key: i }),
|
|
173
|
+
) ?? [];
|
|
174
|
+
function combineAdornments(placement: AdornmentPlacement) {
|
|
175
|
+
return (
|
|
176
|
+
<>
|
|
177
|
+
{rAdornments
|
|
178
|
+
.filter((x) => x.child && x.child[0] === placement)
|
|
179
|
+
.map((x) => x.child![1])}
|
|
180
|
+
</>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
return [
|
|
184
|
+
rAdornments,
|
|
185
|
+
combineAdornments,
|
|
186
|
+
(mainElem) =>
|
|
187
|
+
!adornments
|
|
188
|
+
? mainElem
|
|
189
|
+
: rAdornments.reduce((e, n) => n.wrap?.(e) ?? e, mainElem),
|
|
190
|
+
];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function renderData(
|
|
194
|
+
props: DataRendererProps,
|
|
195
|
+
adornments?: AdornmentRenderer[],
|
|
196
|
+
): ReactElement {
|
|
197
|
+
const {
|
|
198
|
+
definition,
|
|
199
|
+
renderOptions: { type: renderType },
|
|
200
|
+
visible,
|
|
201
|
+
required,
|
|
202
|
+
control,
|
|
203
|
+
field,
|
|
204
|
+
} = props;
|
|
205
|
+
|
|
206
|
+
const options = hasOptions(props);
|
|
207
|
+
const renderer =
|
|
208
|
+
dataRegistrations.find(
|
|
209
|
+
(x) =>
|
|
210
|
+
(x.collection ?? false) === (field.collection ?? false) &&
|
|
211
|
+
(x.options ?? false) === options &&
|
|
212
|
+
isOneOf(x.schemaType, field.type) &&
|
|
213
|
+
isOneOf(x.renderType, renderType) &&
|
|
214
|
+
(!x.match || x.match(props)),
|
|
215
|
+
) ?? defaultRenderers.data;
|
|
216
|
+
|
|
217
|
+
const [rAdornments, renderAdornment, wrapElem] = withAdornments(
|
|
218
|
+
definition,
|
|
219
|
+
adornments,
|
|
220
|
+
);
|
|
221
|
+
return wrapElem(
|
|
222
|
+
renderer.render(props, createLabel, {
|
|
223
|
+
...formRenderers,
|
|
224
|
+
renderData: (p) => renderData(p, rAdornments),
|
|
225
|
+
}),
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
function createLabel(labelProps?: Partial<LabelRendererProps>) {
|
|
229
|
+
return {
|
|
230
|
+
visible,
|
|
231
|
+
required,
|
|
232
|
+
control,
|
|
233
|
+
forId: "c" + control.uniqueId,
|
|
234
|
+
renderAdornment,
|
|
235
|
+
...labelProps,
|
|
236
|
+
title: labelProps?.title ?? controlTitle(definition.title, field),
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function renderGroup(
|
|
242
|
+
props: GroupRendererProps,
|
|
243
|
+
adornments?: AdornmentRenderer[],
|
|
244
|
+
): ReactElement {
|
|
245
|
+
const { definition, visible, field } = props;
|
|
246
|
+
|
|
247
|
+
const [rAdornments, renderAdornment, wrapElem] = withAdornments(
|
|
248
|
+
props.definition,
|
|
249
|
+
adornments,
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
const title = props.hideTitle
|
|
253
|
+
? undefined
|
|
254
|
+
: field
|
|
255
|
+
? controlTitle(definition.title, field)
|
|
256
|
+
: definition.title;
|
|
257
|
+
|
|
258
|
+
return wrapElem(
|
|
259
|
+
defaultRenderers.group.render(props, createLabel, {
|
|
260
|
+
...formRenderers,
|
|
261
|
+
renderGroup: (p) => renderGroup(p, rAdornments),
|
|
262
|
+
}),
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
function createLabel(
|
|
266
|
+
labelProps?: Partial<LabelRendererProps>,
|
|
267
|
+
): LabelRendererProps {
|
|
268
|
+
return {
|
|
269
|
+
required: false,
|
|
270
|
+
visible,
|
|
271
|
+
group: true,
|
|
272
|
+
renderAdornment,
|
|
273
|
+
title,
|
|
274
|
+
...labelProps,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function renderAction(
|
|
280
|
+
props: ActionRendererProps,
|
|
281
|
+
adornments?: AdornmentRenderer[],
|
|
282
|
+
) {
|
|
283
|
+
const renderer =
|
|
284
|
+
customRenderers.find(isActionRegistration) ?? defaultRenderers.action;
|
|
285
|
+
const [rAdornments, renderAdornment, wrapElem] = withAdornments(
|
|
286
|
+
props.definition,
|
|
287
|
+
adornments,
|
|
288
|
+
);
|
|
289
|
+
return wrapElem(renderer.render(props, formRenderers));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function renderDisplay(
|
|
293
|
+
props: DisplayRendererProps,
|
|
294
|
+
adornments?: AdornmentRenderer[],
|
|
295
|
+
) {
|
|
296
|
+
const [rAdornments, renderAdornment, wrapElem] = withAdornments(
|
|
297
|
+
props.definition,
|
|
298
|
+
adornments,
|
|
299
|
+
);
|
|
300
|
+
return wrapElem(defaultRenderers.display.render(props, formRenderers));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return formRenderers;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
interface DefaultLabelRendererOptions {
|
|
307
|
+
className?: string;
|
|
308
|
+
groupLabelClass?: string;
|
|
309
|
+
requiredElement?: ReactNode;
|
|
310
|
+
labelClass?: string;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
interface DefaultActionRendererOptions {
|
|
314
|
+
className?: string;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export function createDefaultActionRenderer(
|
|
318
|
+
options: DefaultActionRendererOptions = {},
|
|
319
|
+
): ActionRendererRegistration {
|
|
320
|
+
function render(
|
|
321
|
+
{ visible, onClick, definition: { title } }: ActionRendererProps,
|
|
322
|
+
{ renderVisibility }: Pick<FormRenderer, "renderVisibility">,
|
|
323
|
+
) {
|
|
324
|
+
return renderVisibility(
|
|
325
|
+
visible,
|
|
326
|
+
<button className={options.className} onClick={onClick}>
|
|
327
|
+
{title}
|
|
328
|
+
</button>,
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
return { render, type: "action" };
|
|
332
|
+
}
|
|
333
|
+
export function createDefaultLabelRenderer(
|
|
334
|
+
options: DefaultLabelRendererOptions = { requiredElement: <span> *</span> },
|
|
335
|
+
): LabelRendererRegistration {
|
|
336
|
+
return {
|
|
337
|
+
render: (p, elem, { renderVisibility }) =>
|
|
338
|
+
renderVisibility(
|
|
339
|
+
p.visible,
|
|
340
|
+
<DefaultLabelRenderer {...p} {...options} children={elem} />,
|
|
341
|
+
),
|
|
342
|
+
type: "label",
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export function DefaultLabelRenderer({
|
|
347
|
+
className,
|
|
348
|
+
labelClass,
|
|
349
|
+
title,
|
|
350
|
+
forId,
|
|
351
|
+
required,
|
|
352
|
+
children,
|
|
353
|
+
group,
|
|
354
|
+
groupLabelClass,
|
|
355
|
+
renderAdornment,
|
|
356
|
+
requiredElement,
|
|
357
|
+
}: LabelRendererProps &
|
|
358
|
+
DefaultLabelRendererOptions & { children: ReactElement }) {
|
|
359
|
+
return title ? (
|
|
360
|
+
<div className={className}>
|
|
361
|
+
{renderAdornment(AdornmentPlacement.LabelStart)}
|
|
362
|
+
<label
|
|
363
|
+
htmlFor={forId}
|
|
364
|
+
className={clsx(labelClass, group && groupLabelClass)}
|
|
365
|
+
>
|
|
366
|
+
{title}
|
|
367
|
+
{required && requiredElement}
|
|
368
|
+
</label>
|
|
369
|
+
{renderAdornment(AdornmentPlacement.LabelEnd)}
|
|
370
|
+
{renderAdornment(AdornmentPlacement.ControlStart)}
|
|
371
|
+
{children}
|
|
372
|
+
{renderAdornment(AdornmentPlacement.ControlEnd)}
|
|
373
|
+
</div>
|
|
374
|
+
) : (
|
|
375
|
+
<>{children}</>
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
interface DefaultArrayRendererOptions {
|
|
380
|
+
className?: string;
|
|
381
|
+
removableClass?: string;
|
|
382
|
+
childClass?: string;
|
|
383
|
+
removableChildClass?: string;
|
|
384
|
+
removeActionClass?: string;
|
|
385
|
+
addActionClass?: string;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export function createDefaultArrayRenderer(
|
|
389
|
+
options?: DefaultArrayRendererOptions,
|
|
390
|
+
): ArrayRendererRegistration {
|
|
391
|
+
const {
|
|
392
|
+
className,
|
|
393
|
+
removableClass,
|
|
394
|
+
childClass,
|
|
395
|
+
removableChildClass,
|
|
396
|
+
removeActionClass,
|
|
397
|
+
addActionClass,
|
|
398
|
+
} = options ?? {};
|
|
399
|
+
function render(
|
|
400
|
+
{
|
|
401
|
+
childCount,
|
|
402
|
+
renderChild,
|
|
403
|
+
addAction,
|
|
404
|
+
removeAction,
|
|
405
|
+
childKey,
|
|
406
|
+
}: ArrayRendererProps,
|
|
407
|
+
{ renderAction }: Pick<FormRenderer, "renderAction">,
|
|
408
|
+
) {
|
|
409
|
+
return (
|
|
410
|
+
<>
|
|
411
|
+
<div className={clsx(className, removeAction && removableClass)}>
|
|
412
|
+
{Array.from({ length: childCount }, (_, x) =>
|
|
413
|
+
removeAction ? (
|
|
414
|
+
<Fragment key={childKey(x)}>
|
|
415
|
+
<div className={clsx(childClass, removableChildClass)}>
|
|
416
|
+
{renderChild(x)}
|
|
417
|
+
</div>
|
|
418
|
+
<div className={removeActionClass}>
|
|
419
|
+
{renderAction(removeAction(x))}
|
|
420
|
+
</div>
|
|
421
|
+
</Fragment>
|
|
422
|
+
) : (
|
|
423
|
+
<div key={childKey(x)} className={childClass}>
|
|
424
|
+
{renderChild(x)}
|
|
425
|
+
</div>
|
|
426
|
+
),
|
|
427
|
+
)}
|
|
428
|
+
</div>
|
|
429
|
+
{addAction && (
|
|
430
|
+
<div className={addActionClass}>{renderAction(addAction)}</div>
|
|
431
|
+
)}
|
|
432
|
+
</>
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
return { render, type: "array" };
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
interface StyleProps {
|
|
439
|
+
className?: string;
|
|
440
|
+
style?: CSSProperties;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
interface DefaultGroupRendererOptions {
|
|
444
|
+
className?: string;
|
|
445
|
+
standardClassName?: string;
|
|
446
|
+
gridStyles?: (columns: GridRenderer) => StyleProps;
|
|
447
|
+
gridClassName?: string;
|
|
448
|
+
defaultGridColumns?: number;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export function createDefaultGroupRenderer(
|
|
452
|
+
options?: DefaultGroupRendererOptions,
|
|
453
|
+
): GroupRendererRegistration {
|
|
454
|
+
const {
|
|
455
|
+
className,
|
|
456
|
+
gridStyles = defaultGridStyles,
|
|
457
|
+
defaultGridColumns = 2,
|
|
458
|
+
gridClassName,
|
|
459
|
+
standardClassName,
|
|
460
|
+
} = options ?? {};
|
|
461
|
+
|
|
462
|
+
function defaultGridStyles({
|
|
463
|
+
columns = defaultGridColumns,
|
|
464
|
+
}: GridRenderer): StyleProps {
|
|
465
|
+
return {
|
|
466
|
+
className: gridClassName,
|
|
467
|
+
style: {
|
|
468
|
+
display: "grid",
|
|
469
|
+
gridTemplateColumns: `repeat(${columns}, 1fr)`,
|
|
470
|
+
},
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function render(
|
|
475
|
+
props: GroupRendererProps,
|
|
476
|
+
defaultLabel: (label?: Partial<LabelRendererProps>) => LabelRendererProps,
|
|
477
|
+
{
|
|
478
|
+
renderLabel,
|
|
479
|
+
renderArray,
|
|
480
|
+
}: Pick<FormRenderer, "renderLabel" | "renderArray" | "renderGroup">,
|
|
481
|
+
) {
|
|
482
|
+
const { childCount, renderChild, definition } = props;
|
|
483
|
+
|
|
484
|
+
return renderLabel(
|
|
485
|
+
defaultLabel(),
|
|
486
|
+
props.array ? renderArray(props.array) : renderChildren(),
|
|
487
|
+
);
|
|
488
|
+
|
|
489
|
+
function renderChildren() {
|
|
490
|
+
const { groupOptions } = definition;
|
|
491
|
+
const { style, className: gcn } = isGridRenderer(groupOptions)
|
|
492
|
+
? gridStyles(groupOptions)
|
|
493
|
+
: ({ className: standardClassName } satisfies StyleProps);
|
|
494
|
+
return (
|
|
495
|
+
<div className={clsx(className, gcn)} style={style}>
|
|
496
|
+
{Array.from({ length: childCount }, (_, x) => renderChild(x))}
|
|
497
|
+
</div>
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return { type: "group", render };
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
export interface DefaultDisplayRendererOptions {
|
|
505
|
+
textClassName?: string;
|
|
506
|
+
htmlClassName?: string;
|
|
507
|
+
}
|
|
508
|
+
export function createDefaultDisplayRenderer(
|
|
509
|
+
options: DefaultDisplayRendererOptions = {},
|
|
510
|
+
): DisplayRendererRegistration {
|
|
511
|
+
function doRender({ definition: { displayData } }: DisplayRendererProps) {
|
|
512
|
+
switch (displayData.type) {
|
|
513
|
+
case DisplayDataType.Text:
|
|
514
|
+
return (
|
|
515
|
+
<div className={options.textClassName}>
|
|
516
|
+
{(displayData as TextDisplay).text}
|
|
517
|
+
</div>
|
|
518
|
+
);
|
|
519
|
+
case DisplayDataType.Html:
|
|
520
|
+
return (
|
|
521
|
+
<div
|
|
522
|
+
className={options.htmlClassName}
|
|
523
|
+
dangerouslySetInnerHTML={{
|
|
524
|
+
__html: (displayData as HtmlDisplay).html,
|
|
525
|
+
}}
|
|
526
|
+
/>
|
|
527
|
+
);
|
|
528
|
+
default:
|
|
529
|
+
return <h1>Unknown display type: {displayData.type}</h1>;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
return {
|
|
533
|
+
render: (p, { renderVisibility }) =>
|
|
534
|
+
renderVisibility(p.visible, doRender(p)),
|
|
535
|
+
type: "display",
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
export const DefaultBoolOptions: FieldOption[] = [
|
|
540
|
+
{ name: "Yes", value: true },
|
|
541
|
+
{ name: "No", value: false },
|
|
542
|
+
];
|
|
543
|
+
interface DefaultDataRendererOptions {
|
|
544
|
+
inputClass?: string;
|
|
545
|
+
selectOptions?: SelectRendererOptions;
|
|
546
|
+
booleanOptions?: FieldOption[];
|
|
547
|
+
optionRenderer?: DataRendererRegistration;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
export function createDefaultDataRenderer(
|
|
551
|
+
options: DefaultDataRendererOptions = {},
|
|
552
|
+
): DataRendererRegistration {
|
|
553
|
+
const selectRenderer = createSelectRenderer(options.selectOptions ?? {});
|
|
554
|
+
const { inputClass, booleanOptions, optionRenderer } = {
|
|
555
|
+
optionRenderer: selectRenderer,
|
|
556
|
+
booleanOptions: DefaultBoolOptions,
|
|
557
|
+
...options,
|
|
558
|
+
};
|
|
559
|
+
return createDataRenderer((props, defaultLabel, renderers) => {
|
|
560
|
+
if (props.array) {
|
|
561
|
+
return renderers.renderArray(props.array);
|
|
562
|
+
}
|
|
563
|
+
let renderType = props.renderOptions.type;
|
|
564
|
+
const fieldType = props.field.type;
|
|
565
|
+
const isBool = fieldType === FieldType.Bool;
|
|
566
|
+
if (booleanOptions != null && isBool && props.options == null) {
|
|
567
|
+
return renderers.renderData({ ...props, options: booleanOptions });
|
|
568
|
+
}
|
|
569
|
+
if (renderType === DataRenderType.Standard && hasOptions(props)) {
|
|
570
|
+
return optionRenderer.render(props, defaultLabel, renderers);
|
|
571
|
+
}
|
|
572
|
+
switch (renderType) {
|
|
573
|
+
case DataRenderType.Dropdown:
|
|
574
|
+
return selectRenderer.render(props, defaultLabel, renderers);
|
|
575
|
+
}
|
|
576
|
+
const l = defaultLabel();
|
|
577
|
+
return renderers.renderLabel(
|
|
578
|
+
l,
|
|
579
|
+
renderType === DataRenderType.Checkbox ? (
|
|
580
|
+
<Fcheckbox control={props.control} />
|
|
581
|
+
) : (
|
|
582
|
+
<ControlInput
|
|
583
|
+
className={inputClass}
|
|
584
|
+
id={l.forId}
|
|
585
|
+
readOnly={props.readonly}
|
|
586
|
+
control={props.control}
|
|
587
|
+
convert={createInputConversion(props.field.type)}
|
|
588
|
+
/>
|
|
589
|
+
),
|
|
590
|
+
);
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
export function ControlInput({
|
|
595
|
+
control,
|
|
596
|
+
convert,
|
|
597
|
+
...props
|
|
598
|
+
}: React.InputHTMLAttributes<HTMLInputElement> & {
|
|
599
|
+
control: Control<any>;
|
|
600
|
+
convert: InputConversion;
|
|
601
|
+
}) {
|
|
602
|
+
const { errorText, value, onChange, ...inputProps } =
|
|
603
|
+
formControlProps(control);
|
|
604
|
+
return (
|
|
605
|
+
<input
|
|
606
|
+
{...inputProps}
|
|
607
|
+
type={convert[0]}
|
|
608
|
+
value={value == null ? "" : convert[2](value)}
|
|
609
|
+
onChange={(e) => {
|
|
610
|
+
control.value = convert[1](e.target.value);
|
|
611
|
+
}}
|
|
612
|
+
{...props}
|
|
613
|
+
/>
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
export interface DefaultVisibilityRendererOptions {}
|
|
618
|
+
|
|
619
|
+
export interface DefaultAdornmentRendererOptions {}
|
|
620
|
+
|
|
621
|
+
export function createDefaultAdornmentRenderer(
|
|
622
|
+
options: DefaultAdornmentRendererOptions = {},
|
|
623
|
+
): AdornmentRendererRegistration {
|
|
624
|
+
return { type: "adornment", render: () => ({}) };
|
|
625
|
+
}
|
|
626
|
+
export function createDefaultVisibilityRenderer(
|
|
627
|
+
options: DefaultVisibilityRendererOptions = {},
|
|
628
|
+
): VisibilityRendererRegistration {
|
|
629
|
+
return {
|
|
630
|
+
type: "visibility",
|
|
631
|
+
render: (visible, children) => (visible.value ? children : <></>),
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
export interface DefaultRendererOptions {
|
|
636
|
+
data?: DefaultDataRendererOptions;
|
|
637
|
+
display?: DefaultDisplayRendererOptions;
|
|
638
|
+
action?: DefaultActionRendererOptions;
|
|
639
|
+
array?: DefaultArrayRendererOptions;
|
|
640
|
+
group?: DefaultGroupRendererOptions;
|
|
641
|
+
label?: DefaultLabelRendererOptions;
|
|
642
|
+
visibility?: DefaultVisibilityRendererOptions;
|
|
643
|
+
adornment?: DefaultAdornmentRendererOptions;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
export function createDefaultRenderers(
|
|
647
|
+
options: DefaultRendererOptions = {},
|
|
648
|
+
): DefaultRenderers {
|
|
649
|
+
return {
|
|
650
|
+
data: createDefaultDataRenderer(options.data),
|
|
651
|
+
display: createDefaultDisplayRenderer(options.display),
|
|
652
|
+
action: createDefaultActionRenderer(options.action),
|
|
653
|
+
array: createDefaultArrayRenderer(options.array),
|
|
654
|
+
group: createDefaultGroupRenderer(options.group),
|
|
655
|
+
label: createDefaultLabelRenderer(options.label),
|
|
656
|
+
visibility: createDefaultVisibilityRenderer(options.visibility),
|
|
657
|
+
adornment: createDefaultAdornmentRenderer(options.adornment),
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function createClassStyledRenderers() {
|
|
662
|
+
return createDefaultRenderers({
|
|
663
|
+
label: { className: "control" },
|
|
664
|
+
group: { className: "group" },
|
|
665
|
+
array: { className: "control-array" },
|
|
666
|
+
action: { className: "action" },
|
|
667
|
+
data: { inputClass: "data" },
|
|
668
|
+
display: { htmlClassName: "html", textClassName: "text" },
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function isAdornmentRegistration(
|
|
673
|
+
x: AnyRendererRegistration,
|
|
674
|
+
): x is AdornmentRendererRegistration {
|
|
675
|
+
return x.type === "adornment";
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function isDataRegistration(
|
|
679
|
+
x: AnyRendererRegistration,
|
|
680
|
+
): x is DataRendererRegistration {
|
|
681
|
+
return x.type === "data";
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function isLabelRegistration(
|
|
685
|
+
x: AnyRendererRegistration,
|
|
686
|
+
): x is LabelRendererRegistration {
|
|
687
|
+
return x.type === "label";
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function isActionRegistration(
|
|
691
|
+
x: AnyRendererRegistration,
|
|
692
|
+
): x is ActionRendererRegistration {
|
|
693
|
+
return x.type === "action";
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function isVisibilityRegistration(
|
|
697
|
+
x: AnyRendererRegistration,
|
|
698
|
+
): x is VisibilityRendererRegistration {
|
|
699
|
+
return x.type === "visibility";
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function isOneOf(x: string | string[] | undefined, v: string) {
|
|
703
|
+
return x == null ? true : Array.isArray(x) ? x.includes(v) : v === x;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
export function createDataRenderer(
|
|
707
|
+
render: DataRendererRegistration["render"],
|
|
708
|
+
options?: Partial<DataRendererRegistration>,
|
|
709
|
+
): DataRendererRegistration {
|
|
710
|
+
return { type: "data", render, ...options };
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
export function createDataRendererLabelled(
|
|
714
|
+
render: (
|
|
715
|
+
props: DataRendererProps,
|
|
716
|
+
id: string,
|
|
717
|
+
renderers: FormRenderer,
|
|
718
|
+
) => ReactElement,
|
|
719
|
+
options?: Partial<DataRendererRegistration>,
|
|
720
|
+
): DataRendererRegistration {
|
|
721
|
+
return {
|
|
722
|
+
type: "data",
|
|
723
|
+
render: (props, defaultLabel, renderers) => {
|
|
724
|
+
const dl = defaultLabel();
|
|
725
|
+
return renderers.renderLabel(dl, render(props, dl.forId!, renderers));
|
|
726
|
+
},
|
|
727
|
+
...options,
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
export function createLabelRenderer(
|
|
732
|
+
options: Omit<LabelRendererRegistration, "type">,
|
|
733
|
+
): LabelRendererRegistration {
|
|
734
|
+
return { type: "label", ...options };
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
export function createAdornmentRenderer(
|
|
738
|
+
render: (props: AdornmentProps) => AdornmentRenderer,
|
|
739
|
+
options?: Omit<AdornmentRendererRegistration, "type">,
|
|
740
|
+
): AdornmentRendererRegistration {
|
|
741
|
+
return { type: "adornment", ...options, render };
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
export interface SelectRendererOptions {
|
|
745
|
+
className?: string;
|
|
746
|
+
emptyText?: string;
|
|
747
|
+
requiredText?: string;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
export function createSelectRenderer(options: SelectRendererOptions = {}) {
|
|
751
|
+
return createDataRendererLabelled(
|
|
752
|
+
(props, id) => (
|
|
753
|
+
<SelectDataRenderer
|
|
754
|
+
className={options.className}
|
|
755
|
+
state={props.control}
|
|
756
|
+
id={id}
|
|
757
|
+
options={props.options!}
|
|
758
|
+
required={props.required}
|
|
759
|
+
emptyText={options.emptyText}
|
|
760
|
+
requiredText={options.requiredText}
|
|
761
|
+
convert={createSelectConversion(props.field.type)}
|
|
762
|
+
/>
|
|
763
|
+
),
|
|
764
|
+
{
|
|
765
|
+
options: true,
|
|
766
|
+
},
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
type SelectConversion = (a: any) => string | number;
|
|
771
|
+
|
|
772
|
+
interface SelectDataRendererProps {
|
|
773
|
+
id?: string;
|
|
774
|
+
className?: string;
|
|
775
|
+
options: {
|
|
776
|
+
name: string;
|
|
777
|
+
value: any;
|
|
778
|
+
disabled?: boolean;
|
|
779
|
+
}[];
|
|
780
|
+
emptyText?: string;
|
|
781
|
+
requiredText?: string;
|
|
782
|
+
required: boolean;
|
|
783
|
+
state: Control<any>;
|
|
784
|
+
convert: SelectConversion;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
export function SelectDataRenderer({
|
|
788
|
+
state,
|
|
789
|
+
options,
|
|
790
|
+
className,
|
|
791
|
+
convert,
|
|
792
|
+
required,
|
|
793
|
+
emptyText = "N/A",
|
|
794
|
+
requiredText = "<please select>",
|
|
795
|
+
...props
|
|
796
|
+
}: SelectDataRendererProps) {
|
|
797
|
+
const { value, disabled } = state;
|
|
798
|
+
const [showEmpty] = useState(!required || value == null);
|
|
799
|
+
const optionStringMap = useMemo(
|
|
800
|
+
() => Object.fromEntries(options.map((x) => [convert(x.value), x.value])),
|
|
801
|
+
[options],
|
|
802
|
+
);
|
|
803
|
+
return (
|
|
804
|
+
<select
|
|
805
|
+
{...props}
|
|
806
|
+
className={className}
|
|
807
|
+
onChange={(v) => (state.value = optionStringMap[v.target.value])}
|
|
808
|
+
value={convert(value)}
|
|
809
|
+
disabled={disabled}
|
|
810
|
+
>
|
|
811
|
+
{showEmpty && (
|
|
812
|
+
<option value="">{required ? requiredText : emptyText}</option>
|
|
813
|
+
)}
|
|
814
|
+
{options.map((x, i) => (
|
|
815
|
+
<option key={i} value={convert(x.value)} disabled={x.disabled}>
|
|
816
|
+
{x.name}
|
|
817
|
+
</option>
|
|
818
|
+
))}
|
|
819
|
+
</select>
|
|
820
|
+
);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
export function createSelectConversion(ft: string): SelectConversion {
|
|
824
|
+
switch (ft) {
|
|
825
|
+
case FieldType.String:
|
|
826
|
+
case FieldType.Int:
|
|
827
|
+
case FieldType.Double:
|
|
828
|
+
return (a) => a;
|
|
829
|
+
default:
|
|
830
|
+
return (a) => a?.toString() ?? "";
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
type InputConversion = [string, (s: any) => any, (a: any) => string | number];
|
|
835
|
+
|
|
836
|
+
export function createInputConversion(ft: string): InputConversion {
|
|
837
|
+
switch (ft) {
|
|
838
|
+
case FieldType.String:
|
|
839
|
+
return ["text", (a) => a, (a) => a];
|
|
840
|
+
case FieldType.Bool:
|
|
841
|
+
return ["text", (a) => a === "true", (a) => a?.toString() ?? ""];
|
|
842
|
+
case FieldType.Int:
|
|
843
|
+
return [
|
|
844
|
+
"number",
|
|
845
|
+
(a) => (a !== "" ? parseInt(a) : null),
|
|
846
|
+
(a) => (a == null ? "" : a),
|
|
847
|
+
];
|
|
848
|
+
case FieldType.Date:
|
|
849
|
+
return ["date", (a) => a, (a) => a];
|
|
850
|
+
case FieldType.Double:
|
|
851
|
+
return ["number", (a) => parseFloat(a), (a) => a];
|
|
852
|
+
default:
|
|
853
|
+
return ["text", (a) => a, (a) => a];
|
|
854
|
+
}
|
|
855
|
+
}
|