@lightningjs/blits 2.2.0 → 2.4.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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## v2.4.0
4
+
5
+ _1 june 2026_
6
+
7
+ - Improved correct registration of reactivity in computed props
8
+ - Added named router views to enable multiple nested router views
9
+
10
+ ## v2.3.0
11
+
12
+ _20 may 2026_
13
+
14
+ - Refactored router functionality
15
+ - Replaced `window` reference with `self` reference in announcer
16
+
3
17
  ## v2.2.0
4
18
 
5
19
  _11 may 2026_
package/index.d.ts CHANGED
@@ -329,7 +329,14 @@ declare module '@lightningjs/blits' {
329
329
  *
330
330
  * @param {string}
331
331
  */
332
- to(location: string, data?: RouteData, options?: RouteOptions): void;
332
+ to(location: string, data?: RouteData, options?: RouteOptions): boolean;
333
+
334
+ /**
335
+ * Get a router scoped to a named RouterView
336
+ *
337
+ * @param {string}
338
+ */
339
+ get(name: string): Router;
333
340
 
334
341
  /**
335
342
  * Navigate to the previous location
@@ -707,6 +714,10 @@ declare module '@lightningjs/blits' {
707
714
  * Watchers for changes to state variables, props or computed properties
708
715
  */
709
716
  watch?: W & ComponentContext<P, S, M, C>
717
+ /**
718
+ * Router Configuration
719
+ */
720
+ router?: RouterConfig<P, S, M, C>
710
721
  }
711
722
 
