@react-typed-forms/schemas 14.2.0 → 14.3.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/lib/controlDefinition.d.ts +11 -0
- package/lib/index.cjs +3392 -1
- package/lib/index.cjs.map +1 -1
- package/lib/index.js +2869 -1
- package/lib/index.js.map +1 -1
- package/lib/renderers.d.ts +2 -2
- package/lib/util.d.ts +7 -2
- package/package.json +3 -2
- package/src/controlBuilder.ts +268 -0
- package/src/controlDefinition.ts +792 -0
- package/src/controlRender.tsx +1252 -0
- package/src/createFormRenderer.tsx +218 -0
- package/src/defaultSchemaInterface.ts +191 -0
- package/src/dynamicHooks.ts +98 -0
- package/src/entityExpression.ts +38 -0
- package/src/hooks.tsx +459 -0
- package/src/index.ts +14 -0
- package/src/renderers.tsx +205 -0
- package/src/schemaBuilder.ts +318 -0
- package/src/schemaField.ts +552 -0
- package/src/schemaValidator.ts +32 -0
- package/src/util.ts +1039 -0
- package/src/validators.ts +217 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
import {
|
|
3
|
+
ActionRendererProps,
|
|
4
|
+
AdornmentProps,
|
|
5
|
+
AdornmentRenderer,
|
|
6
|
+
ArrayRendererProps,
|
|
7
|
+
ControlLayoutProps,
|
|
8
|
+
DataRendererProps,
|
|
9
|
+
DisplayRendererProps,
|
|
10
|
+
FormRenderer,
|
|
11
|
+
GroupRendererProps,
|
|
12
|
+
LabelRendererProps,
|
|
13
|
+
LabelType,
|
|
14
|
+
} from "./controlRender";
|
|
15
|
+
import { hasOptions } from "./util";
|
|
16
|
+
import {
|
|
17
|
+
ActionRendererRegistration,
|
|
18
|
+
AdornmentRendererRegistration,
|
|
19
|
+
ArrayRendererRegistration,
|
|
20
|
+
DataRendererRegistration,
|
|
21
|
+
DefaultRenderers,
|
|
22
|
+
DisplayRendererRegistration,
|
|
23
|
+
GroupRendererRegistration,
|
|
24
|
+
LabelRendererRegistration,
|
|
25
|
+
LayoutRendererRegistration,
|
|
26
|
+
RendererRegistration,
|
|
27
|
+
VisibilityRendererRegistration,
|
|
28
|
+
} from "./renderers";
|
|
29
|
+
import { DataRenderType } from "./controlDefinition";
|
|
30
|
+
|
|
31
|
+
export function createFormRenderer(
|
|
32
|
+
customRenderers: RendererRegistration[] = [],
|
|
33
|
+
defaultRenderers: DefaultRenderers,
|
|
34
|
+
): FormRenderer {
|
|
35
|
+
const dataRegistrations = customRenderers.filter(isDataRegistration);
|
|
36
|
+
const groupRegistrations = customRenderers.filter(isGroupRegistration);
|
|
37
|
+
const adornmentRegistrations = customRenderers.filter(
|
|
38
|
+
isAdornmentRegistration,
|
|
39
|
+
);
|
|
40
|
+
const displayRegistrations = customRenderers.filter(isDisplayRegistration);
|
|
41
|
+
const labelRenderers = customRenderers.filter(isLabelRegistration);
|
|
42
|
+
const arrayRenderers = customRenderers.filter(isArrayRegistration);
|
|
43
|
+
const actionRenderers = customRenderers.filter(isActionRegistration);
|
|
44
|
+
const layoutRenderers = customRenderers.filter(isLayoutRegistration);
|
|
45
|
+
const visibilityRenderer =
|
|
46
|
+
customRenderers.find(isVisibilityRegistration) ??
|
|
47
|
+
defaultRenderers.visibility;
|
|
48
|
+
|
|
49
|
+
const formRenderers: FormRenderer = {
|
|
50
|
+
renderAction,
|
|
51
|
+
renderData,
|
|
52
|
+
renderGroup,
|
|
53
|
+
renderDisplay,
|
|
54
|
+
renderLabel,
|
|
55
|
+
renderArray,
|
|
56
|
+
renderAdornment,
|
|
57
|
+
renderLayout,
|
|
58
|
+
renderVisibility: visibilityRenderer.render,
|
|
59
|
+
renderLabelText,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
function renderLabelText(label: ReactNode) {
|
|
63
|
+
return renderLabel({ label, type: LabelType.Text }, undefined, undefined);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function renderLayout(props: ControlLayoutProps) {
|
|
67
|
+
const renderer =
|
|
68
|
+
layoutRenderers.find((x) => !x.match || x.match(props)) ??
|
|
69
|
+
defaultRenderers.renderLayout;
|
|
70
|
+
return renderer.render(props, formRenderers);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function renderAdornment(props: AdornmentProps): AdornmentRenderer {
|
|
74
|
+
const renderer =
|
|
75
|
+
adornmentRegistrations.find((x) =>
|
|
76
|
+
isOneOf(x.adornmentType, props.adornment.type),
|
|
77
|
+
) ?? defaultRenderers.adornment;
|
|
78
|
+
return renderer.render(props, formRenderers);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function renderArray(props: ArrayRendererProps) {
|
|
82
|
+
return (arrayRenderers[0] ?? defaultRenderers.array).render(
|
|
83
|
+
props,
|
|
84
|
+
formRenderers,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function renderLabel(
|
|
89
|
+
props: LabelRendererProps,
|
|
90
|
+
labelStart: ReactNode,
|
|
91
|
+
labelEnd: ReactNode,
|
|
92
|
+
) {
|
|
93
|
+
const renderer =
|
|
94
|
+
labelRenderers.find((x) => isOneOf(x.labelType, props.type)) ??
|
|
95
|
+
defaultRenderers.label;
|
|
96
|
+
return renderer.render(props, labelStart, labelEnd, formRenderers);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function renderData(
|
|
100
|
+
props: DataRendererProps,
|
|
101
|
+
): (layout: ControlLayoutProps) => ControlLayoutProps {
|
|
102
|
+
const { renderOptions, field } = props;
|
|
103
|
+
|
|
104
|
+
const options = hasOptions(props);
|
|
105
|
+
const renderType = renderOptions.type;
|
|
106
|
+
const renderer =
|
|
107
|
+
dataRegistrations.find(matchesRenderer) ?? defaultRenderers.data;
|
|
108
|
+
|
|
109
|
+
const result = renderer.render(props, formRenderers);
|
|
110
|
+
if (typeof result === "function") return result;
|
|
111
|
+
return (l) => ({ ...l, children: result });
|
|
112
|
+
|
|
113
|
+
function matchesRenderer(x: DataRendererRegistration) {
|
|
114
|
+
const matchCollection =
|
|
115
|
+
(x.collection ?? false) ===
|
|
116
|
+
(props.elementIndex == null && (field.collection ?? false));
|
|
117
|
+
const matchSchemaType =
|
|
118
|
+
x.schemaType &&
|
|
119
|
+
renderType == DataRenderType.Standard &&
|
|
120
|
+
isOneOf(x.schemaType, field.type);
|
|
121
|
+
const matchRenderType =
|
|
122
|
+
!x.renderType || isOneOf(x.renderType, renderType);
|
|
123
|
+
return (
|
|
124
|
+
matchCollection &&
|
|
125
|
+
(x.options ?? false) === options &&
|
|
126
|
+
(matchSchemaType || matchRenderType) &&
|
|
127
|
+
(!x.match || x.match(props, renderOptions))
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function renderGroup(
|
|
133
|
+
props: GroupRendererProps,
|
|
134
|
+
): (layout: ControlLayoutProps) => ControlLayoutProps {
|
|
135
|
+
const renderType = props.renderOptions.type;
|
|
136
|
+
const renderer =
|
|
137
|
+
groupRegistrations.find((x) => isOneOf(x.renderType, renderType)) ??
|
|
138
|
+
defaultRenderers.group;
|
|
139
|
+
const result = renderer.render(props, formRenderers);
|
|
140
|
+
if (typeof result === "function") return result;
|
|
141
|
+
return (l) => ({ ...l, children: result });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function renderAction(props: ActionRendererProps) {
|
|
145
|
+
const renderer =
|
|
146
|
+
actionRenderers.find((x) => isOneOf(x.actionType, props.actionId)) ??
|
|
147
|
+
defaultRenderers.action;
|
|
148
|
+
return renderer.render(props, formRenderers);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function renderDisplay(props: DisplayRendererProps) {
|
|
152
|
+
const renderType = props.data.type;
|
|
153
|
+
const renderer =
|
|
154
|
+
displayRegistrations.find((x) => isOneOf(x.renderType, renderType)) ??
|
|
155
|
+
defaultRenderers.display;
|
|
156
|
+
return renderer.render(props, formRenderers);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return formRenderers;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function isOneOf<A>(x: A | A[] | undefined, v: A) {
|
|
163
|
+
return x == null ? true : Array.isArray(x) ? x.includes(v) : v === x;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function isAdornmentRegistration(
|
|
167
|
+
x: RendererRegistration,
|
|
168
|
+
): x is AdornmentRendererRegistration {
|
|
169
|
+
return x.type === "adornment";
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function isDataRegistration(
|
|
173
|
+
x: RendererRegistration,
|
|
174
|
+
): x is DataRendererRegistration {
|
|
175
|
+
return x.type === "data";
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function isGroupRegistration(
|
|
179
|
+
x: RendererRegistration,
|
|
180
|
+
): x is GroupRendererRegistration {
|
|
181
|
+
return x.type === "group";
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function isLabelRegistration(
|
|
185
|
+
x: RendererRegistration,
|
|
186
|
+
): x is LabelRendererRegistration {
|
|
187
|
+
return x.type === "label";
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function isLayoutRegistration(
|
|
191
|
+
x: RendererRegistration,
|
|
192
|
+
): x is LayoutRendererRegistration {
|
|
193
|
+
return x.type === "layout";
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function isVisibilityRegistration(
|
|
197
|
+
x: RendererRegistration,
|
|
198
|
+
): x is VisibilityRendererRegistration {
|
|
199
|
+
return x.type === "visibility";
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function isActionRegistration(
|
|
203
|
+
x: RendererRegistration,
|
|
204
|
+
): x is ActionRendererRegistration {
|
|
205
|
+
return x.type === "action";
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function isDisplayRegistration(
|
|
209
|
+
x: RendererRegistration,
|
|
210
|
+
): x is DisplayRendererRegistration {
|
|
211
|
+
return x.type === "display";
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function isArrayRegistration(
|
|
215
|
+
x: RendererRegistration,
|
|
216
|
+
): x is ArrayRendererRegistration {
|
|
217
|
+
return x.type === "array";
|
|
218
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { Control, ControlSetup } from "@react-typed-forms/core";
|
|
2
|
+
import {
|
|
3
|
+
EqualityFunc,
|
|
4
|
+
FieldOption,
|
|
5
|
+
FieldType,
|
|
6
|
+
SchemaDataNode,
|
|
7
|
+
SchemaField,
|
|
8
|
+
SchemaInterface,
|
|
9
|
+
SchemaNode,
|
|
10
|
+
ValidationMessageType,
|
|
11
|
+
} from "./schemaField";
|
|
12
|
+
|
|
13
|
+
export class DefaultSchemaInterface implements SchemaInterface {
|
|
14
|
+
constructor(
|
|
15
|
+
protected boolStrings: [string, string] = ["No", "Yes"],
|
|
16
|
+
protected parseDateTime: (s: string) => number = (s) => Date.parse(s),
|
|
17
|
+
) {}
|
|
18
|
+
|
|
19
|
+
parseToMillis(field: SchemaField, v: string): number {
|
|
20
|
+
return this.parseDateTime(v);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
validationMessageText(
|
|
24
|
+
field: SchemaField,
|
|
25
|
+
messageType: ValidationMessageType,
|
|
26
|
+
actual: any,
|
|
27
|
+
expected: any,
|
|
28
|
+
): string {
|
|
29
|
+
switch (messageType) {
|
|
30
|
+
case ValidationMessageType.NotEmpty:
|
|
31
|
+
return "Please enter a value";
|
|
32
|
+
case ValidationMessageType.MinLength:
|
|
33
|
+
return "Length must be at least " + expected;
|
|
34
|
+
case ValidationMessageType.MaxLength:
|
|
35
|
+
return "Length must be less than " + expected;
|
|
36
|
+
case ValidationMessageType.NotBeforeDate:
|
|
37
|
+
return `Date must not be before ${new Date(expected).toDateString()}`;
|
|
38
|
+
case ValidationMessageType.NotAfterDate:
|
|
39
|
+
return `Date must not be after ${new Date(expected).toDateString()}`;
|
|
40
|
+
default:
|
|
41
|
+
return "Unknown error";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
getDataOptions(node: SchemaDataNode): FieldOption[] | null | undefined {
|
|
46
|
+
return this.getNodeOptions(node.schema);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
getNodeOptions(node: SchemaNode): FieldOption[] | null | undefined {
|
|
50
|
+
return this.getOptions(node.field);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
getOptions({ options }: SchemaField): FieldOption[] | null | undefined {
|
|
54
|
+
return options && options.length > 0 ? options : null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
getFilterOptions(
|
|
58
|
+
array: SchemaDataNode,
|
|
59
|
+
field: SchemaNode,
|
|
60
|
+
): FieldOption[] | undefined | null {
|
|
61
|
+
return this.getNodeOptions(field);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
isEmptyValue(f: SchemaField, value: any): boolean {
|
|
65
|
+
if (f.collection)
|
|
66
|
+
return Array.isArray(value) ? value.length === 0 : value == null;
|
|
67
|
+
switch (f.type) {
|
|
68
|
+
case FieldType.String:
|
|
69
|
+
case FieldType.DateTime:
|
|
70
|
+
case FieldType.Date:
|
|
71
|
+
case FieldType.Time:
|
|
72
|
+
return !value;
|
|
73
|
+
default:
|
|
74
|
+
return value == null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
searchText(field: SchemaField, value: any): string {
|
|
79
|
+
return this.textValue(field, value)?.toLowerCase() ?? "";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
textValue(
|
|
83
|
+
field: SchemaField,
|
|
84
|
+
value: any,
|
|
85
|
+
element?: boolean | undefined,
|
|
86
|
+
): string | undefined {
|
|
87
|
+
const options = this.getOptions(field);
|
|
88
|
+
const option = options?.find((x) => x.value === value);
|
|
89
|
+
if (option) return option.name;
|
|
90
|
+
switch (field.type) {
|
|
91
|
+
case FieldType.Date:
|
|
92
|
+
return value ? new Date(value).toLocaleDateString() : undefined;
|
|
93
|
+
case FieldType.DateTime:
|
|
94
|
+
return value
|
|
95
|
+
? new Date(this.parseToMillis(field, value)).toLocaleString()
|
|
96
|
+
: undefined;
|
|
97
|
+
case FieldType.Time:
|
|
98
|
+
return value
|
|
99
|
+
? new Date("1970-01-01T" + value).toLocaleTimeString()
|
|
100
|
+
: undefined;
|
|
101
|
+
case FieldType.Bool:
|
|
102
|
+
return this.boolStrings[value ? 1 : 0];
|
|
103
|
+
default:
|
|
104
|
+
return value != null ? value.toString() : undefined;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
controlLength(f: SchemaField, control: Control<any>): number {
|
|
109
|
+
return f.collection
|
|
110
|
+
? (control.elements?.length ?? 0)
|
|
111
|
+
: this.valueLength(f, control.value);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
valueLength(field: SchemaField, value: any): number {
|
|
115
|
+
return (value && value?.length) ?? 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
compareValue(field: SchemaField, v1: unknown, v2: unknown): number {
|
|
119
|
+
if (v1 == null) return v2 == null ? 0 : 1;
|
|
120
|
+
if (v2 == null) return -1;
|
|
121
|
+
switch (field.type) {
|
|
122
|
+
case FieldType.Date:
|
|
123
|
+
case FieldType.DateTime:
|
|
124
|
+
case FieldType.Time:
|
|
125
|
+
case FieldType.String:
|
|
126
|
+
return (v1 as string).localeCompare(v2 as string);
|
|
127
|
+
case FieldType.Bool:
|
|
128
|
+
return (v1 as boolean) ? ((v2 as boolean) ? 0 : 1) : -1;
|
|
129
|
+
case FieldType.Int:
|
|
130
|
+
case FieldType.Double:
|
|
131
|
+
return (v1 as number) - (v2 as number);
|
|
132
|
+
default:
|
|
133
|
+
return 0;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
compoundFieldSetup(f: SchemaNode): [string, ControlSetup<any>][] {
|
|
138
|
+
return f.getChildNodes().map((x) => {
|
|
139
|
+
const { field } = x.field;
|
|
140
|
+
return [field, this.makeControlSetup(x)];
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
compoundFieldEquality(f: SchemaNode): [string, EqualityFunc][] {
|
|
145
|
+
return f.getChildNodes().map((x) => {
|
|
146
|
+
const { field } = x.field;
|
|
147
|
+
return [field, (a, b) => this.makeEqualityFunc(x)(a[field], b[field])];
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
makeEqualityFunc(field: SchemaNode, element?: boolean): EqualityFunc {
|
|
152
|
+
if (field.field.collection && !element) {
|
|
153
|
+
const elemEqual = this.makeEqualityFunc(field, true);
|
|
154
|
+
return (a, b) => {
|
|
155
|
+
if (a === b) return true;
|
|
156
|
+
if (a == null || b == null) return false;
|
|
157
|
+
if (a.length !== b.length) return false;
|
|
158
|
+
for (let i = 0; i < a.length; i++) {
|
|
159
|
+
if (!elemEqual(a[i], b[i])) return false;
|
|
160
|
+
}
|
|
161
|
+
return true;
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
switch (field.field.type) {
|
|
165
|
+
case FieldType.Compound:
|
|
166
|
+
const allChecks = this.compoundFieldEquality(field);
|
|
167
|
+
return (a, b) =>
|
|
168
|
+
a === b ||
|
|
169
|
+
(a != null && b != null && allChecks.every((x) => x[1](a, b)));
|
|
170
|
+
default:
|
|
171
|
+
return (a, b) => a === b;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
makeControlSetup(field: SchemaNode, element?: boolean): ControlSetup<any> {
|
|
175
|
+
let setup: ControlSetup<any> = {
|
|
176
|
+
equals: this.makeEqualityFunc(field, element),
|
|
177
|
+
};
|
|
178
|
+
if (field.field.collection && !element) {
|
|
179
|
+
setup.elems = this.makeControlSetup(field, true);
|
|
180
|
+
return setup;
|
|
181
|
+
}
|
|
182
|
+
switch (field.field.type) {
|
|
183
|
+
case FieldType.Compound:
|
|
184
|
+
setup.fields = Object.fromEntries(this.compoundFieldSetup(field));
|
|
185
|
+
}
|
|
186
|
+
return setup;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export const defaultSchemaInterface: SchemaInterface =
|
|
191
|
+
new DefaultSchemaInterface();
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { useCallback, useRef } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Type representing a hook dependency, which can be a string, number, undefined, or null.
|
|
5
|
+
*/
|
|
6
|
+
export type HookDep = string | number | undefined | null;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Interface representing a dynamic hook generator.
|
|
10
|
+
* @template A - The type of the hook result.
|
|
11
|
+
* @template P - The type of the hook context.
|
|
12
|
+
*/
|
|
13
|
+
export interface DynamicHookGenerator<A, P> {
|
|
14
|
+
deps: HookDep;
|
|
15
|
+
state: any;
|
|
16
|
+
|
|
17
|
+
runHook(ctx: P, state: any): A;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Creates a dynamic hook generator.
|
|
22
|
+
* @template A - The type of the hook result.
|
|
23
|
+
* @template P - The type of the hook context.
|
|
24
|
+
* @template S - The type of the hook state.
|
|
25
|
+
* @param runHook - The function to run the hook.
|
|
26
|
+
* @param state - The initial state of the hook.
|
|
27
|
+
* @param deps - The dependencies of the hook.
|
|
28
|
+
* @returns The dynamic hook generator.
|
|
29
|
+
*/
|
|
30
|
+
export function makeHook<A, P, S = undefined>(
|
|
31
|
+
runHook: (ctx: P, state: S) => A,
|
|
32
|
+
state: S,
|
|
33
|
+
deps?: HookDep,
|
|
34
|
+
): DynamicHookGenerator<A, P> {
|
|
35
|
+
return { deps, state, runHook };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Type representing the value of a dynamic hook.
|
|
40
|
+
* @template A - The type of the dynamic hook generator.
|
|
41
|
+
*/
|
|
42
|
+
export type DynamicHookValue<A> =
|
|
43
|
+
A extends DynamicHookGenerator<infer V, any> ? V : never;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Converts an array of dependencies to a dependency string.
|
|
47
|
+
* @template A - The type of the dependencies.
|
|
48
|
+
* @param deps - The array of dependencies.
|
|
49
|
+
* @param asHookDep - The function to convert a dependency to a hook dependency.
|
|
50
|
+
* @returns The dependency string.
|
|
51
|
+
*/
|
|
52
|
+
export function makeHookDepString<A>(
|
|
53
|
+
deps: A[],
|
|
54
|
+
asHookDep: (a: A) => HookDep,
|
|
55
|
+
): string {
|
|
56
|
+
return deps.map((x) => toDepString(asHookDep(x))).join(",");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Custom hook to use dynamic hooks.
|
|
61
|
+
* @template P - The type of the hook context.
|
|
62
|
+
* @template Hooks - The type of the hooks.
|
|
63
|
+
* @param hooks - The hooks to use.
|
|
64
|
+
* @returns A function that takes the hook context and returns the hook values.
|
|
65
|
+
*/
|
|
66
|
+
export function useDynamicHooks<
|
|
67
|
+
P,
|
|
68
|
+
Hooks extends Record<string, DynamicHookGenerator<any, P>>,
|
|
69
|
+
>(
|
|
70
|
+
hooks: Hooks,
|
|
71
|
+
): (p: P) => {
|
|
72
|
+
[K in keyof Hooks]: DynamicHookValue<Hooks[K]>;
|
|
73
|
+
} {
|
|
74
|
+
const hookEntries = Object.entries(hooks);
|
|
75
|
+
const deps = makeHookDepString(hookEntries, (x) => x[1].deps);
|
|
76
|
+
const ref = useRef<Record<string, any>>({});
|
|
77
|
+
const s = ref.current;
|
|
78
|
+
hookEntries.forEach((x) => (s[x[0]] = x[1].state));
|
|
79
|
+
return useCallback(
|
|
80
|
+
(p: P) => {
|
|
81
|
+
return Object.fromEntries(
|
|
82
|
+
hookEntries.map(([f, hg]) => [f, hg.runHook(p, ref.current[f])]),
|
|
83
|
+
) as any;
|
|
84
|
+
},
|
|
85
|
+
[deps],
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Converts a value to a dependency string.
|
|
91
|
+
* @param x - The value to convert.
|
|
92
|
+
* @returns The dependency string.
|
|
93
|
+
*/
|
|
94
|
+
export function toDepString(x: any): string {
|
|
95
|
+
if (x === undefined) return "_";
|
|
96
|
+
if (x === null) return "~";
|
|
97
|
+
return x.toString();
|
|
98
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export interface EntityExpression {
|
|
2
|
+
type: string;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export enum ExpressionType {
|
|
6
|
+
Jsonata = "Jsonata",
|
|
7
|
+
Data = "Data",
|
|
8
|
+
DataMatch = "FieldValue",
|
|
9
|
+
UserMatch = "UserMatch",
|
|
10
|
+
NotEmpty = "NotEmpty",
|
|
11
|
+
UUID = "UUID",
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface JsonataExpression extends EntityExpression {
|
|
15
|
+
type: ExpressionType.Jsonata;
|
|
16
|
+
expression: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface DataExpression extends EntityExpression {
|
|
20
|
+
type: ExpressionType.Data;
|
|
21
|
+
field: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface DataMatchExpression extends EntityExpression {
|
|
25
|
+
type: ExpressionType.DataMatch;
|
|
26
|
+
field: string;
|
|
27
|
+
value: any;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface NotEmptyExpression extends EntityExpression {
|
|
31
|
+
type: ExpressionType.DataMatch;
|
|
32
|
+
field: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface UserMatchExpression extends EntityExpression {
|
|
36
|
+
type: ExpressionType.UserMatch;
|
|
37
|
+
userMatch: string;
|
|
38
|
+
}
|