@player-tools/dsl 0.0.2-next.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.
@@ -0,0 +1,186 @@
1
+ import React from 'react';
2
+ import type { ObjectNode, JsonNode } from 'react-json-reconciler';
3
+ import {
4
+ ArrayNode,
5
+ PropertyNode,
6
+ ValueNode,
7
+ createPortal,
8
+ ProxyNode,
9
+ toJSON,
10
+ } from 'react-json-reconciler';
11
+ import { OptionalIDSuffixProvider } from './auto-id';
12
+ import type { BindingTemplateInstance } from './string-templates';
13
+ import type { WithChildren } from './types';
14
+
15
+ export interface TemplateContextType {
16
+ /** The number of nested templates */
17
+ depth: number;
18
+ }
19
+
20
+ export const TemplateContext = React.createContext<TemplateContextType>({
21
+ depth: 0,
22
+ });
23
+
24
+ export interface TemplateProps {
25
+ /** The source binding */
26
+ data: BindingTemplateInstance;
27
+
28
+ /** The target property */
29
+ output?: string;
30
+
31
+ /** The template value */
32
+ children: React.ReactNode;
33
+ }
34
+
35
+ /** Add a template instance to the object */
36
+ function addTemplateToObject(
37
+ obj: ObjectNode,
38
+ templateObj: ObjectNode,
39
+ templateParentNodeType: string
40
+ ): () => void {
41
+ // find a template property
42
+ // add one if none exists
43
+
44
+ let templateProp = obj.properties.find(
45
+ (p) => p.keyNode.value === 'template' && p.valueNode?.type === 'array'
46
+ );
47
+
48
+ if (!templateProp) {
49
+ templateProp = new PropertyNode(new ValueNode('template'), new ArrayNode());
50
+ templateProp.parent = obj;
51
+ obj.properties.push(templateProp);
52
+ }
53
+
54
+ const templateItems = templateProp.valueNode as ArrayNode;
55
+
56
+ templateItems.items.push(templateObj);
57
+ // eslint-disable-next-line no-param-reassign
58
+ templateObj.parent = templateItems;
59
+
60
+ const templateParentProp = obj.properties.find(
61
+ (p) =>
62
+ p.keyNode.value === templateParentNodeType &&
63
+ p.valueNode?.type === 'array'
64
+ );
65
+
66
+ if (templateParentProp) {
67
+ const indexOfTemplateParent = obj.properties.indexOf(templateParentProp, 1);
68
+ const templateParentValueNode =
69
+ obj.properties[indexOfTemplateParent]?.valueNode;
70
+ if (templateParentValueNode) {
71
+ const templateParentArray = toJSON(templateParentValueNode);
72
+
73
+ // Delete the parent of template if it is an empty array
74
+ if (
75
+ Array.isArray(templateParentArray) &&
76
+ templateParentArray.length === 0
77
+ ) {
78
+ obj.properties.splice(indexOfTemplateParent, 1);
79
+ }
80
+ }
81
+ }
82
+
83
+ return () => {
84
+ // Remove the template item from the list
85
+ templateItems.items = templateItems.items.filter((t) => t !== templateObj);
86
+
87
+ // Clean up the whole template if it's removed
88
+ if (templateItems.children.length === 0 && templateProp) {
89
+ obj.properties.splice(obj.properties.indexOf(templateProp, 1));
90
+ }
91
+ };
92
+ }
93
+
94
+ /** Context provider wrapper to handle nested templates */
95
+ const TemplateProvider = (props: WithChildren) => {
96
+ const baseContext = React.useContext(TemplateContext);
97
+
98
+ return (
99
+ <TemplateContext.Provider value={{ depth: baseContext.depth + 1 }}>
100
+ {props.children}
101
+ </TemplateContext.Provider>
102
+ );
103
+ };
104
+
105
+ /** Find the first object node in the tree */
106
+ const getParentObject = (node: JsonNode): ObjectNode | undefined => {
107
+ if (node.type === 'object') {
108
+ return node;
109
+ }
110
+
111
+ if (node.parent) {
112
+ return getParentObject(node.parent);
113
+ }
114
+ };
115
+
116
+ /** Find the property of the node on the parent */
117
+ const getParentProperty = (node: JsonNode): PropertyNode | undefined => {
118
+ if (node.type === 'property') {
119
+ return node;
120
+ }
121
+
122
+ if (node.parent) {
123
+ return getParentProperty(node.parent);
124
+ }
125
+ };
126
+
127
+ /** A template allows users to dynamically map over an array of data */
128
+ export const Template = (props: TemplateProps) => {
129
+ const baseContext = React.useContext(TemplateContext);
130
+ const [outputProp, setOutputProp] = React.useState<string | undefined>(
131
+ props.output
132
+ );
133
+ const proxyRef = React.useRef<ProxyNode>(null);
134
+ const valueRef = React.useRef<ValueNode>(null);
135
+ const outputElement = React.useMemo(() => new ProxyNode(), []);
136
+
137
+ React.useLayoutEffect(() => {
138
+ // Get the output prop
139
+ const propNode = proxyRef.current && getParentProperty(proxyRef.current);
140
+
141
+ if (outputProp === undefined && propNode) {
142
+ setOutputProp(propNode.keyNode.value);
143
+ }
144
+ }, [proxyRef, outputProp]);
145
+
146
+ React.useEffect(() => {
147
+ const templateObj = outputElement.items[0] as ObjectNode;
148
+ if (proxyRef.current) {
149
+ const parentObject = getParentObject(proxyRef.current);
150
+
151
+ if (!parentObject) {
152
+ throw new Error('Unable to find parent to add template to');
153
+ }
154
+
155
+ if (!outputProp) {
156
+ return;
157
+ }
158
+
159
+ // remove the template when unmounted
160
+ return addTemplateToObject(parentObject, templateObj, outputProp);
161
+ }
162
+ }, [proxyRef, outputProp, outputElement.items]);
163
+
164
+ return (
165
+ <proxy ref={proxyRef}>
166
+ {createPortal(
167
+ <OptionalIDSuffixProvider
168
+ wrapperRef={valueRef}
169
+ templateIndex={`_index${
170
+ baseContext.depth === 0 ? '' : baseContext.depth
171
+ }_`}
172
+ >
173
+ <TemplateProvider>
174
+ <object>
175
+ <property name="data">{props.data.toValue()}</property>
176
+ <property name="output">{outputProp}</property>
177
+ <property name="value">{props.children}</property>
178
+ </object>
179
+ </TemplateProvider>
180
+ </OptionalIDSuffixProvider>,
181
+ outputElement
182
+ )}
183
+ <value ref={valueRef} value={undefined} />
184
+ </proxy>
185
+ );
186
+ };
package/src/types.ts ADDED
@@ -0,0 +1,81 @@
1
+ import type {
2
+ Asset,
3
+ Expression,
4
+ Navigation as PlayerNav,
5
+ } from '@player-ui/types';
6
+ import type {
7
+ BindingTemplateInstance,
8
+ ExpressionTemplateInstance,
9
+ } from './string-templates';
10
+
11
+ export type WithChildren<T = Record<string, unknown>> = T & {
12
+ /** child nodes */
13
+ children?: React.ReactNode;
14
+ };
15
+
16
+ export type RemoveUnknownIndex<T> = {
17
+ [P in keyof T as T[P] extends unknown
18
+ ? unknown extends T[P]
19
+ ? never
20
+ : P
21
+ : P]: T[P];
22
+ };
23
+
24
+ export type AddUnknownIndex<T> = T & {
25
+ [key: string]: unknown;
26
+ };
27
+
28
+ /** Make an ID prop optional an a type */
29
+ export type OmitProp<T, K extends string> = {
30
+ [P in keyof T as P extends K ? never : P]: T[P];
31
+ };
32
+
33
+ export interface PlayerApplicability {
34
+ /** An expression to evaluate to determine if this node should appear in a view or not */
35
+ applicability?:
36
+ | BindingTemplateInstance
37
+ | ExpressionTemplateInstance
38
+ | boolean;
39
+ }
40
+
41
+ export type AssetPropsWithChildren<T extends Asset> = WithChildren<
42
+ WithTemplateTypes<
43
+ OmitProp<RemoveUnknownIndex<T>, 'id' | 'type'> & Partial<Pick<Asset, 'id'>>
44
+ > &
45
+ PlayerApplicability
46
+ >;
47
+
48
+ export type SwapKeysToType<T, K extends keyof T, NewType> = {
49
+ [P in keyof T]: P extends K ? NewType : T[P];
50
+ };
51
+
52
+ export type WithTemplateTypes<T> = T extends Record<any, any>
53
+ ? {
54
+ [P in keyof T]: WithTemplateTypes<T[P]>;
55
+ }
56
+ : T | BindingTemplateInstance | ExpressionTemplateInstance;
57
+
58
+ type ValidKeys = 'exp' | 'onStart' | 'onEnd';
59
+
60
+ type DeepReplace<T, Old, New> = {
61
+ [P in keyof T]: T[P] extends Old
62
+ ? P extends ValidKeys
63
+ ? New
64
+ : DeepReplace<T[P], Old, New> // Set to new if one of the valid keys: replace with `? New` for all keys
65
+ : T[P] extends (infer R)[] // Is this a Tuple or array
66
+ ? DeepReplace<R, Old, New>[] // Replace the type of the tuple/array
67
+ : T[P] extends object
68
+ ? DeepReplace<T[P], Old, New>
69
+ : Extract<T[P], Old> extends Old // Is this a union with the searched for type?
70
+ ?
71
+ | DeepReplace<Extract<T[P], object>, Old, New> // Replace all object types of the union
72
+ | Exclude<T[P], Old | object> // Get all types that are not objects (handled above) or Old (handled below
73
+ | New // Direct Replacement of Old
74
+ : T[P];
75
+ };
76
+
77
+ export type Navigation = DeepReplace<
78
+ PlayerNav,
79
+ Expression,
80
+ ExpressionTemplateInstance | ExpressionTemplateInstance[] | Expression
81
+ >;
package/src/utils.tsx ADDED
@@ -0,0 +1,109 @@
1
+ import React from 'react';
2
+ import {
3
+ isTemplateStringInstance,
4
+ TemplateStringComponent,
5
+ } from './string-templates';
6
+
7
+ /** Get an array version of the value */
8
+ export function toArray<T>(val: T | Array<T>): Array<T> {
9
+ return Array.isArray(val) ? val : [val];
10
+ }
11
+
12
+ /** Create a component version */
13
+ export function toJsonElement(value: any, index?: number): React.ReactElement {
14
+ const keyProp = index === undefined ? null : { key: index };
15
+
16
+ if (Array.isArray(value)) {
17
+ return (
18
+ <array {...keyProp}>
19
+ {value.map((item, idx) => toJsonElement(item, idx))}
20
+ </array>
21
+ );
22
+ }
23
+
24
+ /** Allow users to pass in BindingTemplateInstance and ExpressionTemplateInstance directly without turning them into strings first */
25
+ if (isTemplateStringInstance(value)) {
26
+ return <value {...keyProp}>{value.toRefString()}</value>;
27
+ }
28
+
29
+ if (typeof value === 'object' && value !== null) {
30
+ return (
31
+ <obj {...keyProp}>
32
+ {Object.keys(value).map((key) => (
33
+ <property key={key} name={key}>
34
+ {toJsonElement(value[key])}
35
+ </property>
36
+ ))}
37
+ </obj>
38
+ );
39
+ }
40
+
41
+ return <value {...keyProp} value={value} />;
42
+ }
43
+
44
+ /** Create a fragment for the properties */
45
+ export function toJsonProperties(value: Record<string, any>) {
46
+ return Object.keys(value).map((key) => (
47
+ <property key={key} name={key}>
48
+ {toJsonElement(value[key])}
49
+ </property>
50
+ ));
51
+ }
52
+
53
+ /** Create a text asset if needed */
54
+ export function normalizeText(options: {
55
+ /** The current node */
56
+ node: React.ReactNode;
57
+
58
+ /** A component to render a text asset */
59
+ TextComp?: React.ComponentType;
60
+ }): React.ReactNode {
61
+ const { node, TextComp } = options;
62
+
63
+ const nodeArr = React.Children.toArray(node);
64
+
65
+ if (
66
+ nodeArr.every(
67
+ (n) => React.isValidElement(n) && n.type !== TemplateStringComponent
68
+ )
69
+ ) {
70
+ return node;
71
+ }
72
+
73
+ if (TextComp) {
74
+ return <TextComp>{nodeArr}</TextComp>;
75
+ }
76
+
77
+ throw new Error(
78
+ `Tried to convert node to Text Asset, but no Component was supplied.`
79
+ );
80
+ }
81
+
82
+ /** Create a collection if needed */
83
+ export function normalizeToCollection(options: {
84
+ /** the node to look at */
85
+ node: React.ReactNode;
86
+
87
+ /** A Text asset */
88
+ TextComp?: React.ComponentType;
89
+
90
+ /** A collection asset */
91
+ CollectionComp?: React.ComponentType;
92
+ }) {
93
+ const { node, CollectionComp } = options;
94
+
95
+ if (
96
+ React.Children.count(node) > 1 &&
97
+ React.Children.toArray(node).every((n) => typeof n !== 'string')
98
+ ) {
99
+ if (!CollectionComp) {
100
+ throw new Error(
101
+ `Tried to convert array to a collection asset, but no Component was given.`
102
+ );
103
+ }
104
+
105
+ return <CollectionComp>{node}</CollectionComp>;
106
+ }
107
+
108
+ return normalizeText({ ...options, node });
109
+ }