712
723
  export interface RouterHooks {
@@ -759,7 +770,7 @@ declare module '@lightningjs/blits' {
759
770
 
760
771
  export type ApplicationConfig<P extends Props, S, M, C, W> = Omit<
761
772
  ComponentConfig<P, S, M, C, W>,
762
- 'hooks' | 'methods' | 'input' | 'computed' | 'watch' | 'state'
773
+ 'hooks' | 'methods' | 'input' | 'computed' | 'watch' | 'state' | 'router' | 'routes'
763
774
  > & {
764
775
  hooks?: Hooks & ApplicationContext<P, S, M, C>
765
776
  methods?: M & ApplicationContext<P, S, M, C>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightningjs/blits",
3
- "version": "2.2.0",
3
+ "version": "2.4.0",
4
4
  "description": "Blits: The Lightning 3 App Development Framework",
5
5
  "bin": "bin/index.js",
6
6
  "exports": {
@@ -27,7 +27,7 @@ let debounce = null
27
27
 
28
28
  // Global default utterance options
29
29
  let globalDefaultOptions = {
30
- enableUtteranceKeepAlive: !/android/i.test((window.navigator || {}).userAgent || ''),
30
+ enableUtteranceKeepAlive: !/android/i.test((self.navigator || {}).userAgent || ''),
31
31
  }
32
32
 
33
33
  const noopAnnouncement = {
@@ -17,7 +17,7 @@
17
17
 
18
18
  import { Log } from '../lib/log.js'
19
19
 
20
- const syn = window.speechSynthesis
20
+ const syn = self.speechSynthesis
21
21
 
22
22
  const utterances = new Map() // id -> { utterance, timer, ignoreResume }
23
23
 
@@ -16,31 +16,145 @@
16
16
  */
17
17
 
18
18
  import symbols from '../../lib/symbols.js'
19
- import { to, currentRoute, back, state } from '../../router/router.js'
19
+ import {
20
+ back,
21
+ currentRoute,
22
+ getRegisteredRouterView,
23
+ getSingleRegisteredRouterView,
24
+ state,
25
+ toRouterView,
26
+ } from '../../router/router.js'
27
+
28
+ const routerCache = new WeakMap()
29
+
30
+ const isRouterView = (component, routerViewName = '') => {
31
+ if (component === undefined || component === null) return false
32
+ return (
33
+ Array.isArray(component.history) && (routerViewName === '' || component.name === routerViewName)
34
+ )
35
+ }
36
+
37
+ const getRouterViewInChildren = (component, routerViewName = '') => {
38
+ if (component === undefined || component === null) return null
39
+
40
+ const children = component[symbols.children]
41
+ if (Array.isArray(children) === false) return null
42
+
43
+ for (let i = 0; i < children.length; i++) {
44
+ const child = children[i]
45
+ if (isRouterView(child, routerViewName)) return child
46
+
47
+ const nestedRouterView = getRouterViewInChildren(child, routerViewName)
48
+ if (nestedRouterView !== null) return nestedRouterView
49
+ }
50
+
51
+ return null
52
+ }
53
+
54
+ const getRouterView = (component, routerViewName = '') => {
55
+ let current = component
56
+ while (current !== undefined && current !== null) {
57
+ if (isRouterView(current, routerViewName)) {
58
+ return current
59
+ }
60
+ current = current[symbols.parent]
61
+ }
62
+ return getRouterViewInChildren(component, routerViewName)
63
+ }
64
+
65
+ const resolveRouterView = (component, routerViewName) => {
66
+ if (routerViewName !== undefined) {
67
+ const registeredRouterView = getRegisteredRouterView(routerViewName)
68
+ if (registeredRouterView !== null) return registeredRouterView
69
+
70
+ return getRouterView(component, routerViewName)
71
+ }
72
+
73
+ const singleRouterView = getSingleRegisteredRouterView()
74
+ if (singleRouterView !== null) return singleRouterView
75
+
76
+ return getRouterView(component)
77
+ }
78
+
79
+ const getRoutes = (component, routerViewName) => {
80
+ const routerView = resolveRouterView(component, routerViewName)
81
+ if (
82
+ routerView !== null &&
83
+ routerView[symbols.parent] !== undefined &&
84
+ routerView[symbols.parent][symbols.routes] !== undefined
85
+ ) {
86
+ return routerView[symbols.parent][symbols.routes]
87
+ }
88
+
89
+ if (component[symbols.routes] !== undefined) return component[symbols.routes]
90
+
91
+ if (
92
+ component[symbols.parent] !== undefined &&
93
+ component[symbols.parent][symbols.routes] !== undefined
94
+ ) {
95
+ return component[symbols.parent][symbols.routes]
96
+ }
97
+
98
+ return undefined
99
+ }
100
+
101
+ const getRouterFromCache = (component, routerViewName) => {
102
+ let routers = routerCache.get(component)
103
+ if (routers === undefined) {
104
+ routers = new Map()
105
+ routerCache.set(component, routers)
106
+ }
107
+
108
+ const cacheKey = routerViewName === undefined ? '' : routerViewName
109
+ const router = routers.get(cacheKey)
110
+ if (router !== undefined) return router
111
+
112
+ const newRouter = createRouter(component, routerViewName)
113
+ routers.set(cacheKey, newRouter)
114
+ return newRouter
115
+ }
116
+
117
+ const createRouter = (component, routerViewName) => {
118
+ return {
119
+ to(path, data = {}, options = {}) {
120
+ const routerView = resolveRouterView(component, routerViewName)
121
+ if (routerView === null) return false
122
+
123
+ toRouterView(routerView, path, data, options)
124
+ return true
125
+ },
126
+ back() {
127
+ const routerView = resolveRouterView(component, routerViewName)
128
+ return routerView === null ? false : back.call(routerView)
129
+ },
130
+ get(name) {
131
+ return getRouterFromCache(component, name)
132
+ },
133
+ get backNavigation() {
134
+ return state.backNavigation !== false
135
+ },
136
+ set backNavigation(enabled) {
137
+ state.backNavigation = enabled !== false
138
+ },
139
+ get currentRoute() {
140
+ const routerView = resolveRouterView(component, routerViewName)
141
+ return routerView === null ? currentRoute : routerView.currentRoute
142
+ },
143
+ get routes() {
144
+ return getRoutes(component, routerViewName)
145
+ },
146
+ get navigating() {
147
+ return state.navigating
148
+ },
149
+ state,
150
+ }
151
+ }
20
152
 
21
153
  export default {
22
154
  $router: {
23
- value: {
24
- to,
25
- back,
26
- get backNavigation() {
27
- return state.backNavigation !== false
28
- },
29
- set backNavigation(enabled) {
30
- state.backNavigation = enabled !== false
31
- },
32
- get currentRoute() {
33
- return currentRoute
34
- },
35
- get routes() {
36
- return this[symbols.routes]
37
- },
38
- get navigating() {
39
- return state.navigating
40
- },
41
- state,
42
- },
43
- writable: false,
155
+ get() {
156
+ return getRouterFromCache(this)
157
+ },
44
158
  enumerable: true,
45
159
  configurable: false,
46
160
  },
@@ -16,12 +16,14 @@
16
16
  */
17
17
 
18
18
  import Component from '../component.js'
19
- import Router, { state as routerState } from '../router/router.js'
19
+ import Router, {
20
+ registerRouterView,
21
+ state as routerState,
22
+ unregisterRouterView,
23
+ } from '../router/router.js'
20
24
  import symbols from '../lib/symbols.js'
21
25
  import Focus from '../focus/focus.js'
22
26
 
23
- let hashchangeHandler = null
24
-
25
27
  /** @typedef {{ $input?: (event: any) => boolean, $focus?: (event: any) => void }} RouterViewParent */
26
28
 
27
29
  export default () =>
@@ -123,24 +125,42 @@ export default () =>
123
125
  effects: [],
124
126
  context: {},
125
127
  },
128
+ props: {
129
+ name: '',
130
+ active: true,
131
+ },
126
132
  state() {
127
133
  return {
128
134
  activeView: null,
135
+ history: [],
129
136
  }
130
137
  },
138
+ watch: {
139
+ active(v, old) {
140
+ if (v === true && old === false) {
141
+ Router.navigate.apply(this)
142
+ }
143
+ },
144
+ },
131
145
  hooks: {
132
146
  async ready() {
147
+ registerRouterView(this)
148
+
133
149
  const parent = this[symbols.parent]
134
150
  if (parent && parent[symbols.routerHooks] && parent[symbols.routerHooks].init) {
135
151
  await parent[symbols.routerHooks].init.apply(parent)
136
152
  }
153
+ this.hashchangeHandler = () => Router.navigate.apply(this)
154
+
155
+ if (this.active === true) {
156
+ Router.navigate.apply(this)
157
+ }
137
158
 
138
- hashchangeHandler = () => Router.navigate.apply(this)
139
- Router.navigate.apply(this)
140
- window.addEventListener('hashchange', hashchangeHandler)
159
+ window.addEventListener('hashchange', this.hashchangeHandler)
141
160
  },
142
161
  destroy() {
143
- window.removeEventListener('hashchange', hashchangeHandler, false)
162
+ unregisterRouterView(this)
163
+ window.removeEventListener('hashchange', this.hashchangeHandler, false)
144
164
  },
145
165
  focus() {
146
166
  if (this.activeView && Focus.get() === this) {
@@ -81,6 +81,7 @@ export default {
81
81
  },
82
82
  input(key, event) {
83
83
  if (state.navigating === true) return
84
+ if (focusedComponent === null) return
84
85
 
85
86
  const componentWithInputEvent = getComponentWithInputEvent(focusedComponent, key)
86
87