@nordcraft/runtime 1.0.60 → 1.0.62

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/src/page.main.ts CHANGED
@@ -26,6 +26,7 @@ import { createAPI } from './api/createAPIv2'
26
26
  import { renderComponent } from './components/renderComponent'
27
27
  import { isContextProvider } from './context/isContextProvider'
28
28
  import { initLogState, registerComponentToLogState } from './debug/logState'
29
+ import type { Signal } from './signal/signal'
29
30
  import { signal } from './signal/signal'
30
31
  import type { ComponentContext, LocationSignal } from './types'
31
32
 
@@ -175,6 +176,167 @@ export const createRoot = (domNode: HTMLElement) => {
175
176
  ]),
176
177
  })
177
178
 
179
+ registerComponentToLogState(component, dataSignal)
180
+
181
+ routeSignal.subscribe((route) =>
182
+ dataSignal.update((data) => ({
183
+ ...data,
184
+ 'URL parameters': route as Record<string, string>,
185
+ Attributes: route,
186
+ })),
187
+ )
188
+
189
+ // Call the abort signal if the component's datasignal is destroyed (component unmounted) to cancel any pending requests
190
+ const abortController = new AbortController()
191
+ dataSignal.subscribe(() => {}, {
192
+ destroy: () =>
193
+ abortController.abort(`Component ${component.name} unmounted`),
194
+ })
195
+
196
+ const ctx: ComponentContext = {
197
+ component,
198
+ components: window.toddle.components,
199
+ root: document,
200
+ isRootComponent: true,
201
+ dataSignal,
202
+ abortSignal: abortController.signal,
203
+ children: {},
204
+ formulaCache: {},
205
+ providers: {},
206
+ apis: {},
207
+ toddle: window.toddle,
208
+ triggerEvent: (event: string, data: unknown) =>
209
+ // eslint-disable-next-line no-console
210
+ console.info('EVENT FIRED', event, data),
211
+ package: undefined,
212
+ env,
213
+ }
214
+
215
+ // Note: this function must run procedurally to ensure apis (which are in correct order) can reference each other
216
+ sortApiObjects(Object.entries(component.apis)).forEach(([name, api]) => {
217
+ if (isLegacyApi(api)) {
218
+ ctx.apis[name] = createLegacyAPI(api, ctx)
219
+ } else {
220
+ ctx.apis[name] = createAPI({
221
+ apiRequest: api,
222
+ ctx,
223
+ componentData: dataSignal.get(),
224
+ })
225
+ }
226
+ })
227
+ // Trigger actions for all APIs after all of them are created.
228
+ Object.values(ctx.apis)
229
+ .filter(isContextApiV2)
230
+ .forEach((api) => {
231
+ api.triggerActions(dataSignal.get())
232
+ })
233
+
234
+ let providers = ctx.providers
235
+ if (isContextProvider(component)) {
236
+ // Subscribe to exposed formulas and update the component's data signal
237
+ const formulaDataSignals = Object.fromEntries(
238
+ Object.entries(component.formulas ?? {})
239
+ .filter(([, formula]) => formula.exposeInContext)
240
+ .map(([name, formula]) => [
241
+ name,
242
+ dataSignal.map((data) =>
243
+ applyFormula(formula.formula, {
244
+ data,
245
+ component,
246
+ formulaCache: ctx.formulaCache,
247
+ root: ctx.root,
248
+ package: ctx.package,
249
+ toddle: window.toddle,
250
+ env,
251
+ }),
252
+ ),
253
+ ]),
254
+ )
255
+
256
+ providers = {
257
+ ...providers,
258
+ [component.name]: {
259
+ component,
260
+ formulaDataSignals,
261
+ ctx,
262
+ },
263
+ }
264
+ }
265
+
266
+ // We can only setup meta updates after the dataSignal has been initiated with API data etc.
267
+ setupMetaUpdates(component, dataSignal)
268
+
269
+ const elements = renderComponent({
270
+ ...ctx,
271
+ providers,
272
+ path: '0',
273
+ package: undefined,
274
+ onEvent: ctx.triggerEvent,
275
+ parentElement: domNode,
276
+ instance: {},
277
+ })
278
+ domNode.innerText = ''
279
+ elements.forEach((elem) => {
280
+ domNode.appendChild(elem)
281
+ })
282
+ window.__toddle.isPageLoaded = true
283
+ }
284
+
285
+ function parseUrl(component: Component) {
286
+ const path = window.location.pathname.split('/').slice(1)
287
+ let params: Record<string, string | null> = {}
288
+ if (component.route) {
289
+ component.route.path.forEach((segment, i) => {
290
+ if (segment.type === 'param') {
291
+ if (isDefined(path[i]) && path[i] !== '') {
292
+ params[segment.name] = decodeURIComponent(path[i])
293
+ } else {
294
+ params[segment.name] = null
295
+ }
296
+ } else {
297
+ params[segment.name] = segment.name
298
+ }
299
+ })
300
+ } else {
301
+ const urlPattern = match<Record<string, string>>(component.page ?? '', {
302
+ decode: decodeURIComponent,
303
+ })
304
+ const res = urlPattern(window.location.pathname) || {
305
+ params: {},
306
+ }
307
+ params = res.params
308
+ }
309
+
310
+ const [hash] = window.location.hash.split('?')
311
+ const query = parseQuery(window.location.search) as Record<string, string>
312
+ // Explicitly set all query params to null by default
313
+ // to avoid undefined values in the runtime
314
+ const defaultQueryParams = Object.keys(component.route?.query ?? {}).reduce<
315
+ Record<string, null>
316
+ >((params, key) => ({ ...params, [key]: null }), {})
317
+ return {
318
+ params,
319
+ hash: hash?.slice(1),
320
+ query: { ...defaultQueryParams, ...query },
321
+ }
322
+ }
323
+
324
+ const parseQuery = (queryString: string) =>
325
+ Object.fromEntries(
326
+ queryString
327
+ .replace('?', '')
328
+ .split('&')
329
+ .filter((pair) => pair !== '')
330
+ .map((pair: string) => {
331
+ return pair.split('=').map(decodeURIComponent)
332
+ }),
333
+ )
334
+
335
+ const setupMetaUpdates = (
336
+ component: Component,
337
+ dataSignal: Signal<ComponentData>,
338
+ ) => {
339
+ // Handle dynamic updates of the document language
178
340
  const langFormula = component.route?.info?.language?.formula
179
341
  const dynamicLang = langFormula && langFormula.type !== 'value'
180
342
  if (dynamicLang) {
@@ -358,156 +520,4 @@ export const createRoot = (domNode: HTMLElement) => {
358
520
  })
359
521
  }
