@player-tools/dsl 0.4.0-next.2 → 0.4.0-next.4
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/dist/index.cjs.js +176 -113
- package/dist/index.d.ts +64 -11
- package/dist/index.esm.js +177 -114
- package/package.json +1 -3
- package/src/compiler/compiler.ts +69 -32
- package/src/compiler/schema.ts +37 -20
- package/src/compiler/types.ts +49 -2
- package/src/components.tsx +42 -17
- package/src/string-templates/index.ts +5 -1
- package/src/template.tsx +8 -0
- package/src/types.ts +8 -0
- package/src/utils.tsx +31 -14
package/src/compiler/compiler.ts
CHANGED
|
@@ -3,11 +3,24 @@ import type { JsonType } from 'react-json-reconciler';
|
|
|
3
3
|
import { SourceMapGenerator, SourceMapConsumer } from 'source-map-js';
|
|
4
4
|
import { render } from 'react-json-reconciler';
|
|
5
5
|
import type { Flow, View, Navigation as PlayerNav } from '@player-ui/types';
|
|
6
|
-
import {
|
|
7
|
-
|
|
6
|
+
import {
|
|
7
|
+
AsyncSeriesHook,
|
|
8
|
+
AsyncSeriesWaterfallHook,
|
|
9
|
+
SyncHook,
|
|
10
|
+
} from 'tapable-ts';
|
|
11
|
+
import type { LoggingInterface, SerializeType } from './types';
|
|
8
12
|
import type { Navigation } from '../types';
|
|
9
13
|
import { SchemaGenerator } from './schema';
|
|
10
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Argument passed to the DSLCompiler onEnd hook
|
|
17
|
+
* Defined as an object so additional fields can be added later without breaking API
|
|
18
|
+
* */
|
|
19
|
+
export interface OnEndArg {
|
|
20
|
+
/** target output directory **/
|
|
21
|
+
output: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
11
24
|
/** Recursively find BindingTemplateInstance and call toValue on them */
|
|
12
25
|
const parseNavigationExpressions = (nav: Navigation): PlayerNav => {
|
|
13
26
|
/** Same as above but signature changed */
|
|
@@ -93,10 +106,22 @@ const mergeSourceMaps = (
|
|
|
93
106
|
|
|
94
107
|
/** A compiler for transforming DSL content into JSON */
|
|
95
108
|
export class DSLCompiler {
|
|
109
|
+
public readonly logger: LoggingInterface;
|
|
96
110
|
public hooks = {
|
|
111
|
+
// Hook to access the schema generator instance when initialized
|
|
97
112
|
schemaGenerator: new SyncHook<[SchemaGenerator]>(),
|
|
113
|
+
// Hook to access pre-compilation object
|
|
114
|
+
preProcessFlow: new AsyncSeriesWaterfallHook<[object]>(),
|
|
115
|
+
// Hook to access post-compilation Flow before output is written
|
|
116
|
+
postProcessFlow: new AsyncSeriesWaterfallHook<[Flow]>(),
|
|
117
|
+
// Hook called after all files are compiled. Revives the output directory
|
|
118
|
+
onEnd: new AsyncSeriesHook<[OnEndArg]>(),
|
|
98
119
|
};
|
|
99
120
|
|
|
121
|
+
constructor(logger?: LoggingInterface) {
|
|
122
|
+
this.logger = logger ?? console;
|
|
123
|
+
}
|
|
124
|
+
|
|
100
125
|
/** Convert an object (flow, view, schema, etc) into it's JSON representation */
|
|
101
126
|
async serialize(value: unknown): Promise<{
|
|
102
127
|
/** the JSON value of the source */
|
|
@@ -112,6 +137,9 @@ export class DSLCompiler {
|
|
|
112
137
|
throw new Error('Unable to serialize non-object');
|
|
113
138
|
}
|
|
114
139
|
|
|
140
|
+
const schemaGenerator = new SchemaGenerator(this.logger);
|
|
141
|
+
this.hooks.schemaGenerator.call(schemaGenerator);
|
|
142
|
+
|
|
115
143
|
if (React.isValidElement(value)) {
|
|
116
144
|
const { jsonValue, sourceMap } = await render(value, {
|
|
117
145
|
collectSourceMap: true,
|
|
@@ -124,14 +152,16 @@ export class DSLCompiler {
|
|
|
124
152
|
};
|
|
125
153
|
}
|
|
126
154
|
|
|
127
|
-
|
|
155
|
+
const preProcessedValue = await this.hooks.preProcessFlow.call(value);
|
|
156
|
+
|
|
157
|
+
if ('navigation' in preProcessedValue) {
|
|
128
158
|
// Source maps from all the nested views
|
|
129
159
|
// Merge these together before returning
|
|
130
160
|
const allSourceMaps: SourceMapList = [];
|
|
131
161
|
|
|
132
162
|
// Assume this is a flow
|
|
133
163
|
const copiedValue: Flow = {
|
|
134
|
-
...(
|
|
164
|
+
...(preProcessedValue as any),
|
|
135
165
|
};
|
|
136
166
|
|
|
137
167
|
copiedValue.views = (await Promise.all(
|
|
@@ -165,31 +195,37 @@ export class DSLCompiler {
|
|
|
165
195
|
)) as View[];
|
|
166
196
|
|
|
167
197
|
// Go through the flow and sub out any view refs that are react elements w/ the right id
|
|
168
|
-
if ('navigation' in
|
|
169
|
-
Object.entries((
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
198
|
+
if ('navigation' in preProcessedValue) {
|
|
199
|
+
Object.entries((preProcessedValue as Flow).navigation).forEach(
|
|
200
|
+
([navKey, node]) => {
|
|
201
|
+
if (typeof node === 'object') {
|
|
202
|
+
Object.entries(node).forEach(([nodeKey, flowNode]) => {
|
|
203
|
+
if (
|
|
204
|
+
flowNode &&
|
|
205
|
+
typeof flowNode === 'object' &&
|
|
206
|
+
'state_type' in flowNode &&
|
|
207
|
+
flowNode.state_type === 'VIEW' &&
|
|
208
|
+
React.isValidElement(flowNode.ref)
|
|
209
|
+
) {
|
|
210
|
+
const actualViewIndex = (
|
|
211
|
+
preProcessedValue as Flow
|
|
212
|
+
).views?.indexOf?.(flowNode.ref as any);
|
|
213
|
+
|
|
214
|
+
if (actualViewIndex !== undefined && actualViewIndex > -1) {
|
|
215
|
+
const actualId = copiedValue.views?.[actualViewIndex]?.id;
|
|
216
|
+
|
|
217
|
+
(copiedValue as any).navigation[navKey][nodeKey].ref =
|
|
218
|
+
actualId;
|
|
219
|
+
}
|
|
188
220
|
}
|
|
189
|
-
}
|
|
190
|
-
}
|
|
221
|
+
});
|
|
222
|
+
}
|
|
191
223
|
}
|
|
192
|
-
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
if ('schema' in preProcessedValue) {
|
|
227
|
+
copiedValue.schema = schemaGenerator.toSchema(copiedValue.schema);
|
|
228
|
+
}
|
|
193
229
|
|
|
194
230
|
copiedValue.navigation = parseNavigationExpressions(
|
|
195
231
|
copiedValue.navigation
|
|
@@ -197,8 +233,12 @@ export class DSLCompiler {
|
|
|
197
233
|
}
|
|
198
234
|
|
|
199
235
|
if (value) {
|
|
236
|
+
const postProcessFlow = await this.hooks.postProcessFlow.call(
|
|
237
|
+
copiedValue
|
|
238
|
+
);
|
|
239
|
+
|
|
200
240
|
return {
|
|
201
|
-
value:
|
|
241
|
+
value: postProcessFlow as JsonType,
|
|
202
242
|
contentType: 'flow',
|
|
203
243
|
sourceMap: mergeSourceMaps(
|
|
204
244
|
allSourceMaps,
|
|
@@ -208,11 +248,8 @@ export class DSLCompiler {
|
|
|
208
248
|
}
|
|
209
249
|
}
|
|
210
250
|
|
|
211
|
-
const schemaGenerator = new SchemaGenerator();
|
|
212
|
-
this.hooks.schemaGenerator.call(schemaGenerator);
|
|
213
|
-
|
|
214
251
|
return {
|
|
215
|
-
value: schemaGenerator.toSchema(
|
|
252
|
+
value: schemaGenerator.toSchema(preProcessedValue) as JsonType,
|
|
216
253
|
contentType: 'schema',
|
|
217
254
|
};
|
|
218
255
|
}
|
package/src/compiler/schema.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Schema, Language } from '@player-ui/types';
|
|
2
|
-
import signale from 'signale';
|
|
3
2
|
import { dequal } from 'dequal';
|
|
4
3
|
import { SyncWaterfallHook } from 'tapable-ts';
|
|
4
|
+
import type { LoggingInterface } from '..';
|
|
5
5
|
import { binding as b } from '..';
|
|
6
6
|
import type { BindingTemplateInstance } from '../string-templates';
|
|
7
7
|
|
|
@@ -9,6 +9,13 @@ const bindingSymbol = Symbol('binding');
|
|
|
9
9
|
|
|
10
10
|
export const SchemaTypeName = Symbol('Schema Rename');
|
|
11
11
|
|
|
12
|
+
interface GeneratedDataType {
|
|
13
|
+
/** The SchemaNode that was generated */
|
|
14
|
+
node: SchemaNode;
|
|
15
|
+
/** How many times it has been generated */
|
|
16
|
+
count: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
12
19
|
interface SchemaChildren {
|
|
13
20
|
/** Object property that will be used to create the intermediate type */
|
|
14
21
|
name: string;
|
|
@@ -36,7 +43,8 @@ const isTypeDef = (property: SchemaNode): property is Schema.DataType => {
|
|
|
36
43
|
*/
|
|
37
44
|
export class SchemaGenerator {
|
|
38
45
|
private children: Array<SchemaChildren>;
|
|
39
|
-
private
|
|
46
|
+
private generatedDataTypes: Map<string, GeneratedDataType>;
|
|
47
|
+
private logger: LoggingInterface;
|
|
40
48
|
|
|
41
49
|
public hooks = {
|
|
42
50
|
createSchemaNode: new SyncWaterfallHook<
|
|
@@ -47,9 +55,10 @@ export class SchemaGenerator {
|
|
|
47
55
|
>(),
|
|
48
56
|
};
|
|
49
57
|
|
|
50
|
-
constructor() {
|
|
58
|
+
constructor(logger?: LoggingInterface) {
|
|
51
59
|
this.children = [];
|
|
52
|
-
this.
|
|
60
|
+
this.generatedDataTypes = new Map();
|
|
61
|
+
this.logger = logger ?? console;
|
|
53
62
|
}
|
|
54
63
|
|
|
55
64
|
/**
|
|
@@ -62,12 +71,12 @@ export class SchemaGenerator {
|
|
|
62
71
|
};
|
|
63
72
|
|
|
64
73
|
this.children = [];
|
|
65
|
-
this.
|
|
74
|
+
this.generatedDataTypes.clear();
|
|
66
75
|
|
|
67
76
|
Object.keys(schema).forEach((property) => {
|
|
68
77
|
const subType = schema[property] as SchemaNode;
|
|
69
78
|
newSchema.ROOT[property] = this.hooks.createSchemaNode.call(
|
|
70
|
-
this.
|
|
79
|
+
this.processChild(property, subType),
|
|
71
80
|
subType as any
|
|
72
81
|
);
|
|
73
82
|
});
|
|
@@ -84,7 +93,7 @@ export class SchemaGenerator {
|
|
|
84
93
|
Object.keys(child).forEach((property) => {
|
|
85
94
|
const subType = (child as any)[property] as SchemaNode;
|
|
86
95
|
typeDef[property] = this.hooks.createSchemaNode.call(
|
|
87
|
-
this.
|
|
96
|
+
this.processChild(property, subType),
|
|
88
97
|
subType as any
|
|
89
98
|
);
|
|
90
99
|
});
|
|
@@ -98,10 +107,7 @@ export class SchemaGenerator {
|
|
|
98
107
|
* Processes the children of an object Node
|
|
99
108
|
* Newly discovered children get added to the provided array
|
|
100
109
|
*/
|
|
101
|
-
private
|
|
102
|
-
property: string,
|
|
103
|
-
subType: SchemaNode
|
|
104
|
-
): Schema.DataType {
|
|
110
|
+
private processChild(property: string, subType: SchemaNode): Schema.DataType {
|
|
105
111
|
if (isTypeDef(subType)) {
|
|
106
112
|
return subType;
|
|
107
113
|
}
|
|
@@ -110,7 +116,7 @@ export class SchemaGenerator {
|
|
|
110
116
|
|
|
111
117
|
if (Array.isArray(subType)) {
|
|
112
118
|
if (subType.length > 1) {
|
|
113
|
-
|
|
119
|
+
this.logger.warn(
|
|
114
120
|
`Type ${property} has multiple types in array, should only contain one top level object type. Only taking first defined type`
|
|
115
121
|
);
|
|
116
122
|
}
|
|
@@ -124,23 +130,34 @@ export class SchemaGenerator {
|
|
|
124
130
|
this.children.push({ name: intermediateType.type, child: subType });
|
|
125
131
|
}
|
|
126
132
|
|
|
127
|
-
if (this.
|
|
133
|
+
if (this.generatedDataTypes.has(intermediateType.type)) {
|
|
134
|
+
const generatedType = this.generatedDataTypes.get(
|
|
135
|
+
intermediateType.type
|
|
136
|
+
) as GeneratedDataType;
|
|
128
137
|
if (
|
|
129
138
|
!dequal(
|
|
130
139
|
subType,
|
|
131
|
-
this.
|
|
140
|
+
this.generatedDataTypes.get(intermediateType.type)?.node as object
|
|
132
141
|
)
|
|
133
142
|
) {
|
|
134
|
-
|
|
135
|
-
|
|
143
|
+
generatedType.count += 1;
|
|
144
|
+
const newIntermediateType = {
|
|
145
|
+
...intermediateType,
|
|
146
|
+
type: `${intermediateType.type}${generatedType.count}`,
|
|
147
|
+
};
|
|
148
|
+
this.logger.warn(
|
|
149
|
+
`WARNING: Generated two intermediate types with the name: ${intermediateType.type} that are of different shapes, using artificial type ${newIntermediateType.type}`
|
|
136
150
|
);
|
|
151
|
+
intermediateType = newIntermediateType;
|
|
152
|
+
this.children.pop();
|
|
153
|
+
this.children.push({ name: intermediateType.type, child: subType });
|
|
137
154
|
}
|
|
138
|
-
|
|
139
|
-
// remove last added type since we don't need to reprocess it
|
|
140
|
-
this.children.pop();
|
|
141
155
|
}
|
|
142
156
|
|
|
143
|
-
this.
|
|
157
|
+
this.generatedDataTypes.set(intermediateType.type, {
|
|
158
|
+
node: subType,
|
|
159
|
+
count: 1,
|
|
160
|
+
});
|
|
144
161
|
return intermediateType;
|
|
145
162
|
}
|
|
146
163
|
|
package/src/compiler/types.ts
CHANGED
|
@@ -1,14 +1,59 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
Schema,
|
|
3
|
+
Navigation,
|
|
4
|
+
Flow,
|
|
5
|
+
Expression,
|
|
6
|
+
ExpressionObject,
|
|
7
|
+
NavigationFlow,
|
|
8
|
+
NavigationFlowState,
|
|
9
|
+
NavigationFlowViewState,
|
|
10
|
+
} from '@player-ui/types';
|
|
2
11
|
import type { RemoveUnknownIndex, AddUnknownIndex } from '../types';
|
|
3
12
|
|
|
13
|
+
export type NavigationFlowReactViewState = Omit<
|
|
14
|
+
NavigationFlowViewState,
|
|
15
|
+
'ref'
|
|
16
|
+
> & {
|
|
17
|
+
/** The view element */
|
|
18
|
+
ref: React.ReactElement | string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type NavFlowState =
|
|
22
|
+
| Exclude<NavigationFlowState, NavigationFlowViewState>
|
|
23
|
+
| NavigationFlowReactViewState;
|
|
24
|
+
|
|
25
|
+
export type NavigationFlowWithReactView = Pick<
|
|
26
|
+
NavigationFlow,
|
|
27
|
+
'startState' | 'onStart' | 'onEnd'
|
|
28
|
+
> & {
|
|
29
|
+
[key: string]:
|
|
30
|
+
| undefined
|
|
31
|
+
| string
|
|
32
|
+
| Expression
|
|
33
|
+
| ExpressionObject
|
|
34
|
+
| NavFlowState;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type NavigationWithReactViews = Pick<Navigation, 'BEGIN'> &
|
|
38
|
+
Record<string, string | NavigationFlowWithReactView>;
|
|
39
|
+
|
|
4
40
|
export type FlowWithoutUnknown = RemoveUnknownIndex<Flow>;
|
|
5
41
|
export type FlowWithReactViews = AddUnknownIndex<
|
|
6
|
-
Omit<FlowWithoutUnknown, 'views'> & {
|
|
42
|
+
Omit<FlowWithoutUnknown, 'views' | 'navigation'> & {
|
|
7
43
|
/** An array of JSX view elements */
|
|
8
44
|
views?: Array<React.ReactElement>;
|
|
45
|
+
|
|
46
|
+
/** The navigation element */
|
|
47
|
+
navigation?: NavigationWithReactViews;
|
|
9
48
|
}
|
|
10
49
|
>;
|
|
11
50
|
|
|
51
|
+
export type DSLFlow = FlowWithReactViews;
|
|
52
|
+
|
|
53
|
+
export interface DSLSchema {
|
|
54
|
+
[key: string]: Schema.DataType | DSLSchema;
|
|
55
|
+
}
|
|
56
|
+
|
|
12
57
|
export type SerializeType = 'view' | 'flow' | 'schema' | 'navigation';
|
|
13
58
|
|
|
14
59
|
export type SerializablePlayerExportTypes =
|
|
@@ -16,3 +61,5 @@ export type SerializablePlayerExportTypes =
|
|
|
16
61
|
| FlowWithReactViews
|
|
17
62
|
| Schema.Schema
|
|
18
63
|
| Navigation;
|
|
64
|
+
|
|
65
|
+
export type LoggingInterface = Pick<Console, 'warn' | 'error' | 'log'>;
|
package/src/components.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import React from 'react';
|
|
|
2
2
|
import flattenChildren from 'react-flatten-children';
|
|
3
3
|
import type { ObjectNode, PropertyNode } from 'react-json-reconciler';
|
|
4
4
|
import mergeRefs from 'react-merge-refs';
|
|
5
|
+
import type { View as ViewType } from '@player-ui/types';
|
|
5
6
|
import type { PlayerApplicability, WithChildren } from './types';
|
|
6
7
|
import {
|
|
7
8
|
IDProvider,
|
|
@@ -13,9 +14,24 @@ import {
|
|
|
13
14
|
import {
|
|
14
15
|
normalizeText,
|
|
15
16
|
normalizeToCollection,
|
|
17
|
+
toJsonElement,
|
|
16
18
|
toJsonProperties,
|
|
17
19
|
} from './utils';
|
|
18
20
|
|
|
21
|
+
export type AssetProps = PlayerApplicability & {
|
|
22
|
+
/** id of the asset */
|
|
23
|
+
id?: string;
|
|
24
|
+
|
|
25
|
+
/** the asset type */
|
|
26
|
+
type: string;
|
|
27
|
+
|
|
28
|
+
/** Any other properties on the asset */
|
|
29
|
+
children?: React.ReactNode;
|
|
30
|
+
|
|
31
|
+
/** other things that we don't know about */
|
|
32
|
+
[key: string]: unknown;
|
|
33
|
+
};
|
|
34
|
+
|
|
19
35
|
export const SlotContext = React.createContext<
|
|
20
36
|
| {
|
|
21
37
|
/** The property name for the slot */
|
|
@@ -64,22 +80,7 @@ export const GeneratedIDProperty = (props: {
|
|
|
64
80
|
};
|
|
65
81
|
|
|
66
82
|
/** An asset */
|
|
67
|
-
export const Asset = React.forwardRef<
|
|
68
|
-
ObjectNode,
|
|
69
|
-
{
|
|
70
|
-
/** id of the asset */
|
|
71
|
-
id?: string;
|
|
72
|
-
|
|
73
|
-
/** the asset type */
|
|
74
|
-
type: string;
|
|
75
|
-
|
|
76
|
-
/** Any other properties on the asset */
|
|
77
|
-
children?: React.ReactNode;
|
|
78
|
-
|
|
79
|
-
/** other things that we don't know about */
|
|
80
|
-
[key: string]: unknown;
|
|
81
|
-
} & PlayerApplicability
|
|
82
|
-
>((props, ref) => {
|
|
83
|
+
export const Asset = React.forwardRef<ObjectNode, AssetProps>((props, ref) => {
|
|
83
84
|
const { id, type, applicability, children, ...rest } = props;
|
|
84
85
|
const slotContext = React.useContext(SlotContext);
|
|
85
86
|
const localRef = React.useRef<ObjectNode>(null);
|
|
@@ -110,7 +111,7 @@ export const Asset = React.forwardRef<
|
|
|
110
111
|
value={
|
|
111
112
|
typeof applicability === 'boolean'
|
|
112
113
|
? applicability
|
|
113
|
-
: applicability.
|
|
114
|
+
: applicability.toValue()
|
|
114
115
|
}
|
|
115
116
|
/>
|
|
116
117
|
</property>
|
|
@@ -130,6 +131,30 @@ Asset.defaultProps = {
|
|
|
130
131
|
children: undefined,
|
|
131
132
|
};
|
|
132
133
|
|
|
134
|
+
export const View = React.forwardRef<ObjectNode, AssetProps & ViewType>(
|
|
135
|
+
(props, ref) => {
|
|
136
|
+
const { validation, children, ...rest } = props;
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<Asset ref={ref} {...rest}>
|
|
140
|
+
{validation && (
|
|
141
|
+
<property key="validation" name="validation">
|
|
142
|
+
{toJsonElement(validation, 'validation', {
|
|
143
|
+
propertiesToSkip: ['ref'],
|
|
144
|
+
})}
|
|
145
|
+
</property>
|
|
146
|
+
)}
|
|
147
|
+
{children}
|
|
148
|
+
</Asset>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
View.defaultProps = {
|
|
154
|
+
id: undefined,
|
|
155
|
+
children: undefined,
|
|
156
|
+
};
|
|
157
|
+
|
|
133
158
|
/** A component to generate a named property slot */
|
|
134
159
|
export const Slot = (props: {
|
|
135
160
|
/** The name of the slot */
|
|
@@ -177,5 +177,9 @@ export const expression = (
|
|
|
177
177
|
export const isTemplateStringInstance = (
|
|
178
178
|
val: unknown
|
|
179
179
|
): val is ExpressionTemplateInstance | BindingTemplateInstance => {
|
|
180
|
-
return
|
|
180
|
+
return (
|
|
181
|
+
val !== null &&
|
|
182
|
+
typeof val === 'object' &&
|
|
183
|
+
(val as any)[OpaqueIdentifier] === true
|
|
184
|
+
);
|
|
181
185
|
};
|
package/src/template.tsx
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import { OptionalIDSuffixProvider } from './auto-id';
|
|
12
12
|
import type { BindingTemplateInstance } from './string-templates';
|
|
13
13
|
import type { WithChildren } from './types';
|
|
14
|
+
import { toJsonElement } from './utils';
|
|
14
15
|
|
|
15
16
|
export interface TemplateContextType {
|
|
16
17
|
/** The number of nested templates */
|
|
@@ -30,6 +31,9 @@ export interface TemplateProps {
|
|
|
30
31
|
|
|
31
32
|
/** The template value */
|
|
32
33
|
children: React.ReactNode;
|
|
34
|
+
|
|
35
|
+
/** boolean that specifies whether template should recompute when data changes */
|
|
36
|
+
dynamic?: boolean;
|
|
33
37
|
}
|
|
34
38
|
|
|
35
39
|
/** Add a template instance to the object */
|
|
@@ -127,6 +131,7 @@ const getParentProperty = (node: JsonNode): PropertyNode | undefined => {
|
|
|
127
131
|
/** A template allows users to dynamically map over an array of data */
|
|
128
132
|
export const Template = (props: TemplateProps) => {
|
|
129
133
|
const baseContext = React.useContext(TemplateContext);
|
|
134
|
+
const dynamicProp = props.dynamic ?? false;
|
|
130
135
|
const [outputProp, setOutputProp] = React.useState<string | undefined>(
|
|
131
136
|
props.output
|
|
132
137
|
);
|
|
@@ -175,6 +180,9 @@ export const Template = (props: TemplateProps) => {
|
|
|
175
180
|
<property name="data">{props.data.toValue()}</property>
|
|
176
181
|
<property name="output">{outputProp}</property>
|
|
177
182
|
<property name="value">{props.children}</property>
|
|
183
|
+
{dynamicProp && (
|
|
184
|
+
<property name="dynamic">{toJsonElement(dynamicProp)}</property>
|
|
185
|
+
)}
|
|
178
186
|
</object>
|
|
179
187
|
</TemplateProvider>
|
|
180
188
|
</OptionalIDSuffixProvider>,
|
package/src/types.ts
CHANGED
|
@@ -79,3 +79,11 @@ export type Navigation = DeepReplace<
|
|
|
79
79
|
Expression,
|
|
80
80
|
ExpressionTemplateInstance | ExpressionTemplateInstance[] | Expression
|
|
81
81
|
>;
|
|
82
|
+
|
|
83
|
+
export interface toJsonOptions {
|
|
84
|
+
/**
|
|
85
|
+
* List of string keys that should not be parsed in a special way
|
|
86
|
+
* default is 'applicability'
|
|
87
|
+
*/
|
|
88
|
+
propertiesToSkip?: string[];
|
|
89
|
+
}
|
package/src/utils.tsx
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
isTemplateStringInstance,
|
|
4
4
|
TemplateStringComponent,
|
|
5
5
|
} from './string-templates';
|
|
6
|
+
import type { toJsonOptions } from './types';
|
|
6
7
|
|
|
7
8
|
/** Get an array version of the value */
|
|
8
9
|
export function toArray<T>(val: T | Array<T>): Array<T> {
|
|
@@ -10,44 +11,60 @@ export function toArray<T>(val: T | Array<T>): Array<T> {
|
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
/** Create a component version */
|
|
13
|
-
export function toJsonElement(
|
|
14
|
-
|
|
14
|
+
export function toJsonElement(
|
|
15
|
+
value: any,
|
|
16
|
+
indexOrKey?: number | string,
|
|
17
|
+
options?: toJsonOptions
|
|
18
|
+
): React.ReactElement {
|
|
19
|
+
const indexProp = typeof indexOrKey === 'number' ? { key: indexOrKey } : null;
|
|
15
20
|
|
|
16
21
|
if (Array.isArray(value)) {
|
|
17
22
|
return (
|
|
18
|
-
<array {...
|
|
19
|
-
{value.map((item, idx) => toJsonElement(item, idx))}
|
|
23
|
+
<array {...indexProp}>
|
|
24
|
+
{value.map((item, idx) => toJsonElement(item, idx, options))}
|
|
20
25
|
</array>
|
|
21
26
|
);
|
|
22
27
|
}
|
|
23
28
|
|
|
24
29
|
/** Allow users to pass in BindingTemplateInstance and ExpressionTemplateInstance directly without turning them into strings first */
|
|
25
30
|
if (isTemplateStringInstance(value)) {
|
|
26
|
-
|
|
31
|
+
if (
|
|
32
|
+
typeof indexOrKey === 'string' &&
|
|
33
|
+
options?.propertiesToSkip?.includes(indexOrKey)
|
|
34
|
+
) {
|
|
35
|
+
return <value {...indexProp}>{value.toValue()}</value>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return <value {...indexProp}>{value.toRefString()}</value>;
|
|
27
39
|
}
|
|
28
40
|
|
|
29
41
|
if (typeof value === 'object' && value !== null) {
|
|
30
42
|
return (
|
|
31
|
-
<obj {...
|
|
43
|
+
<obj {...indexProp}>
|
|
32
44
|
{Object.keys(value).map((key) => (
|
|
33
45
|
<property key={key} name={key}>
|
|
34
|
-
{toJsonElement(value[key])}
|
|
46
|
+
{toJsonElement(value[key], key, options)}
|
|
35
47
|
</property>
|
|
36
48
|
))}
|
|
37
49
|
</obj>
|
|
38
50
|
);
|
|
39
51
|
}
|
|
40
52
|
|
|
41
|
-
return <value {...
|
|
53
|
+
return <value {...indexProp} value={value} />;
|
|
42
54
|
}
|
|
43
55
|
|
|
44
56
|
/** Create a fragment for the properties */
|
|
45
|
-
export function toJsonProperties(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
57
|
+
export function toJsonProperties(
|
|
58
|
+
value: Record<string, any>,
|
|
59
|
+
options: toJsonOptions = { propertiesToSkip: ['applicability'] }
|
|
60
|
+
) {
|
|
61
|
+
return Object.keys(value).map((key) => {
|
|
62
|
+
return (
|
|
63
|
+
<property key={key} name={key}>
|
|
64
|
+
{toJsonElement(value[key], key, options)}
|
|
65
|
+
</property>
|
|
66
|
+
);
|
|
67
|
+
});
|
|
51
68
|
}
|
|
52
69
|
|
|
53
70
|
/** Create a text asset if needed */
|