@lightningjs/blits 1.36.0 → 1.38.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
+ ## v1.38.0
4
+
5
+ _26 Aug 2025_
6
+
7
+ - Added `reuseComponent` to route options to enable navigating to the same page and reusing the same page component instance
8
+
9
+ ## v1.37.0
10
+
11
+ _22 Aug 2025_
12
+
13
+ - Added Route options check before checking passFocus flag
14
+ - Added support for additional metadata in Route config (for associating arbitrary data to a route)
15
+ - Upgraded dev dependencies to latest versions
16
+
3
17
  ## v1.36.0
4
18
 
5
19
  _11 Aug 2025_
@@ -0,0 +1,85 @@
1
+ /*
2
+ * Copyright 2023 Comcast Cable Communications Management, LLC
3
+ * Licensed under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License.
5
+ * You may obtain a copy of the License at
6
+ *
7
+ * http://www.apache.org/licenses/LICENSE-2.0
8
+ *
9
+ * Unless required by applicable law or agreed to in writing, software
10
+ * distributed under the License is distributed on an "AS IS" BASIS,
11
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ * See the License for the specific language governing permissions and
13
+ * limitations under the License.
14
+ *
15
+ * SPDX-License-Identifier: Apache-2.0
16
+ */
17
+
18
+ const {
19
+ defineConfig,
20
+ globalIgnores,
21
+ } = require("eslint/config");
22
+
23
+ const globals = require("globals");
24
+ const prettier = require("eslint-plugin-prettier");
25
+ const babelParser = require("@babel/eslint-parser");
26
+ const js = require("@eslint/js");
27
+
28
+ const {
29
+ FlatCompat,
30
+ } = require("@eslint/eslintrc");
31
+
32
+ const compat = new FlatCompat({
33
+ baseDirectory: __dirname,
34
+ recommendedConfig: js.configs.recommended,
35
+ allConfig: js.configs.all
36
+ });
37
+
38
+ module.exports = defineConfig([{
39
+ languageOptions: {
40
+ globals: {
41
+ ...globals.browser,
42
+ ...globals.node,
43
+ globalThis: false,
44
+ },
45
+
46
+ parser: babelParser,
47
+ ecmaVersion: 2018,
48
+ sourceType: "module",
49
+
50
+ parserOptions: {
51
+ requireConfigFile: false,
52
+
53
+ babelOptions: {
54
+ plugins: ["@babel/plugin-syntax-import-assertions"],
55
+ },
56
+ },
57
+ },
58
+
59
+ plugins: {
60
+ prettier,
61
+ },
62
+
63
+ extends: compat.extends("eslint:recommended", "plugin:prettier/recommended", "prettier"),
64
+
65
+ rules: {
66
+ "no-console": process.env.NODE_ENV === "production" ? "error" : "off",
67
+ "no-debugger": process.env.NODE_ENV === "production" ? "error" : "off",
68
+ quotes: [2, "single", "avoid-escape"],
69
+ semi: [2, "never"],
70
+ "no-extra-boolean-cast": "off",
71
+
72
+ "no-unused-vars": [1, {
73
+ argsIgnorePattern: "res|next|^err",
74
+ }],
75
+
76
+ "prettier/prettier": ["error", {
77
+ trailingComma: "es5",
78
+ singleQuote: true,
79
+ tabWidth: 2,
80
+ semi: false,
81
+ printWidth: 100,
82
+ endOfLine: "auto",
83
+ }],
84
+ },
85
+ }, globalIgnores(["**/node_modules"])]);
package/index.d.ts CHANGED
@@ -185,15 +185,31 @@ declare module '@lightningjs/blits' {
185
185
  [key: string]: any
186
186
  }
187
187
 