360
522
  }
361
-
362
- registerComponentToLogState(component, dataSignal)
363
-
364
- routeSignal.subscribe((route) =>
365
- dataSignal.update((data) => ({
366
- ...data,
367
- 'URL parameters': route as Record<string, string>,
368
- Attributes: route,
369
- })),
370
- )
371
-
372
- // Call the abort signal if the component's datasignal is destroyed (component unmounted) to cancel any pending requests
373
- const abortController = new AbortController()
374
- dataSignal.subscribe(() => {}, {
375
- destroy: () =>
376
- abortController.abort(`Component ${component.name} unmounted`),
377
- })
378
-
379
- const ctx: ComponentContext = {
380
- component,
381
- components: window.toddle.components,
382
- root: document,
383
- isRootComponent: true,
384
- dataSignal,
385
- abortSignal: abortController.signal,
386
- children: {},
387
- formulaCache: {},
388
- providers: {},
389
- apis: {},
390
- toddle: window.toddle,
391
- triggerEvent: (event: string, data: unknown) =>
392
- // eslint-disable-next-line no-console
393
- console.info('EVENT FIRED', event, data),
394
- package: undefined,
395
- env,
396
- }
397
-
398
- // Note: this function must run procedurally to ensure apis (which are in correct order) can reference each other
399
- sortApiObjects(Object.entries(component.apis)).forEach(([name, api]) => {
400
- if (isLegacyApi(api)) {
401
- ctx.apis[name] = createLegacyAPI(api, ctx)
402
- } else {
403
- ctx.apis[name] = createAPI({
404
- apiRequest: api,
405
- ctx,
406
- componentData: dataSignal.get(),
407
- })
408
- }
409
- })
410
- // Trigger actions for all APIs after all of them are created.
411
- Object.values(ctx.apis)
412
- .filter(isContextApiV2)
413
- .forEach((api) => {
414
- api.triggerActions(dataSignal.get())
415
- })
416
-
417
- let providers = ctx.providers
418
- if (isContextProvider(component)) {
419
- // Subscribe to exposed formulas and update the component's data signal
420
- const formulaDataSignals = Object.fromEntries(
421
- Object.entries(component.formulas ?? {})
422
- .filter(([, formula]) => formula.exposeInContext)
423
- .map(([name, formula]) => [
424
- name,
425
- dataSignal.map((data) =>
426
- applyFormula(formula.formula, {
427
- data,
428
- component,
429
- formulaCache: ctx.formulaCache,
430
- root: ctx.root,
431
- package: ctx.package,
432
- toddle: window.toddle,
433
- env,
434
- }),
435
- ),
436
- ]),
437
- )
438
-
439
- providers = {
440
- ...providers,
441
- [component.name]: {
442
- component,
443
- formulaDataSignals,
444
- ctx,
445
- },
446
- }
447
- }
448
-
449
- const elements = renderComponent({
450
- ...ctx,
451
- providers,
452
- path: '0',
453
- package: undefined,
454
- onEvent: ctx.triggerEvent,
455
- parentElement: domNode,
456
- instance: {},
457
- })
458
- domNode.innerText = ''
459
- elements.forEach((elem) => {
460
- domNode.appendChild(elem)
461
- })
462
- window.__toddle.isPageLoaded = true
463
- }
464
-
465
- function parseUrl(component: Component) {
466
- const path = window.location.pathname.split('/').slice(1)
467
- let params: Record<string, string | null> = {}
468
- if (component.route) {
469
- component.route.path.forEach((segment, i) => {
470
- if (segment.type === 'param') {
471
- if (isDefined(path[i]) && path[i] !== '') {
472
- params[segment.name] = decodeURIComponent(path[i])
473
- } else {
474
- params[segment.name] = null
475
- }
476
- } else {
477
- params[segment.name] = segment.name
478
- }
479
- })
480
- } else {
481
- const urlPattern = match<Record<string, string>>(component.page ?? '', {
482
- decode: decodeURIComponent,
483
- })
484
- const res = urlPattern(window.location.pathname) || {
485
- params: {},
486
- }
487
- params = res.params
488
- }
489
-
490
- const [hash] = window.location.hash.split('?')
491
- const query = parseQuery(window.location.search) as Record<string, string>
492
- // Explicitly set all query params to null by default
493
- // to avoid undefined values in the runtime
494
- const defaultQueryParams = Object.keys(component.route?.query ?? {}).reduce<
495
- Record<string, null>
496
- >((params, key) => ({ ...params, [key]: null }), {})
497
- return {
498
- params,
499
- hash: hash?.slice(1),
500
- query: { ...defaultQueryParams, ...query },
501
- }
502
523
  }
503
-
504
- const parseQuery = (queryString: string) =>
505
- Object.fromEntries(
506
- queryString
507
- .replace('?', '')
508
- .split('&')
509
- .filter((pair) => pair !== '')
510
- .map((pair: string) => {
511
- return pair.split('=').map(decodeURIComponent)
512
- }),
513
- )