@nordcraft/runtime 1.0.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/README.md +5 -0
- package/dist/api/createAPI.d.ts +20 -0
- package/dist/api/createAPI.js +319 -0
- package/dist/api/createAPI.js.map +1 -0
- package/dist/api/createAPIv2.d.ts +7 -0
- package/dist/api/createAPIv2.js +686 -0
- package/dist/api/createAPIv2.js.map +1 -0
- package/dist/components/createComponent.d.ts +13 -0
- package/dist/components/createComponent.js +216 -0
- package/dist/components/createComponent.js.map +1 -0
- package/dist/components/createElement.d.ts +3 -0
- package/dist/components/createElement.js +208 -0
- package/dist/components/createElement.js.map +1 -0
- package/dist/components/createNode.d.ts +22 -0
- package/dist/components/createNode.js +272 -0
- package/dist/components/createNode.js.map +1 -0
- package/dist/components/createSlot.d.ts +3 -0
- package/dist/components/createSlot.js +49 -0
- package/dist/components/createSlot.js.map +1 -0
- package/dist/components/createText.d.ts +23 -0
- package/dist/components/createText.js +68 -0
- package/dist/components/createText.js.map +1 -0
- package/dist/components/createText.test.d.ts +1 -0
- package/dist/components/createText.test.js +113 -0
- package/dist/components/createText.test.js.map +1 -0
- package/dist/components/renderComponent.d.ts +34 -0
- package/dist/components/renderComponent.js +66 -0
- package/dist/components/renderComponent.js.map +1 -0
- package/dist/context/isContextProvider.d.ts +2 -0
- package/dist/context/isContextProvider.js +5 -0
- package/dist/context/isContextProvider.js.map +1 -0
- package/dist/context/subscribeToContext.d.ts +4 -0
- package/dist/context/subscribeToContext.js +93 -0
- package/dist/context/subscribeToContext.js.map +1 -0
- package/dist/custom-components/components.d.ts +1 -0
- package/dist/custom-components/components.js +2 -0
- package/dist/custom-components/components.js.map +1 -0
- package/dist/custom-components/toddle-portal.d.ts +6 -0
- package/dist/custom-components/toddle-portal.js +20 -0
- package/dist/custom-components/toddle-portal.js.map +1 -0
- package/dist/custom-element/ToddleComponent.d.ts +37 -0
- package/dist/custom-element/ToddleComponent.js +244 -0
- package/dist/custom-element/ToddleComponent.js.map +1 -0
- package/dist/custom-element/defineComponents.d.ts +26 -0
- package/dist/custom-element/defineComponents.js +42 -0
- package/dist/custom-element/defineComponents.js.map +1 -0
- package/dist/custom-element.main.d.ts +3 -0
- package/dist/custom-element.main.esm.js +266 -0
- package/dist/custom-element.main.esm.js.map +7 -0
- package/dist/custom-element.main.js +14 -0
- package/dist/custom-element.main.js.map +1 -0
- package/dist/debug/logState.d.ts +4 -0
- package/dist/debug/logState.js +19 -0
- package/dist/debug/logState.js.map +1 -0
- package/dist/editor/drag-drop/dragEnded.d.ts +2 -0
- package/dist/editor/drag-drop/dragEnded.js +56 -0
- package/dist/editor/drag-drop/dragEnded.js.map +1 -0
- package/dist/editor/drag-drop/dragMove.d.ts +3 -0
- package/dist/editor/drag-drop/dragMove.js +74 -0
- package/dist/editor/drag-drop/dragMove.js.map +1 -0
- package/dist/editor/drag-drop/dragReorder.d.ts +3 -0
- package/dist/editor/drag-drop/dragReorder.js +92 -0
- package/dist/editor/drag-drop/dragReorder.js.map +1 -0
- package/dist/editor/drag-drop/dragStarted.d.ts +9 -0
- package/dist/editor/drag-drop/dragStarted.js +100 -0
- package/dist/editor/drag-drop/dragStarted.js.map +1 -0
- package/dist/editor/drag-drop/dropHighlight.d.ts +16 -0
- package/dist/editor/drag-drop/dropHighlight.js +50 -0
- package/dist/editor/drag-drop/dropHighlight.js.map +1 -0
- package/dist/editor/drag-drop/getInsertAreas.d.ts +20 -0
- package/dist/editor/drag-drop/getInsertAreas.js +220 -0
- package/dist/editor/drag-drop/getInsertAreas.js.map +1 -0
- package/dist/editor-preview.main.d.ts +19 -0
- package/dist/editor-preview.main.js +1303 -0
- package/dist/editor-preview.main.js.map +1 -0
- package/dist/events/handleAction.d.ts +3 -0
- package/dist/events/handleAction.js +307 -0
- package/dist/events/handleAction.js.map +1 -0
- package/dist/page.main.d.ts +7 -0
- package/dist/page.main.esm.js +8 -0
- package/dist/page.main.esm.js.map +7 -0
- package/dist/page.main.js +395 -0
- package/dist/page.main.js.map +1 -0
- package/dist/signal/signal.d.ts +19 -0
- package/dist/signal/signal.js +65 -0
- package/dist/signal/signal.js.map +1 -0
- package/dist/styles/style.d.ts +4 -0
- package/dist/styles/style.js +196 -0
- package/dist/styles/style.js.map +1 -0
- package/dist/utils/BatchQueue.d.ts +10 -0
- package/dist/utils/BatchQueue.js +25 -0
- package/dist/utils/BatchQueue.js.map +1 -0
- package/dist/utils/createFormulaCache.d.ts +3 -0
- package/dist/utils/createFormulaCache.js +81 -0
- package/dist/utils/createFormulaCache.js.map +1 -0
- package/dist/utils/findNearestLine.d.ts +13 -0
- package/dist/utils/findNearestLine.js +74 -0
- package/dist/utils/findNearestLine.js.map +1 -0
- package/dist/utils/findNearestLine.test.d.ts +1 -0
- package/dist/utils/findNearestLine.test.js +59 -0
- package/dist/utils/findNearestLine.test.js.map +1 -0
- package/dist/utils/getDragData.d.ts +1 -0
- package/dist/utils/getDragData.js +10 -0
- package/dist/utils/getDragData.js.map +1 -0
- package/dist/utils/getElementTagName.d.ts +3 -0
- package/dist/utils/getElementTagName.js +7 -0
- package/dist/utils/getElementTagName.js.map +1 -0
- package/dist/utils/nodes.d.ts +21 -0
- package/dist/utils/nodes.js +89 -0
- package/dist/utils/nodes.js.map +1 -0
- package/dist/utils/omitStyle.d.ts +2 -0
- package/dist/utils/omitStyle.js +13 -0
- package/dist/utils/omitStyle.js.map +1 -0
- package/dist/utils/rectHasPoint.d.ts +2 -0
- package/dist/utils/rectHasPoint.js +4 -0
- package/dist/utils/rectHasPoint.js.map +1 -0
- package/dist/utils/setAttribute.d.ts +4 -0
- package/dist/utils/setAttribute.js +57 -0
- package/dist/utils/setAttribute.js.map +1 -0
- package/dist/utils/tryStartViewTransition.d.ts +5 -0
- package/dist/utils/tryStartViewTransition.js +14 -0
- package/dist/utils/tryStartViewTransition.js.map +1 -0
- package/dist/utils/url.d.ts +2 -0
- package/dist/utils/url.js +36 -0
- package/dist/utils/url.js.map +1 -0
- package/package.json +25 -0
- package/src/api/createAPI.ts +375 -0
- package/src/api/createAPIv2.ts +931 -0
- package/src/components/createComponent.ts +280 -0
- package/src/components/createElement.ts +240 -0
- package/src/components/createNode.ts +381 -0
- package/src/components/createSlot.ts +61 -0
- package/src/components/createText.test.ts +117 -0
- package/src/components/createText.ts +104 -0
- package/src/components/renderComponent.ts +145 -0
- package/src/context/isContextProvider.ts +12 -0
- package/src/context/subscribeToContext.ts +135 -0
- package/src/custom-components/components.ts +1 -0
- package/src/custom-components/toddle-portal.ts +19 -0
- package/src/custom-element/ToddleComponent.ts +315 -0
- package/src/custom-element/defineComponents.ts +65 -0
- package/src/custom-element.main.ts +24 -0
- package/src/debug/logState.ts +30 -0
- package/src/editor/drag-drop/dragEnded.ts +75 -0
- package/src/editor/drag-drop/dragMove.ts +95 -0
- package/src/editor/drag-drop/dragReorder.ts +137 -0
- package/src/editor/drag-drop/dragStarted.ts +145 -0
- package/src/editor/drag-drop/dropHighlight.ts +82 -0
- package/src/editor/drag-drop/getInsertAreas.ts +235 -0
- package/src/editor/types.d.ts +36 -0
- package/src/editor-preview.main.ts +1782 -0
- package/src/events/handleAction.ts +387 -0
- package/src/page.main.ts +489 -0
- package/src/signal/signal.ts +74 -0
- package/src/styles/style.ts +254 -0
- package/src/types.d.ts +93 -0
- package/src/utils/BatchQueue.ts +24 -0
- package/src/utils/createFormulaCache.ts +96 -0
- package/src/utils/findNearestLine.test.ts +65 -0
- package/src/utils/findNearestLine.ts +92 -0
- package/src/utils/getDragData.ts +11 -0
- package/src/utils/getElementTagName.ts +14 -0
- package/src/utils/nodes.ts +125 -0
- package/src/utils/omitStyle.ts +19 -0
- package/src/utils/rectHasPoint.ts +5 -0
- package/src/utils/setAttribute.ts +56 -0
- package/src/utils/tryStartViewTransition.ts +32 -0
- package/src/utils/url.ts +45 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Component,
|
|
3
|
+
ComponentData,
|
|
4
|
+
SupportedNamespaces,
|
|
5
|
+
} from '@nordcraft/core/dist/component/component.types'
|
|
6
|
+
import type { ToddleEnv } from '@nordcraft/core/dist/formula/formula'
|
|
7
|
+
import type { Toddle } from '@nordcraft/core/dist/types'
|
|
8
|
+
import deepEqual from 'fast-deep-equal'
|
|
9
|
+
import { handleAction } from '../events/handleAction'
|
|
10
|
+
import type { Signal } from '../signal/signal'
|
|
11
|
+
import type {
|
|
12
|
+
ComponentChild,
|
|
13
|
+
ComponentContext,
|
|
14
|
+
FormulaCache,
|
|
15
|
+
LocationSignal,
|
|
16
|
+
PreviewShowSignal,
|
|
17
|
+
} from '../types'
|
|
18
|
+
import { BatchQueue } from '../utils/BatchQueue'
|
|
19
|
+
import { createNode } from './createNode'
|
|
20
|
+
|
|
21
|
+
interface RenderComponentProps {
|
|
22
|
+
component: Component
|
|
23
|
+
components: Component[]
|
|
24
|
+
dataSignal: Signal<ComponentData>
|
|
25
|
+
apis: Record<string, { fetch: Function; destroy: Function }>
|
|
26
|
+
abortSignal: AbortSignal
|
|
27
|
+
onEvent: (event: string, data: unknown) => void
|
|
28
|
+
isRootComponent: boolean
|
|
29
|
+
formulaCache: FormulaCache
|
|
30
|
+
path: string
|
|
31
|
+
children: Record<string, Array<ComponentChild>>
|
|
32
|
+
root: Document | ShadowRoot
|
|
33
|
+
providers: Record<
|
|
34
|
+
string,
|
|
35
|
+
{
|
|
36
|
+
component: Component
|
|
37
|
+
formulaDataSignals: Record<string, Signal<ComponentData>>
|
|
38
|
+
ctx: ComponentContext
|
|
39
|
+
}
|
|
40
|
+
>
|
|
41
|
+
package: string | undefined
|
|
42
|
+
parentElement: Element | ShadowRoot
|
|
43
|
+
instance: Record<string, string>
|
|
44
|
+
toddle: Toddle<LocationSignal, PreviewShowSignal>
|
|
45
|
+
namespace?: SupportedNamespaces
|
|
46
|
+
env: ToddleEnv
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const BATCH_QUEUE = new BatchQueue()
|
|
50
|
+
|
|
51
|
+
export function renderComponent({
|
|
52
|
+
component,
|
|
53
|
+
dataSignal,
|
|
54
|
+
onEvent,
|
|
55
|
+
isRootComponent,
|
|
56
|
+
path,
|
|
57
|
+
children,
|
|
58
|
+
formulaCache,
|
|
59
|
+
components,
|
|
60
|
+
apis,
|
|
61
|
+
abortSignal,
|
|
62
|
+
root,
|
|
63
|
+
providers,
|
|
64
|
+
package: packageName,
|
|
65
|
+
parentElement,
|
|
66
|
+
instance,
|
|
67
|
+
toddle,
|
|
68
|
+
namespace,
|
|
69
|
+
env,
|
|
70
|
+
}: RenderComponentProps): ReadonlyArray<Element | Text> {
|
|
71
|
+
const ctx: ComponentContext = {
|
|
72
|
+
triggerEvent: onEvent,
|
|
73
|
+
component,
|
|
74
|
+
components,
|
|
75
|
+
dataSignal,
|
|
76
|
+
isRootComponent,
|
|
77
|
+
apis,
|
|
78
|
+
formulaCache,
|
|
79
|
+
children,
|
|
80
|
+
abortSignal,
|
|
81
|
+
root,
|
|
82
|
+
providers,
|
|
83
|
+
package: packageName,
|
|
84
|
+
toddle,
|
|
85
|
+
env,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const rootElem = createNode({
|
|
89
|
+
id: 'root',
|
|
90
|
+
path,
|
|
91
|
+
dataSignal,
|
|
92
|
+
ctx,
|
|
93
|
+
parentElement,
|
|
94
|
+
namespace,
|
|
95
|
+
instance,
|
|
96
|
+
})
|
|
97
|
+
BATCH_QUEUE.add(() => {
|
|
98
|
+
let prev: Record<string, any> | undefined
|
|
99
|
+
if (
|
|
100
|
+
component.onAttributeChange?.actions &&
|
|
101
|
+
component.onAttributeChange.actions.length > 0
|
|
102
|
+
) {
|
|
103
|
+
dataSignal
|
|
104
|
+
.map((data) => data.Attributes)
|
|
105
|
+
.subscribe((props) => {
|
|
106
|
+
if (prev) {
|
|
107
|
+
component.onAttributeChange?.actions.forEach((action) => {
|
|
108
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
109
|
+
handleAction(
|
|
110
|
+
action,
|
|
111
|
+
dataSignal.get(),
|
|
112
|
+
ctx,
|
|
113
|
+
new CustomEvent('attribute-change', {
|
|
114
|
+
detail: Object.entries(props).reduce(
|
|
115
|
+
(
|
|
116
|
+
changes: Record<string, { current: any; new: any }>,
|
|
117
|
+
[key, value],
|
|
118
|
+
) => {
|
|
119
|
+
if (
|
|
120
|
+
deepEqual(value, prev![key]) === false &&
|
|
121
|
+
component.attributes[key]?.name
|
|
122
|
+
) {
|
|
123
|
+
changes[component.attributes[key]?.name] = {
|
|
124
|
+
current: prev![key],
|
|
125
|
+
new: value,
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return changes
|
|
129
|
+
},
|
|
130
|
+
{},
|
|
131
|
+
),
|
|
132
|
+
}),
|
|
133
|
+
)
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
prev = props
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
component.onLoad?.actions.forEach((action) => {
|
|
140
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
141
|
+
handleAction(action, dataSignal.get(), ctx)
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
return rootElem
|
|
145
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
|
|
2
|
+
import type { Component } from '@nordcraft/core/dist/component/component.types'
|
|
3
|
+
|
|
4
|
+
export const isContextProvider = (component: Component) =>
|
|
5
|
+
(component.formulas &&
|
|
6
|
+
Object.values(component.formulas).some(
|
|
7
|
+
({ exposeInContext }) => exposeInContext,
|
|
8
|
+
)) ||
|
|
9
|
+
(component.workflows &&
|
|
10
|
+
Object.values(component.workflows).some(
|
|
11
|
+
({ exposeInContext }) => exposeInContext,
|
|
12
|
+
))
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Component,
|
|
3
|
+
ComponentData,
|
|
4
|
+
} from '@nordcraft/core/dist/component/component.types'
|
|
5
|
+
import type { FormulaContext } from '@nordcraft/core/dist/formula/formula'
|
|
6
|
+
import { applyFormula } from '@nordcraft/core/dist/formula/formula'
|
|
7
|
+
import { mapObject } from '@nordcraft/core/dist/utils/collections'
|
|
8
|
+
import { isDefined } from '@nordcraft/core/dist/utils/util'
|
|
9
|
+
import type { Signal } from '../signal/signal'
|
|
10
|
+
import type { ComponentContext } from '../types'
|
|
11
|
+
|
|
12
|
+
export function subscribeToContext(
|
|
13
|
+
componentDataSignal: Signal<ComponentData>,
|
|
14
|
+
component: Component,
|
|
15
|
+
ctx: ComponentContext,
|
|
16
|
+
) {
|
|
17
|
+
Object.entries(component.contexts ?? {}).forEach(
|
|
18
|
+
([providerName, context]) => {
|
|
19
|
+
const provider =
|
|
20
|
+
ctx.providers[[ctx.package, providerName].filter(isDefined).join('/')]
|
|
21
|
+
|
|
22
|
+
if (provider) {
|
|
23
|
+
context.formulas.forEach((formulaName) => {
|
|
24
|
+
const formulaDataSignal = provider.formulaDataSignals[formulaName]
|
|
25
|
+
if (!formulaDataSignal) {
|
|
26
|
+
// eslint-disable-next-line no-console
|
|
27
|
+
console.warn(
|
|
28
|
+
`Provider ${providerName} does not expose a formula named "${formulaName}". Available formulas are: ["${Object.keys(
|
|
29
|
+
provider.formulaDataSignals,
|
|
30
|
+
).join('", "')}"]`,
|
|
31
|
+
)
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
formulaDataSignal.subscribe((value) => {
|
|
36
|
+
componentDataSignal.update((data) => ({
|
|
37
|
+
...data,
|
|
38
|
+
Contexts: {
|
|
39
|
+
...data.Contexts,
|
|
40
|
+
[providerName]: {
|
|
41
|
+
...data.Contexts?.[providerName],
|
|
42
|
+
[formulaName]: value,
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
}))
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// In preview and absence of a real provider, we fake providers with their testData values. This is useful for testing components in isolation.
|
|
51
|
+
// This is for preview mode only, and should preferably be stripped from the page and custom-elements runtime.
|
|
52
|
+
else if (
|
|
53
|
+
!provider &&
|
|
54
|
+
ctx.env.runtime === 'preview' &&
|
|
55
|
+
ctx.toddle._preview
|
|
56
|
+
) {
|
|
57
|
+
const testProvider = ctx.components?.find(
|
|
58
|
+
(comp) =>
|
|
59
|
+
comp.name ===
|
|
60
|
+
[ctx.package, providerName].filter(isDefined).join('/'),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
if (!testProvider) {
|
|
64
|
+
// eslint-disable-next-line no-console
|
|
65
|
+
console.error(
|
|
66
|
+
`Could not find provider "${providerName}". No such component exist.`,
|
|
67
|
+
)
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Derive the package name from the provider name as we do not have a real component to work with
|
|
72
|
+
const [, testProviderPackage] = providerName.split('/').reverse()
|
|
73
|
+
const formulaContext: FormulaContext = {
|
|
74
|
+
data: {
|
|
75
|
+
Attributes: mapObject(testProvider.attributes, ([name, attr]) => [
|
|
76
|
+
name,
|
|
77
|
+
attr.testValue,
|
|
78
|
+
]),
|
|
79
|
+
},
|
|
80
|
+
component: testProvider,
|
|
81
|
+
root: ctx?.root,
|
|
82
|
+
formulaCache: {},
|
|
83
|
+
package: testProviderPackage ?? ctx?.package,
|
|
84
|
+
toddle: ctx.toddle,
|
|
85
|
+
env: ctx.env,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (testProvider.route) {
|
|
89
|
+
formulaContext.data['URL parameters'] = {
|
|
90
|
+
...Object.fromEntries(
|
|
91
|
+
testProvider.route.path
|
|
92
|
+
.filter((p) => p.type === 'param')
|
|
93
|
+
.map((p) => [p.name, p.testValue]),
|
|
94
|
+
),
|
|
95
|
+
...mapObject(testProvider.route.query, ([name, { testValue }]) => [
|
|
96
|
+
name,
|
|
97
|
+
testValue,
|
|
98
|
+
]),
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
formulaContext.data.Variables = mapObject(
|
|
102
|
+
testProvider.variables,
|
|
103
|
+
([name, variable]) => [
|
|
104
|
+
name,
|
|
105
|
+
applyFormula(variable.initialValue, formulaContext),
|
|
106
|
+
],
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
componentDataSignal.update((data) => ({
|
|
110
|
+
...data,
|
|
111
|
+
Contexts: {
|
|
112
|
+
...data.Contexts,
|
|
113
|
+
[providerName]: Object.fromEntries(
|
|
114
|
+
context.formulas.map((formulaName) => {
|
|
115
|
+
const formula = testProvider.formulas?.[formulaName]
|
|
116
|
+
if (!formula) {
|
|
117
|
+
// eslint-disable-next-line no-console
|
|
118
|
+
console.warn(
|
|
119
|
+
`Could not find formula "${formulaName}" in component "${providerName}"`,
|
|
120
|
+
)
|
|
121
|
+
return [formulaName, null]
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return [
|
|
125
|
+
formulaName,
|
|
126
|
+
applyFormula(formula.formula, formulaContext),
|
|
127
|
+
]
|
|
128
|
+
}),
|
|
129
|
+
),
|
|
130
|
+
},
|
|
131
|
+
}))
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
)
|
|
135
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import './toddle-portal'
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
class Portal extends HTMLElement {
|
|
2
|
+
root: HTMLElement
|
|
3
|
+
constructor() {
|
|
4
|
+
// Always call super first in constructor
|
|
5
|
+
super()
|
|
6
|
+
this.root = document.createElement('div')
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
connectedCallback() {
|
|
10
|
+
for (const child of Array.from(this.children)) {
|
|
11
|
+
this.root.appendChild(child)
|
|
12
|
+
}
|
|
13
|
+
document.body.appendChild(this.root)
|
|
14
|
+
}
|
|
15
|
+
disconnectedCallback() {
|
|
16
|
+
this.root?.remove()
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
customElements.define('toddle-portal', Portal)
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import { isLegacyApi, sortApiObjects } from '@nordcraft/core/dist/api/api'
|
|
2
|
+
import type {
|
|
3
|
+
Component,
|
|
4
|
+
ComponentData,
|
|
5
|
+
} from '@nordcraft/core/dist/component/component.types'
|
|
6
|
+
import type { ToddleEnv } from '@nordcraft/core/dist/formula/formula'
|
|
7
|
+
import { applyFormula } from '@nordcraft/core/dist/formula/formula'
|
|
8
|
+
import { createStylesheet } from '@nordcraft/core/dist/styling/style.css'
|
|
9
|
+
import type { Theme } from '@nordcraft/core/dist/styling/theme'
|
|
10
|
+
import { theme as defaultTheme } from '@nordcraft/core/dist/styling/theme.const'
|
|
11
|
+
import type { RequireFields, Toddle } from '@nordcraft/core/dist/types'
|
|
12
|
+
import { mapObject } from '@nordcraft/core/dist/utils/collections'
|
|
13
|
+
import { createLegacyAPI } from '../api/createAPI'
|
|
14
|
+
import { createAPI } from '../api/createAPIv2'
|
|
15
|
+
import { renderComponent } from '../components/renderComponent'
|
|
16
|
+
import { isContextProvider } from '../context/isContextProvider'
|
|
17
|
+
import type { Signal } from '../signal/signal'
|
|
18
|
+
import { signal } from '../signal/signal'
|
|
19
|
+
import type { ComponentContext, ContextApi, LocationSignal } from '../types'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Base class for all toddle components
|
|
23
|
+
*/
|
|
24
|
+
export class ToddleComponent extends HTMLElement {
|
|
25
|
+
/**
|
|
26
|
+
* Public reference to the toddle instance for debugging purposes. `el.toddle.errors` can be used to check for non-verbose errors.
|
|
27
|
+
*/
|
|
28
|
+
toddle: Toddle<LocationSignal, never>
|
|
29
|
+
#component: Component
|
|
30
|
+
#ctx: ComponentContext
|
|
31
|
+
#shadowRoot: ShadowRoot
|
|
32
|
+
#signal: Signal<ComponentData>
|
|
33
|
+
#files: { themes: Theme[] }
|
|
34
|
+
|
|
35
|
+
constructor(
|
|
36
|
+
component: Component,
|
|
37
|
+
options: { components: Component[]; themes: Theme[] },
|
|
38
|
+
toddle: Toddle<LocationSignal, never>,
|
|
39
|
+
) {
|
|
40
|
+
super()
|
|
41
|
+
this.toddle = toddle
|
|
42
|
+
const internals = this.attachInternals()
|
|
43
|
+
if (internals.shadowRoot) {
|
|
44
|
+
// Not used yet, but can be used to hydrate rather than render the shadow dom
|
|
45
|
+
this.#shadowRoot = internals.shadowRoot
|
|
46
|
+
} else {
|
|
47
|
+
this.#shadowRoot = this.attachShadow({ mode: 'open' })
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const env: ToddleEnv = {
|
|
51
|
+
branchName: toddle.branch || 'main',
|
|
52
|
+
isServer: false,
|
|
53
|
+
request: undefined,
|
|
54
|
+
runtime: 'custom-element',
|
|
55
|
+
logErrors: true,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
this.#component = component
|
|
59
|
+
this.#signal = createSignal({
|
|
60
|
+
component,
|
|
61
|
+
root: this.#shadowRoot,
|
|
62
|
+
toddle,
|
|
63
|
+
env,
|
|
64
|
+
})
|
|
65
|
+
this.#files = {
|
|
66
|
+
themes: options.themes,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Call the abort signal if the component's datasignal is destroyed (component unmounted) to cancel any pending requests
|
|
70
|
+
const abortController = new AbortController()
|
|
71
|
+
this.#signal.subscribe(() => {}, {
|
|
72
|
+
destroy: () =>
|
|
73
|
+
abortController.abort(`Component ${component.name} unmounted`),
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
this.#ctx = {
|
|
77
|
+
triggerEvent: this.dispatch.bind(this),
|
|
78
|
+
root: this.#shadowRoot,
|
|
79
|
+
isRootComponent: true,
|
|
80
|
+
components: options.components,
|
|
81
|
+
component: this.#component,
|
|
82
|
+
dataSignal: this.#signal,
|
|
83
|
+
formulaCache: {},
|
|
84
|
+
apis: {},
|
|
85
|
+
abortSignal: abortController.signal,
|
|
86
|
+
children: {},
|
|
87
|
+
providers: {},
|
|
88
|
+
package: undefined,
|
|
89
|
+
toddle,
|
|
90
|
+
env,
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
connectedCallback() {
|
|
95
|
+
sortApiObjects(Object.entries(this.#component.apis)).forEach(
|
|
96
|
+
([name, api]) => {
|
|
97
|
+
if (isLegacyApi(api)) {
|
|
98
|
+
this.#ctx.apis[name] = createLegacyAPI(api, this.#ctx)
|
|
99
|
+
} else {
|
|
100
|
+
this.#ctx.apis[name] = createAPI(api, this.#ctx)
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
)
|
|
104
|
+
Object.values(this.#ctx.apis)
|
|
105
|
+
.filter(
|
|
106
|
+
(api): api is RequireFields<ContextApi, 'triggerActions'> =>
|
|
107
|
+
api.triggerActions !== undefined,
|
|
108
|
+
)
|
|
109
|
+
.forEach((api) => {
|
|
110
|
+
api.triggerActions()
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
let providers = this.#ctx.providers
|
|
114
|
+
if (isContextProvider(this.#component)) {
|
|
115
|
+
// Subscribe to exposed formulas and update the component's data signal
|
|
116
|
+
const formulaDataSignals = Object.fromEntries(
|
|
117
|
+
Object.entries(this.#component.formulas ?? {})
|
|
118
|
+
.filter(([, formula]) => formula.exposeInContext)
|
|
119
|
+
.map(([name, formula]) => [
|
|
120
|
+
name,
|
|
121
|
+
this.#signal.map((data) =>
|
|
122
|
+
applyFormula(formula.formula, {
|
|
123
|
+
data,
|
|
124
|
+
component: this.#component,
|
|
125
|
+
formulaCache: this.#ctx.formulaCache,
|
|
126
|
+
root: this.#ctx.root,
|
|
127
|
+
package: this.#ctx.package,
|
|
128
|
+
toddle: this.#ctx.toddle,
|
|
129
|
+
env: this.#ctx.env,
|
|
130
|
+
}),
|
|
131
|
+
),
|
|
132
|
+
]),
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
providers = {
|
|
136
|
+
...providers,
|
|
137
|
+
[this.#component.name]: {
|
|
138
|
+
component: this.#component,
|
|
139
|
+
formulaDataSignals,
|
|
140
|
+
ctx: this.#ctx,
|
|
141
|
+
},
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
this.#ctx.providers = providers
|
|
146
|
+
this.render()
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
disconnectedCallback() {
|
|
150
|
+
this.#signal.destroy()
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
dispatch(eventName: string, data: any) {
|
|
154
|
+
this.dispatchEvent(
|
|
155
|
+
new CustomEvent(eventName, {
|
|
156
|
+
detail: data,
|
|
157
|
+
bubbles: true,
|
|
158
|
+
composed: true,
|
|
159
|
+
}),
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
render() {
|
|
164
|
+
const elements = renderComponent({
|
|
165
|
+
...this.#ctx,
|
|
166
|
+
path: '0',
|
|
167
|
+
onEvent: this.dispatch.bind(this),
|
|
168
|
+
parentElement: this.#shadowRoot,
|
|
169
|
+
instance: { [this.#component.name]: 'root' },
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
this.#shadowRoot.innerHTML = ''
|
|
173
|
+
const styles = createStylesheet(
|
|
174
|
+
this.#ctx.component,
|
|
175
|
+
this.#ctx.components,
|
|
176
|
+
this.#files.themes ? Object.values(this.#files.themes)[0] : defaultTheme,
|
|
177
|
+
{ includeResetStyle: true, createFontFaces: false },
|
|
178
|
+
)
|
|
179
|
+
const stylesElem = document.createElement('style')
|
|
180
|
+
stylesElem.appendChild(document.createTextNode(styles))
|
|
181
|
+
this.#shadowRoot.appendChild(stylesElem)
|
|
182
|
+
const rootElement = elements[0] as Element | null
|
|
183
|
+
if (rootElement) {
|
|
184
|
+
this.#shadowRoot.appendChild(rootElement)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Overload the setAttribute method to allow setting complex attributes
|
|
189
|
+
setAttribute(name: string, value: unknown) {
|
|
190
|
+
switch (typeof value) {
|
|
191
|
+
case 'number':
|
|
192
|
+
super.setAttribute(name, String(value))
|
|
193
|
+
break
|
|
194
|
+
case 'string':
|
|
195
|
+
super.setAttribute(name, value)
|
|
196
|
+
// Return early, as signal is updated through attributeChangedCallback on string values
|
|
197
|
+
return this
|
|
198
|
+
default:
|
|
199
|
+
super.setAttribute(name, `[Object ${typeof value}]`)
|
|
200
|
+
break
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Update the signal with complex value
|
|
204
|
+
this.#signal.set({
|
|
205
|
+
...this.#signal.get(),
|
|
206
|
+
Attributes: {
|
|
207
|
+
...this.#signal.get().Attributes,
|
|
208
|
+
[name]: value,
|
|
209
|
+
},
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
return this
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Overload the getAttribute method to point to source of truth (the signal)
|
|
216
|
+
getAttribute<T>(name: string) {
|
|
217
|
+
return (
|
|
218
|
+
(this.#signal.get().Attributes[name] as T) || super.getAttribute(name)
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
attributeChangedCallback(name: string, oldValue: never, newValue: string) {
|
|
223
|
+
const attributeName = this.getAttributeCaseInsensitive(name)
|
|
224
|
+
const currentRawValue = this.getAttribute(attributeName)
|
|
225
|
+
// Is this is a complex value that has already been set by `setAttribute`
|
|
226
|
+
if (newValue === '[Object object]') {
|
|
227
|
+
return
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
//Has this just been set by `setAttribute` as a number
|
|
231
|
+
if (parseFloat(newValue) === currentRawValue) {
|
|
232
|
+
return
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
this.#signal.set({
|
|
236
|
+
...this.#signal.get(),
|
|
237
|
+
Attributes: {
|
|
238
|
+
...this.#signal.get().Attributes,
|
|
239
|
+
[attributeName]: newValue,
|
|
240
|
+
},
|
|
241
|
+
})
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private getAttributeCaseInsensitive(name: string) {
|
|
245
|
+
const attributeName = Object.keys(this.#signal.get().Attributes).find(
|
|
246
|
+
(key) => key.toLowerCase() === name.toLowerCase(),
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
// This should never happen (TM) as we only observe attributes that are defined on the component
|
|
250
|
+
if (!attributeName) {
|
|
251
|
+
throw new Error(
|
|
252
|
+
`Unable to find attribute ${name} on component ${this.#component.name}`,
|
|
253
|
+
)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return attributeName
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Debugging purposes
|
|
260
|
+
get __component() {
|
|
261
|
+
return this.#component
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
get __ctx() {
|
|
265
|
+
return this.#ctx
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
get __signal() {
|
|
269
|
+
return this.#signal
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export const createSignal = ({
|
|
274
|
+
component,
|
|
275
|
+
root,
|
|
276
|
+
toddle,
|
|
277
|
+
env,
|
|
278
|
+
}: {
|
|
279
|
+
component: Component
|
|
280
|
+
root: ShadowRoot
|
|
281
|
+
toddle: Toddle<LocationSignal, never>
|
|
282
|
+
env: ToddleEnv
|
|
283
|
+
}) => {
|
|
284
|
+
return signal<ComponentData>({
|
|
285
|
+
// Pages are not supported as custom elements, so no need to add location signal
|
|
286
|
+
Location: undefined,
|
|
287
|
+
Variables: mapObject(component.variables, ([name, { initialValue }]) => {
|
|
288
|
+
if (!component) {
|
|
289
|
+
throw new Error(`Component not found`)
|
|
290
|
+
}
|
|
291
|
+
return [
|
|
292
|
+
name,
|
|
293
|
+
applyFormula(initialValue, {
|
|
294
|
+
data: {
|
|
295
|
+
Attributes: {},
|
|
296
|
+
},
|
|
297
|
+
component: component,
|
|
298
|
+
root,
|
|
299
|
+
package: undefined,
|
|
300
|
+
toddle,
|
|
301
|
+
env,
|
|
302
|
+
}),
|
|
303
|
+
]
|
|
304
|
+
}),
|
|
305
|
+
Attributes: mapObject(component.attributes, ([name]) => [
|
|
306
|
+
name,
|
|
307
|
+
// TODO: Perhaps we can get it from the DOM already and set initial attributes already?
|
|
308
|
+
undefined,
|
|
309
|
+
]),
|
|
310
|
+
Apis: mapObject(component.apis, ([name]) => [
|
|
311
|
+
name,
|
|
312
|
+
{ data: null, isLoading: false, error: null },
|
|
313
|
+
]),
|
|
314
|
+
})
|
|
315
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Component } from '@nordcraft/core/dist/component/component.types'
|
|
2
|
+
import type { Theme } from '@nordcraft/core/dist/styling/theme'
|
|
3
|
+
import type { Toddle } from '@nordcraft/core/dist/types'
|
|
4
|
+
import { safeCustomElementName } from '@nordcraft/core/dist/utils/customElements'
|
|
5
|
+
import type { LocationSignal } from '../types'
|
|
6
|
+
import { ToddleComponent } from './ToddleComponent'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Define each component as a new web component
|
|
10
|
+
*
|
|
11
|
+
* Use them like: `<toddle-test my-attr="test" style="--my-color: rebeccapurple"></toddle-test>`
|
|
12
|
+
*
|
|
13
|
+
* You can access the component and `addEventListener` to it like:
|
|
14
|
+
*
|
|
15
|
+
* ```js
|
|
16
|
+
* const component = document.querySelector('toddle-test')
|
|
17
|
+
* component.addEventListener('my-event', (e) => {
|
|
18
|
+
* console.log(e.detail)
|
|
19
|
+
* })
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* @param componentNames - The names of the components to define. These should be the names of the components in the app context
|
|
23
|
+
* @param options - A subset of the app context. This holds a list of all the components needed to define the components in `componentNames`
|
|
24
|
+
* @param toddle - Also available at `window.toddle`. However, multiple instances of toddle can exist on the same page, so we pass a reference here. We should ultimately remove the global scope reference as polite web components should be self-contained.
|
|
25
|
+
*/
|
|
26
|
+
export const defineComponents = (
|
|
27
|
+
componentNames: string[],
|
|
28
|
+
options: {
|
|
29
|
+
components: Component[]
|
|
30
|
+
themes: Theme[]
|
|
31
|
+
},
|
|
32
|
+
toddle: Toddle<LocationSignal, never>,
|
|
33
|
+
) => {
|
|
34
|
+
componentNames
|
|
35
|
+
.map<Component>(
|
|
36
|
+
(name) =>
|
|
37
|
+
options.components.find(
|
|
38
|
+
(component) => component.name === name,
|
|
39
|
+
) as Component,
|
|
40
|
+
)
|
|
41
|
+
.forEach((component) => {
|
|
42
|
+
const tag = safeCustomElementName(component.name)
|
|
43
|
+
if (customElements.get(tag)) {
|
|
44
|
+
// eslint-disable-next-line no-console
|
|
45
|
+
console.warn(`Component ${tag} already defined`)
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
customElements.define(
|
|
50
|
+
tag,
|
|
51
|
+
class extends ToddleComponent {
|
|
52
|
+
constructor() {
|
|
53
|
+
super(component, options, toddle)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// When read from DOM, all attributes are lower case, so we must observe them as such
|
|
57
|
+
static get observedAttributes() {
|
|
58
|
+
return Object.keys(component.attributes ?? {}).map((key) =>
|
|
59
|
+
key.toLowerCase(),
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
)
|
|
64
|
+
})
|
|
65
|
+
}
|