188
- // todo: specify valid route options
189
- export interface RouteOptions {
188
+ /**
189
+ * Router Options that can be used at the same time.
190
+ */
191
+ interface ConcurrentRouteOpts {
190
192
  /**
191
193
  * Whether the page navigation should be added to the history stack
192
194
  * used when navigating back using `this.$router.back()`
193
195
  *
194
196
  * @default true
195
197
  */
196
- inHistory?: Boolean
198
+ inHistory?: boolean,
199
+ passFocus?: boolean,
200
+ }
201
+
202
+ /**
203
+ * Route Options that can't be true at the same time
204
+ */
205
+ type MutualExclusiveRouteOpts = {
206
+ /**
207
+ * Whether the router should reuse the current page component instance (when matching with the Component
208
+ * specified for the route that we're routing to).
209
+ *
210
+ * @default true
211
+ */
212
+ reuseComponent?: true,
197
213
  /**
198
214
  * Whether the page should be kept alive when navigating away. Can be useful
199
215
  * for a homepage where the state should be fully retained when navigating back
@@ -201,17 +217,25 @@ declare module '@lightningjs/blits' {
201
217
  *
202
218
  * @default false
203
219
  */
204
- keepAlive?: Boolean
220
+ keepAlive?: false
221
+ } | {
205
222
  /**
206
- * Whether the focus should be delegated to the page that's being navigated to.
207
- * Can be useful when navigating to a new page from a widget / menu overlaying the
208
- * RouterView, where the widget should maintain the focus (instead of the new page, which
209
- * is the default behaviour)
223
+ * Whether the router should reuse the current page component instance (when matching with the Component
224
+ * specified for the route that we're routing to).
210
225
  *
211
226
  * @default true
212
227
  */
213
- passFocus?: Boolean
228
+ reuseComponent?: false,
229
+ /**
230
+ * Whether the page should be kept alive when navigating away. Can be useful
231
+ * for a homepage where the state should be fully retained when navigating back
232
+ * from a details page
233
+ *
234
+ * @default false
235
+ */
236
+ keepAlive?: true
214
237
  }
238
+ export type RouteOptions = ConcurrentRouteOpts & MutualExclusiveRouteOpts;
215
239
 
216
240
  export interface Router {
217
241
  /**
@@ -679,6 +703,17 @@ declare module '@lightningjs/blits' {
679
703
  data?: {
680
704
  [key: string]: any
681
705
  }
706
+
707
+ /**
708
+ * Metadata attached to the route, can be used to add any arbitrary
709
+ * data to a route such as `auth: true/false`, a route ID or route description
710
+ * Note that this data is not reactive and not passed as props to components.
711
+ * The metadata is available in the router `before` or `beforeEach` hooks
712
+ *
713
+ */
714
+ meta?: {
715
+ [key: string]: any
716
+ }
682
717
  }
683
718
 
684
719
  type ComponentFactory = () => void
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightningjs/blits",
3
- "version": "1.36.0",
3
+ "version": "1.38.0",
4
4
  "description": "Blits: The Lightning 3 App Development Framework",
5
5
  "bin": "bin/index.js",
6
6
  "exports": {
@@ -21,7 +21,8 @@
21
21
  "prepublishOnly": "node scripts/prepublishOnly.js",
22
22
  "postpublish": "node scripts/postpublish.js",
23
23
  "prepare": "husky",
24
- "pack-preview": "npm pack --dry-run"
24
+ "pack-preview": "npm pack --dry-run",
25
+ "release": "npm publish --access public"
25
26
  },
26
27
  "types": "./index.d.ts",
27
28
  "lint-staged": {
@@ -35,16 +36,19 @@
35
36
  "devDependencies": {
36
37
  "@babel/eslint-parser": "^7.26.5",
37
38
  "@babel/plugin-syntax-import-assertions": "^7.26.0",
38
- "c8": "^8.0.1",
39
- "eslint": "^8.8.0",
40
- "eslint-config-prettier": "^8.3.0",
41
- "eslint-plugin-prettier": "^4.0.0",
39
+ "@eslint/eslintrc": "^3.3.1",
40
+ "@eslint/js": "^9.33.0",
41
+ "c8": "^9.1.0",
42
+ "eslint": "^9.33.0",
43
+ "eslint-config-prettier": "^10.1.8",
44
+ "eslint-plugin-prettier": "^5.5.4",
42
45
  "fast-glob": "^3.3.3",
43
46
  "global-jsdom": "24.0.0",
47
+ "globals": "^16.3.0",
44
48
  "husky": "^9.1.7",
45
49
  "jsdom": "24.0.0",
46
50
  "lint-staged": "^15.5.0",
47
- "prettier": "^2.5.1",
51
+ "prettier": "^3.6.2",
48
52
  "sinon": "^21.0.0",
49
53
  "tap-diff": "^0.1.1",
50
54
  "tape": "^5.5.0"
@@ -96,9 +96,12 @@ const Application = (config) => {
96
96
 
97
97
  Focus.input(key, e)
98
98
  clearTimeout(holdTimeout)
99
- holdTimeout = setTimeout(() => {
100
- Focus.hold = true
101
- }, Settings.get('holdTimeout', DEFAULT_HOLD_TIMEOUT_MS))
99
+ holdTimeout = setTimeout(
100
+ () => {
101
+ Focus.hold = true
102
+ },
103
+ Settings.get('holdTimeout', DEFAULT_HOLD_TIMEOUT_MS)
104
+ )
102
105
  }
103
106
 
104
107
  keyUpHandler = (e) => {
@@ -41,8 +41,8 @@ export default (component, props = []) => {
41
41
  this[symbols.props] !== undefined && prop.key in this[symbols.props]
42
42
  ? this[symbols.props][prop.key]
43
43
  : 'default' in prop
44
- ? prop.default
45
- : undefined
44
+ ? prop.default
45
+ : undefined
46
46
  )
47
47
 
48
48
  if (prop.required === true && value === undefined) {
@@ -107,8 +107,8 @@ const layoutFn = function (config) {
107
107
  ? node.height + gap
108
108
  : 0
109
109
  : node.height !== 0
110
- ? node.height + gap
111
- : 0
110
+ ? node.height + gap
111
+ : 0
112
112
  }
113
113
  otherDimension = Math.max(
114
114
  otherDimension,
@@ -191,9 +191,9 @@ const generateElementCode = function (
191
191
  } else {
192
192
  this.effectsCode.push(`
193
193
  ${elm}.set('${key.substring(1)}', ${interpolate(
194
- templateObject[key],
195
- options.component
196
- )})
194
+ templateObject[key],
195
+ options.component
196
+ )})
197
197
  `)
198
198
  }
199
199
  // value.includes('.') === false &&
@@ -310,9 +310,9 @@ const generateComponentCode = function (
310
310
  if (isReactiveKey(key)) {
311
311
  this.effectsCode.push(`
312
312
  ${elm}[Symbol.for('props')]['${key.substring(1)}'] = ${interpolate(
313
- templateObject[key],
314
- options.component
315
- )}`)
313
+ templateObject[key],
314
+ options.component
315
+ )}`)
316
316
  renderCode.push(`
317
317
  propData = ${interpolate(templateObject[key], options.component)}
318
318
  if (Array.isArray(propData) === true) {
@@ -41,8 +41,8 @@ export default {
41
41
  Object.keys(translations).length === 0
42
42
  ? 'No translations loaded. Please load a file with translations or specify a translations object manually'
43
43
  : language in translations === false
44
- ? `Language ${language} not available in the loaded translations`
45
- : false
44
+ ? `Language ${language} not available in the loaded translations`
45
+ : false
46
46
  if (warningMsg) {
47
47
  Log.warn(warningMsg)
48
48
  // set the current language in the reactive state
@@ -127,12 +127,15 @@ export default {
127
127
  return Object.keys(
128
128
  // maps array input to an object {0: 'item1', 1: 'item2'}
129
129
  Array.isArray(replacements) ? Object.assign({}, replacements) : replacements
130
- ).reduce((text, replacementKey) => {
131
- return text.replace(
132
- new RegExp('{\\s?' + replacementKey + '\\s?}', 'g'),
133
- replacements[replacementKey]
134
- )
135
- }, (dictionary && dictionary[key]) || key)
130
+ ).reduce(
131
+ (text, replacementKey) => {
132
+ return text.replace(
133
+ new RegExp('{\\s?' + replacementKey + '\\s?}', 'g'),
134
+ replacements[replacementKey]
135
+ )
136
+ },
137
+ (dictionary && dictionary[key]) || key
138
+ )
136
139
  }
137
140
  },
138
141
  get language() {
@@ -176,7 +176,7 @@ export const matchHash = (path, routes = []) => {
176
176
  }
177
177
 
178
178
  if (matchingRoute) {
179
- matchingRoute.options = { ...matchingRoute.options, ...overrideOptions }
179
+ matchingRoute.options = { ...defaultOptions, ...matchingRoute.options, ...overrideOptions }
180
180
  if (!matchingRoute.data) {
181
181
  matchingRoute.data = {}
182
182
  }
@@ -186,6 +186,17 @@ export const matchHash = (path, routes = []) => {
186
186
  return matchingRoute
187
187
  }
188
188
 
189
+ /**
190
+ * Default Route options
191
+ *
192
+ */
193
+ const defaultOptions = {
194
+ inHistory: true,
195
+ keepAlive: false,
196
+ passFocus: true,
197
+ reuseComponent: false,
198
+ }
199
+
189
200
  /**
190
201
  * Navigate to a route
191
202
  *
@@ -202,12 +213,14 @@ export const navigate = async function () {
202
213
  Announcer.stop()
203
214
  Announcer.clear()
204
215
  state.navigating = true
216
+ let reuse = false
205
217
  if (this.parent[symbols.routes]) {
206
218
  let previousRoute = currentRoute //? Object.assign({}, currentRoute) : undefined
207
219
  const { hash, path, queryParams } = getHash()
208
220
  let route = matchHash(path, this.parent[symbols.routes])
209
221
 
210
222
  currentRoute = route
223
+
211
224
  if (route) {
212
225
  const queryParamsData = {}
213
226
  const queryParamsEntries = [...queryParams.entries()]
@@ -271,6 +284,26 @@ export const navigate = async function () {
271
284
 
272
285
  let { view, focus } = cacheMap.get(route.hash) || {}
273
286
 
287
+ // merge props with potential route params, navigation data and route data to be injected into the component instance
288
+ const props = {
289
+ ...this[symbols.props],
290
+ ...route.params,
291
+ ...route.data,
292
+ }
293
+
294
+ if (
295
+ previousRoute &&
296
+ route.component === previousRoute.component &&
297
+ route.options.reuseComponent === true &&
298
+ route.options.keepAlive !== true
299
+ ) {
300
+ reuse = true
301
+ view = this[symbols.children][this[symbols.children].length - 1]
302
+ for (const prop in props) {
303
+ view[symbols.props][prop] = props[prop]
304
+ }
305
+ }
306
+
274
307
  // Announce route change if a message has been specified for this route
275
308
  if (route.announce) {
276
309
  if (typeof route.announce === 'string') {
@@ -288,13 +321,6 @@ export const navigate = async function () {
288
321
  holder.set('w', '100%')
289
322
  holder.set('h', '100%')
290
323
 
291
- // merge props with potential route params, navigation data and route data to be injected into the component instance
292
- const props = {
293
- ...this[symbols.props],
294
- ...route.params,
295
- ...route.data,
296
- }
297
-
298
324
  view = await route.component({ props }, holder, this)
299
325
 
300
326
  if (view[Symbol.toStringTag] === 'Module') {
@@ -341,7 +367,7 @@ export const navigate = async function () {
341
367
  this.activeView = children[children.length - 1]
342
368
 
343
369
  // set focus to the view that we're routing to (unless explicitly disabling passing focus)
344
- if (route.options.passFocus !== false) {
370
+ if (route.options === undefined || route.options.passFocus !== false) {
345
371
  focus ? focus.$focus() : /** @type {BlitsComponent} */ (view).$focus()
346
372
  }
347
373
 
@@ -359,7 +385,7 @@ export const navigate = async function () {
359
385
  let shouldAnimate = false
360
386
 
361
387
  // apply out out transition on previous view
362
- if (previousRoute) {
388
+ if (previousRoute && reuse === false) {
363
389
  // only animate when there is a previous route
364
390
  shouldAnimate = true
365
391
  const oldView = this[symbols.children].splice(1, 1).pop()