@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,489 @@
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 type { PluginFormula } from '@nordcraft/core/dist/formula/formulaTypes'
9
+ import type {
10
+ ActionHandler,
11
+ ArgumentInputDataFunction,
12
+ FormulaHandler,
13
+ FormulaHandlerV2,
14
+ PluginActionV2,
15
+ RequireFields,
16
+ Toddle,
17
+ } from '@nordcraft/core/dist/types'
18
+ import { mapObject } from '@nordcraft/core/dist/utils/collections'
19
+ import { isDefined } from '@nordcraft/core/dist/utils/util'
20
+ import * as libActions from '@nordcraft/std-lib/dist/actions'
21
+ import * as libFormulas from '@nordcraft/std-lib/dist/formulas'
22
+ import fastDeepEqual from 'fast-deep-equal'
23
+ import { match } from 'path-to-regexp'
24
+ import { createLegacyAPI } from './api/createAPI'
25
+ import { createAPI } from './api/createAPIv2'
26
+ import { renderComponent } from './components/renderComponent'
27
+ import { isContextProvider } from './context/isContextProvider'
28
+ import { initLogState, registerComponentToLogState } from './debug/logState'
29
+ import { signal } from './signal/signal'
30
+ import type { ComponentContext, ContextApi, LocationSignal } from './types'
31
+
32
+ initLogState()
33
+
34
+ let env: ToddleEnv
35
+
36
+ export const initGlobalObject = (code?: {
37
+ formulas: Record<string, Record<string, PluginFormula<FormulaHandlerV2>>>
38
+ actions: Record<string, Record<string, PluginActionV2>>
39
+ }) => {
40
+ const component = window.__toddle.component
41
+ const { params, hash, query } = parseUrl(component)
42
+ env = {
43
+ isServer: false,
44
+ branchName: window.__toddle.branch,
45
+ request: undefined,
46
+ runtime: 'page',
47
+ logErrors: true,
48
+ }
49
+ window.toddle = (() => {
50
+ const legacyActions: Record<string, ActionHandler> = {}
51
+ const legacyFormulas: Record<string, FormulaHandler> = {}
52
+ const argumentInputDataList: Record<string, ArgumentInputDataFunction> = {}
53
+ const toddle: Toddle<LocationSignal, never> = {
54
+ isEqual: fastDeepEqual,
55
+ errors: [],
56
+ project: window.__toddle.project,
57
+ branch: window.__toddle.branch,
58
+ commit: window.__toddle.commit,
59
+ components: window.__toddle.components,
60
+ formulas: code?.formulas ?? {},
61
+ actions: code?.actions ?? {},
62
+ registerAction: (name, handler) => {
63
+ if (legacyActions[name]) {
64
+ // eslint-disable-next-line no-console
65
+ console.error('There already exists an action with the name ', name)
66
+ return
67
+ }
68
+ legacyActions[name] = handler
69
+ },
70
+ getAction: (name) => legacyActions[name],
71
+ registerFormula: (name, handler, getArgumentInputData) => {
72
+ if (legacyFormulas[name]) {
73
+ // eslint-disable-next-line no-console
74
+ console.error('There already exists a formula with the name ', name)
75
+ return
76
+ }
77
+ legacyFormulas[name] = handler
78
+ if (getArgumentInputData) {
79
+ argumentInputDataList[name] = getArgumentInputData
80
+ }
81
+ },
82
+ getFormula: (name) => legacyFormulas[name],
83
+ getCustomAction: (name, packageName) => {
84
+ return (
85
+ toddle.actions[packageName ?? window.__toddle.project]?.[name] ??
86
+ toddle.actions[window.__toddle.project]?.[name]
87
+ )
88
+ },
89
+ getCustomFormula: (name, packageName) => {
90
+ return (
91
+ toddle.formulas[packageName ?? window.__toddle.project]?.[name] ??
92
+ toddle.formulas[window.__toddle.project]?.[name]
93
+ )
94
+ },
95
+ // eslint-disable-next-line max-params
96
+ getArgumentInputData: (formulaName, args, argIndex, data) =>
97
+ argumentInputDataList[formulaName]?.(args, argIndex, data) || data,
98
+ data: {},
99
+ locationSignal: signal<any>({
100
+ route: component.route,
101
+ page: component.page as string,
102
+ path: window.location.pathname,
103
+ params,
104
+ query,
105
+ hash,
106
+ }),
107
+ eventLog: [],
108
+ pageState: window.__toddle.pageState,
109
+ env,
110
+ }
111
+ return toddle
112
+ })()
113
+
114
+ // load default formulas and actions
115
+ Object.entries(libFormulas).forEach(([name, module]) =>
116
+ window.toddle.registerFormula(
117
+ '@toddle/' + name,
118
+ module.default as FormulaHandler,
119
+ 'getArgumentInputData' in module
120
+ ? module.getArgumentInputData
121
+ : undefined,
122
+ ),
123
+ )
124
+ Object.entries(libActions).forEach(([name, module]) =>
125
+ window.toddle.registerAction('@toddle/' + name, module.default),
126
+ )
127
+ }
128
+
129
+ export const createRoot = (domNode: HTMLElement) => {
130
+ const component = window.__toddle.component
131
+ if (!domNode) {
132
+ throw new Error('Cant find root domNode')
133
+ }
134
+
135
+ if (!window.toddle.components) {
136
+ throw new Error('Missing components')
137
+ }
138
+
139
+ window.addEventListener('popstate', () => {
140
+ if (!component) {
141
+ return
142
+ }
143
+ const { params, hash, query } = parseUrl(component)
144
+ window.toddle.locationSignal.update(() => {
145
+ return {
146
+ route: component?.route,
147
+ page: component!.page as string,
148
+ path: window.location.pathname,
149
+ params,
150
+ query,
151
+ hash,
152
+ }
153
+ })
154
+ })
155
+
156
+ const routeSignal = window.toddle.locationSignal.map(({ query, params }) => {
157
+ return { ...query, ...params }
158
+ })
159
+
160
+ const dataSignal = signal<ComponentData>({
161
+ ...window.toddle.pageState,
162
+ // Re-initialize variables since some of them might rely on client-side
163
+ // state (e.g. localStorage, sensors etc.)
164
+ Variables: mapObject(component.variables, ([name, variable]) => [
165
+ name,
166
+ applyFormula(variable.initialValue, {
167
+ data: window.toddle.pageState,
168
+ component,
169
+ formulaCache: {},
170
+ root: document,
171
+ package: undefined,
172
+ toddle: window.toddle,
173
+ env,
174
+ }),
175
+ ]),
176
+ })
177
+
178
+ // Handle dynamic updates of <head> elements (title, og:image etc.)
179
+ const titleFormula = component.route?.info?.title?.formula
180
+ const dynamicTitle = titleFormula && titleFormula.type !== 'value'
181
+ if (dynamicTitle) {
182
+ dataSignal
183
+ .map<string | null>(() =>
184
+ component
185
+ ? applyFormula(titleFormula, {
186
+ data: dataSignal.get(),
187
+ component,
188
+ root: document,
189
+ package: undefined,
190
+ toddle: window.toddle,
191
+ env,
192
+ })
193
+ : null,
194
+ )
195
+ .subscribe((newTitle) => {
196
+ if (isDefined(newTitle) && document.title !== newTitle) {
197
+ document.title = newTitle
198
+ }
199
+ })
200
+ }
201
+
202
+ const descriptionFormula = component.route?.info?.description?.formula
203
+ const meta = component.route?.info?.meta
204
+ const dynamicDescription =
205
+ descriptionFormula && descriptionFormula.type !== 'value'
206
+ const dynamicMetaFormulas = Object.values(meta ?? {}).some((r) =>
207
+ Object.values(
208
+ r.attrs ?? {}, // fallback to make sure we don't crash on legacy values
209
+ ).some((a) => a.type !== 'value'),
210
+ )
211
+ if (dynamicDescription || dynamicMetaFormulas) {
212
+ const findMetaElement = (name: string) =>
213
+ [...document.getElementsByTagName('meta')].find(
214
+ (el) => el.name === name || el.getAttribute('property') === name,
215
+ ) ?? null
216
+
217
+ const updateMetaElement = (
218
+ entry: { tag: string; attrs: Record<string, string> },
219
+ id?: string,
220
+ ) => {
221
+ let existingElement: HTMLElement | null = null
222
+ if (isDefined(id)) {
223
+ existingElement = document.querySelector(`[data-toddle-id="${id}"]`)
224
+ } else {
225
+ const identifier = Object.entries(entry.attrs ?? {}).find(([key]) =>
226
+ ['property', 'name'].includes(key.toLowerCase()),
227
+ )?.[1]
228
+ if (isDefined(identifier)) {
229
+ existingElement = findMetaElement(identifier)
230
+ }
231
+ }
232
+ if (!existingElement) {
233
+ // If the element didn't already exist, create it
234
+ existingElement = document.createElement(entry.tag)
235
+ if (isDefined(id)) {
236
+ existingElement.setAttribute('data-toddle-id', id)
237
+ }
238
+ document.getElementsByTagName('head')[0].appendChild(existingElement)
239
+ }
240
+ // Apply all attributes to the element
241
+ Object.entries(entry.attrs ?? {}).forEach(([key, value]) => {
242
+ if (!component) {
243
+ return
244
+ }
245
+ existingElement!.setAttribute(key, value)
246
+ })
247
+ }
248
+ if (dynamicDescription) {
249
+ dataSignal
250
+ .map<string | null>((data) =>
251
+ component
252
+ ? applyFormula(descriptionFormula, {
253
+ data,
254
+ component,
255
+ root: document,
256
+ package: undefined,
257
+ toddle: window.toddle,
258
+ env,
259
+ })
260
+ : null,
261
+ )
262
+ .subscribe((newDescription) => {
263
+ if (isDefined(newDescription)) {
264
+ let descriptionElement = document
265
+ .getElementsByTagName('meta')
266
+ .namedItem('description')
267
+ if (!descriptionElement) {
268
+ descriptionElement = document.createElement('meta')
269
+ descriptionElement.name = 'description'
270
+ }
271
+ descriptionElement.content = newDescription
272
+ document
273
+ .getElementsByTagName('head')[0]
274
+ .appendChild(descriptionElement)
275
+ if (
276
+ meta &&
277
+ !Object.values(meta).some((m) =>
278
+ Object.entries(m.attrs ?? {}).some(
279
+ ([k, value]) =>
280
+ k.toLowerCase() === 'property' &&
281
+ value.type === 'value' &&
282
+ typeof value.value === 'string' &&
283
+ value.value.toLowerCase() === 'og:description',
284
+ ),
285
+ )
286
+ ) {
287
+ // If there is no formula for the og:description meta tag,
288
+ // add it with the same value as the description meta tag
289
+ // this mimics the behavior from our SSR
290
+ updateMetaElement({
291
+ tag: 'meta',
292
+ attrs: {
293
+ property: 'og:description',
294
+ content: newDescription,
295
+ },
296
+ })
297
+ }
298
+ }
299
+ })
300
+ }
301
+ if (dynamicMetaFormulas) {
302
+ Object.entries(meta ?? {})
303
+ // Filter out meta tags that have no dynamic formulas
304
+ .filter(([_, entry]) =>
305
+ // fallback to make sure we don't crash on legacy values.
306
+ Object.values(entry.attrs ?? {}).some((a) => a.type !== 'value'),
307
+ )
308
+ .forEach(([id, entry]) => {
309
+ dataSignal
310
+ .map<Record<string, string>>((data) => {
311
+ // Return the new values for all attributes (we assume they're strings)
312
+ const values = Object.entries(entry.attrs ?? {}).reduce(
313
+ (agg, [key, formula]) =>
314
+ component
315
+ ? {
316
+ ...agg,
317
+ [key]: applyFormula(formula, {
318
+ data,
319
+ component,
320
+ root: document,
321
+ package: undefined,
322
+ toddle: window.toddle,
323
+ env,
324
+ }),
325
+ }
326
+ : agg,
327
+ {},
328
+ )
329
+ return values
330
+ })
331
+ .subscribe((attrs) =>
332
+ // Update the meta tags with the new values
333
+ updateMetaElement({ tag: entry.tag, attrs }, id),
334
+ )
335
+ })
336
+ }
337
+ }
338
+
339
+ registerComponentToLogState(component, dataSignal)
340
+
341
+ routeSignal.subscribe((route) =>
342
+ dataSignal.update((data) => ({
343
+ ...data,
344
+ 'URL parameters': route as Record<string, string>,
345
+ Attributes: route,
346
+ })),
347
+ )
348
+
349
+ // Call the abort signal if the component's datasignal is destroyed (component unmounted) to cancel any pending requests
350
+ const abortController = new AbortController()
351
+ dataSignal.subscribe(() => {}, {
352
+ destroy: () =>
353
+ abortController.abort(`Component ${component.name} unmounted`),
354
+ })
355
+
356
+ const ctx: ComponentContext = {
357
+ component,
358
+ components: window.toddle.components,
359
+ root: document,
360
+ isRootComponent: true,
361
+ dataSignal,
362
+ abortSignal: abortController.signal,
363
+ children: {},
364
+ formulaCache: {},
365
+ providers: {},
366
+ apis: {},
367
+ toddle: window.toddle,
368
+ triggerEvent: (event: string, data: unknown) =>
369
+ // eslint-disable-next-line no-console
370
+ console.info('EVENT FIRED', event, data),
371
+ package: undefined,
372
+ env,
373
+ }
374
+
375
+ // Note: this function must run procedurally to ensure apis (which are in correct order) can reference each other
376
+ sortApiObjects(Object.entries(component.apis)).forEach(([name, api]) => {
377
+ if (isLegacyApi(api)) {
378
+ ctx.apis[name] = createLegacyAPI(api, ctx)
379
+ } else {
380
+ ctx.apis[name] = createAPI(api, ctx)
381
+ }
382
+ })
383
+ // Trigger actions for all APIs after all of them are created.
384
+ Object.values(ctx.apis)
385
+ .filter(
386
+ (api): api is RequireFields<ContextApi, 'triggerActions'> =>
387
+ api.triggerActions !== undefined,
388
+ )
389
+ .forEach((api) => {
390
+ api.triggerActions()
391
+ })
392
+
393
+ let providers = ctx.providers
394
+ if (isContextProvider(component)) {
395
+ // Subscribe to exposed formulas and update the component's data signal
396
+ const formulaDataSignals = Object.fromEntries(
397
+ Object.entries(component.formulas ?? {})
398
+ .filter(([, formula]) => formula.exposeInContext)
399
+ .map(([name, formula]) => [
400
+ name,
401
+ dataSignal.map((data) =>
402
+ applyFormula(formula.formula, {
403
+ data,
404
+ component,
405
+ formulaCache: ctx.formulaCache,
406
+ root: ctx.root,
407
+ package: ctx.package,
408
+ toddle: window.toddle,
409
+ env,
410
+ }),
411
+ ),
412
+ ]),
413
+ )
414
+
415
+ providers = {
416
+ ...providers,
417
+ [component.name]: {
418
+ component,
419
+ formulaDataSignals,
420
+ ctx,
421
+ },
422
+ }
423
+ }
424
+
425
+ const elements = renderComponent({
426
+ ...ctx,
427
+ providers,
428
+ path: '0',
429
+ package: undefined,
430
+ onEvent: ctx.triggerEvent,
431
+ parentElement: domNode,
432
+ instance: {},
433
+ })
434
+ domNode.innerText = ''
435
+ elements.forEach((elem) => {
436
+ domNode.appendChild(elem)
437
+ })
438
+ window.__toddle.isPageLoaded = true
439
+ }
440
+
441
+ function parseUrl(component: Component) {
442
+ const path = window.location.pathname.split('/').slice(1)
443
+ let params: Record<string, string | null> = {}
444
+ if (component.route) {
445
+ component.route.path.forEach((segment, i) => {
446
+ if (segment.type === 'param') {
447
+ if (isDefined(path[i]) && path[i] !== '') {
448
+ params[segment.name] = decodeURIComponent(path[i])
449
+ } else {
450
+ params[segment.name] = null
451
+ }
452
+ } else {
453
+ params[segment.name] = segment.name
454
+ }
455
+ })
456
+ } else {
457
+ const urlPattern = match<Record<string, string>>(component.page ?? '', {
458
+ decode: decodeURIComponent,
459
+ })
460
+ const res = urlPattern(window.location.pathname) || {
461
+ params: {},
462
+ }
463
+ params = res.params
464
+ }
465
+
466
+ const [hash] = window.location.hash.split('?')
467
+ const query = parseQuery(window.location.search) as Record<string, string>
468
+ // Explicitly set all query params to null by default
469
+ // to avoid undefined values in the runtime
470
+ const defaultQueryParams = Object.keys(component.route?.query ?? {}).reduce<
471
+ Record<string, null>
472
+ >((params, key) => ({ ...params, [key]: null }), {})
473
+ return {
474
+ params,
475
+ hash: hash?.slice(1),
476
+ query: { ...defaultQueryParams, ...query },
477
+ }
478
+ }
479
+
480
+ const parseQuery = (queryString: string) =>
481
+ Object.fromEntries(
482
+ queryString
483
+ .replace('?', '')
484
+ .split('&')
485
+ .filter((pair) => pair !== '')
486
+ .map((pair: string) => {
487
+ return pair.split('=').map(decodeURIComponent)
488
+ }),
489
+ )
@@ -0,0 +1,74 @@
1
+ import deepEqual from 'fast-deep-equal'
2
+
3
+ export class Signal<T> {
4
+ value: T
5
+ subscribers: Set<{
6
+ notify: (value: T) => void
7
+ destroy?: () => void
8
+ }>
9
+ subscriptions: Array<() => void>
10
+
11
+ constructor(value: T) {
12
+ this.value = value
13
+ this.subscribers = new Set()
14
+ this.subscriptions = []
15
+ }
16
+ get() {
17
+ return this.value
18
+ }
19
+ set(value: T) {
20
+ // Short circuit and skip expensive `deepEqual` if there are not currently any subscribers
21
+ if (this.subscribers.size === 0) {
22
+ this.value = value
23
+ return
24
+ }
25
+
26
+ if (deepEqual(value, this.value) === false) {
27
+ this.value = value
28
+ this.subscribers.forEach(({ notify }) => notify(this.value))
29
+ }
30
+ }
31
+
32
+ update(f: (current: T) => T) {
33
+ this.set(f(this.value))
34
+ }
35
+ subscribe(notify: (value: T) => void, config?: { destroy?: () => void }) {
36
+ const subscriber = { notify, destroy: config?.destroy }
37
+ this.subscribers.add(subscriber)
38
+ notify(this.value)
39
+ return () => {
40
+ this.subscribers.delete(subscriber)
41
+ }
42
+ }
43
+ destroy() {
44
+ this.subscribers.forEach(({ destroy }) => {
45
+ destroy?.()
46
+ })
47
+ this.subscribers.clear()
48
+ this.subscriptions?.forEach((f) => f())
49
+ }
50
+ cleanSubscribers() {
51
+ this.subscribers.forEach(({ destroy }) => {
52
+ destroy?.()
53
+ })
54
+ this.subscribers.clear()
55
+ }
56
+ map<T2>(f: (value: T) => T2): Signal<T2> {
57
+ const signal2 = signal(f(this.value))
58
+ signal2.subscriptions.push(
59
+ this.subscribe((value) => signal2.set(f(value)), {
60
+ destroy: () => signal2.destroy(),
61
+ }),
62
+ )
63
+ return signal2
64
+ }
65
+ }
66
+
67
+ export function signal<T>(value: T) {
68
+ return new Signal(value)
69
+ }
70
+
71
+ if (typeof window !== 'undefined') {
72
+ ;(window as any).signal = signal
73
+ ;(window as any).deepEqual = deepEqual
74
+ }