@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.
Files changed (168) hide show
  1. package/README.md +5 -0
  2. package/dist/api/createAPI.d.ts +20 -0
  3. package/dist/api/createAPI.js +319 -0
  4. package/dist/api/createAPI.js.map +1 -0
  5. package/dist/api/createAPIv2.d.ts +7 -0
  6. package/dist/api/createAPIv2.js +686 -0
  7. package/dist/api/createAPIv2.js.map +1 -0
  8. package/dist/components/createComponent.d.ts +13 -0
  9. package/dist/components/createComponent.js +216 -0
  10. package/dist/components/createComponent.js.map +1 -0
  11. package/dist/components/createElement.d.ts +3 -0
  12. package/dist/components/createElement.js +208 -0
  13. package/dist/components/createElement.js.map +1 -0
  14. package/dist/components/createNode.d.ts +22 -0
  15. package/dist/components/createNode.js +272 -0
  16. package/dist/components/createNode.js.map +1 -0
  17. package/dist/components/createSlot.d.ts +3 -0
  18. package/dist/components/createSlot.js +49 -0
  19. package/dist/components/createSlot.js.map +1 -0
  20. package/dist/components/createText.d.ts +23 -0
  21. package/dist/components/createText.js +68 -0
  22. package/dist/components/createText.js.map +1 -0
  23. package/dist/components/createText.test.d.ts +1 -0
  24. package/dist/components/createText.test.js +113 -0
  25. package/dist/components/createText.test.js.map +1 -0
  26. package/dist/components/renderComponent.d.ts +34 -0
  27. package/dist/components/renderComponent.js +66 -0
  28. package/dist/components/renderComponent.js.map +1 -0
  29. package/dist/context/isContextProvider.d.ts +2 -0
  30. package/dist/context/isContextProvider.js +5 -0
  31. package/dist/context/isContextProvider.js.map +1 -0
  32. package/dist/context/subscribeToContext.d.ts +4 -0
  33. package/dist/context/subscribeToContext.js +93 -0
  34. package/dist/context/subscribeToContext.js.map +1 -0
  35. package/dist/custom-components/components.d.ts +1 -0
  36. package/dist/custom-components/components.js +2 -0
  37. package/dist/custom-components/components.js.map +1 -0
  38. package/dist/custom-components/toddle-portal.d.ts +6 -0
  39. package/dist/custom-components/toddle-portal.js +20 -0
  40. package/dist/custom-components/toddle-portal.js.map +1 -0
  41. package/dist/custom-element/ToddleComponent.d.ts +37 -0
  42. package/dist/custom-element/ToddleComponent.js +244 -0
  43. package/dist/custom-element/ToddleComponent.js.map +1 -0
  44. package/dist/custom-element/defineComponents.d.ts +26 -0
  45. package/dist/custom-element/defineComponents.js +42 -0
  46. package/dist/custom-element/defineComponents.js.map +1 -0
  47. package/dist/custom-element.main.d.ts +3 -0
  48. package/dist/custom-element.main.esm.js +266 -0
  49. package/dist/custom-element.main.esm.js.map +7 -0
  50. package/dist/custom-element.main.js +14 -0
  51. package/dist/custom-element.main.js.map +1 -0
  52. package/dist/debug/logState.d.ts +4 -0
  53. package/dist/debug/logState.js +19 -0
  54. package/dist/debug/logState.js.map +1 -0
  55. package/dist/editor/drag-drop/dragEnded.d.ts +2 -0
  56. package/dist/editor/drag-drop/dragEnded.js +56 -0
  57. package/dist/editor/drag-drop/dragEnded.js.map +1 -0
  58. package/dist/editor/drag-drop/dragMove.d.ts +3 -0
  59. package/dist/editor/drag-drop/dragMove.js +74 -0
  60. package/dist/editor/drag-drop/dragMove.js.map +1 -0
  61. package/dist/editor/drag-drop/dragReorder.d.ts +3 -0
  62. package/dist/editor/drag-drop/dragReorder.js +92 -0
  63. package/dist/editor/drag-drop/dragReorder.js.map +1 -0
  64. package/dist/editor/drag-drop/dragStarted.d.ts +9 -0
  65. package/dist/editor/drag-drop/dragStarted.js +100 -0
  66. package/dist/editor/drag-drop/dragStarted.js.map +1 -0
  67. package/dist/editor/drag-drop/dropHighlight.d.ts +16 -0
  68. package/dist/editor/drag-drop/dropHighlight.js +50 -0
  69. package/dist/editor/drag-drop/dropHighlight.js.map +1 -0
  70. package/dist/editor/drag-drop/getInsertAreas.d.ts +20 -0
  71. package/dist/editor/drag-drop/getInsertAreas.js +220 -0
  72. package/dist/editor/drag-drop/getInsertAreas.js.map +1 -0
  73. package/dist/editor-preview.main.d.ts +19 -0
  74. package/dist/editor-preview.main.js +1303 -0
  75. package/dist/editor-preview.main.js.map +1 -0
  76. package/dist/events/handleAction.d.ts +3 -0
  77. package/dist/events/handleAction.js +307 -0
  78. package/dist/events/handleAction.js.map +1 -0
  79. package/dist/page.main.d.ts +7 -0
  80. package/dist/page.main.esm.js +8 -0
  81. package/dist/page.main.esm.js.map +7 -0
  82. package/dist/page.main.js +395 -0
  83. package/dist/page.main.js.map +1 -0
  84. package/dist/signal/signal.d.ts +19 -0
  85. package/dist/signal/signal.js +65 -0
  86. package/dist/signal/signal.js.map +1 -0
  87. package/dist/styles/style.d.ts +4 -0
  88. package/dist/styles/style.js +196 -0
  89. package/dist/styles/style.js.map +1 -0
  90. package/dist/utils/BatchQueue.d.ts +10 -0
  91. package/dist/utils/BatchQueue.js +25 -0
  92. package/dist/utils/BatchQueue.js.map +1 -0
  93. package/dist/utils/createFormulaCache.d.ts +3 -0
  94. package/dist/utils/createFormulaCache.js +81 -0
  95. package/dist/utils/createFormulaCache.js.map +1 -0
  96. package/dist/utils/findNearestLine.d.ts +13 -0
  97. package/dist/utils/findNearestLine.js +74 -0
  98. package/dist/utils/findNearestLine.js.map +1 -0
  99. package/dist/utils/findNearestLine.test.d.ts +1 -0
  100. package/dist/utils/findNearestLine.test.js +59 -0
  101. package/dist/utils/findNearestLine.test.js.map +1 -0
  102. package/dist/utils/getDragData.d.ts +1 -0
  103. package/dist/utils/getDragData.js +10 -0
  104. package/dist/utils/getDragData.js.map +1 -0
  105. package/dist/utils/getElementTagName.d.ts +3 -0
  106. package/dist/utils/getElementTagName.js +7 -0
  107. package/dist/utils/getElementTagName.js.map +1 -0
  108. package/dist/utils/nodes.d.ts +21 -0
  109. package/dist/utils/nodes.js +89 -0
  110. package/dist/utils/nodes.js.map +1 -0
  111. package/dist/utils/omitStyle.d.ts +2 -0
  112. package/dist/utils/omitStyle.js +13 -0
  113. package/dist/utils/omitStyle.js.map +1 -0
  114. package/dist/utils/rectHasPoint.d.ts +2 -0
  115. package/dist/utils/rectHasPoint.js +4 -0
  116. package/dist/utils/rectHasPoint.js.map +1 -0
  117. package/dist/utils/setAttribute.d.ts +4 -0
  118. package/dist/utils/setAttribute.js +57 -0
  119. package/dist/utils/setAttribute.js.map +1 -0
  120. package/dist/utils/tryStartViewTransition.d.ts +5 -0
  121. package/dist/utils/tryStartViewTransition.js +14 -0
  122. package/dist/utils/tryStartViewTransition.js.map +1 -0
  123. package/dist/utils/url.d.ts +2 -0
  124. package/dist/utils/url.js +36 -0
  125. package/dist/utils/url.js.map +1 -0
  126. package/package.json +25 -0
  127. package/src/api/createAPI.ts +375 -0
  128. package/src/api/createAPIv2.ts +931 -0
  129. package/src/components/createComponent.ts +280 -0
  130. package/src/components/createElement.ts +240 -0
  131. package/src/components/createNode.ts +381 -0
  132. package/src/components/createSlot.ts +61 -0
  133. package/src/components/createText.test.ts +117 -0
  134. package/src/components/createText.ts +104 -0
  135. package/src/components/renderComponent.ts +145 -0
  136. package/src/context/isContextProvider.ts +12 -0
  137. package/src/context/subscribeToContext.ts +135 -0
  138. package/src/custom-components/components.ts +1 -0
  139. package/src/custom-components/toddle-portal.ts +19 -0
  140. package/src/custom-element/ToddleComponent.ts +315 -0
  141. package/src/custom-element/defineComponents.ts +65 -0
  142. package/src/custom-element.main.ts +24 -0
  143. package/src/debug/logState.ts +30 -0
  144. package/src/editor/drag-drop/dragEnded.ts +75 -0
  145. package/src/editor/drag-drop/dragMove.ts +95 -0
  146. package/src/editor/drag-drop/dragReorder.ts +137 -0
  147. package/src/editor/drag-drop/dragStarted.ts +145 -0
  148. package/src/editor/drag-drop/dropHighlight.ts +82 -0
  149. package/src/editor/drag-drop/getInsertAreas.ts +235 -0
  150. package/src/editor/types.d.ts +36 -0
  151. package/src/editor-preview.main.ts +1782 -0
  152. package/src/events/handleAction.ts +387 -0
  153. package/src/page.main.ts +489 -0
  154. package/src/signal/signal.ts +74 -0
  155. package/src/styles/style.ts +254 -0
  156. package/src/types.d.ts +93 -0
  157. package/src/utils/BatchQueue.ts +24 -0
  158. package/src/utils/createFormulaCache.ts +96 -0
  159. package/src/utils/findNearestLine.test.ts +65 -0
  160. package/src/utils/findNearestLine.ts +92 -0
  161. package/src/utils/getDragData.ts +11 -0
  162. package/src/utils/getElementTagName.ts +14 -0
  163. package/src/utils/nodes.ts +125 -0
  164. package/src/utils/omitStyle.ts +19 -0
  165. package/src/utils/rectHasPoint.ts +5 -0
  166. package/src/utils/setAttribute.ts +56 -0
  167. package/src/utils/tryStartViewTransition.ts +32 -0
  168. 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
+ }