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