@bitstillery/mithril 3.4.0 → 3.4.2
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/api/router.ts +15 -23
- package/package.json +1 -1
- package/pathname/compileTemplate.ts +4 -1
- package/render/ssrState.ts +4 -2
- package/store.ts +36 -12
package/api/router.ts
CHANGED
|
@@ -53,7 +53,7 @@ interface MountRedraw {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
interface RouteOptions {
|
|
56
|
-
/** When true,
|
|
56
|
+
/** When true, bumps remountNonce once so the resolver fragment key changes for that navigation (e.g. Link). */
|
|
57
57
|
remount?: boolean
|
|
58
58
|
replace?: boolean
|
|
59
59
|
state?: any
|
|
@@ -80,8 +80,8 @@ export default function router($window: any, mountRedraw: MountRedraw) {
|
|
|
80
80
|
let currentPath: string | undefined
|
|
81
81
|
let lastUpdate: ((comp: any) => void) | null = null
|
|
82
82
|
let routeSetResolve: (() => void) | null = null
|
|
83
|
-
|
|
84
|
-
let
|
|
83
|
+
/** Incremented only when route.set(..., { remount: true }); fragment key is m-route-${remountNonce}. */
|
|
84
|
+
let remountNonce = 0
|
|
85
85
|
|
|
86
86
|
const RouterRoot: ComponentType = {
|
|
87
87
|
onremove: function () {
|
|
@@ -94,21 +94,15 @@ export default function router($window: any, mountRedraw: MountRedraw) {
|
|
|
94
94
|
// if (!hasBeenResolved) return
|
|
95
95
|
|
|
96
96
|
// Pass currentPath in attrs so RouteResolver.render can use it for routePath.
|
|
97
|
-
//
|
|
98
|
-
//
|
|
97
|
+
// No vnode key on the route component: same resolved component updates in place when path/query
|
|
98
|
+
// changes; apps sync state via attrs/onupdate. Do not use attrs.key from merged query (?key=...).
|
|
99
99
|
const routeAttrs = {...attrs, routePath: currentPath || attrs.routePath}
|
|
100
|
-
const
|
|
101
|
-
const vnode = Vnode(component, vnodeKey, routeAttrs, null, null, null)
|
|
100
|
+
const vnode = Vnode(component, undefined, routeAttrs, null, null, null)
|
|
102
101
|
if (currentResolver) {
|
|
103
102
|
const result = currentResolver.render!(vnode as any)
|
|
104
|
-
//
|
|
105
|
-
// (
|
|
106
|
-
|
|
107
|
-
const fragmentKey = remountPath ?? currentPath ?? undefined
|
|
108
|
-
if (remountPath && fragmentKey != null) {
|
|
109
|
-
return hyperscript.fragment({key: fragmentKey}, result)
|
|
110
|
-
}
|
|
111
|
-
return result
|
|
103
|
+
// Always wrap in a keyed fragment (stable shape). Key only changes when remountNonce bumps
|
|
104
|
+
// (route.set with remount: true), not on ordinary URL updates.
|
|
105
|
+
return hyperscript.fragment({key: 'm-route-' + remountNonce}, result)
|
|
112
106
|
}
|
|
113
107
|
// Wrap in a fragment to preserve existing key semantics
|
|
114
108
|
return [vnode]
|
|
@@ -188,7 +182,10 @@ export default function router($window: any, mountRedraw: MountRedraw) {
|
|
|
188
182
|
const path = decodeURIComponentSafe(prefix).slice(route.prefix.length)
|
|
189
183
|
const data = parsePathname(path)
|
|
190
184
|
|
|
191
|
-
|
|
185
|
+
const histState = $window?.history?.state
|
|
186
|
+
if (histState != null && typeof histState === 'object' && !Array.isArray(histState)) {
|
|
187
|
+
Object.assign(data.params, histState)
|
|
188
|
+
}
|
|
192
189
|
|
|
193
190
|
function reject(e: any) {
|
|
194
191
|
console.error(e)
|
|
@@ -248,10 +245,6 @@ export default function router($window: any, mountRedraw: MountRedraw) {
|
|
|
248
245
|
}
|
|
249
246
|
attrs = data.params
|
|
250
247
|
currentPath = path
|
|
251
|
-
if (pendingRemount) {
|
|
252
|
-
remountPath = path
|
|
253
|
-
pendingRemount = false
|
|
254
|
-
}
|
|
255
248
|
lastUpdate = null
|
|
256
249
|
routeSetResolve?.()
|
|
257
250
|
routeSetResolve = null
|
|
@@ -339,10 +332,9 @@ export default function router($window: any, mountRedraw: MountRedraw) {
|
|
|
339
332
|
}
|
|
340
333
|
lastUpdate = null
|
|
341
334
|
|
|
342
|
-
if (
|
|
343
|
-
|
|
335
|
+
if (options?.remount) {
|
|
336
|
+
remountNonce++
|
|
344
337
|
}
|
|
345
|
-
pendingRemount = options?.remount ?? false
|
|
346
338
|
|
|
347
339
|
path = buildPathname(path, data || {})
|
|
348
340
|
if (!ready || !$window) {
|
package/package.json
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import parsePathname from './parse'
|
|
2
2
|
|
|
3
|
+
/** Path segments, :params, and regexp escapes (shared by compileTemplate). */
|
|
4
|
+
const TEMPLATE_PATH_TO_REGEXP = /:([^/.-]+)(\.{3}|\.(?!\.)|-)?|[\\^$*+.()|[\]{}]/g
|
|
5
|
+
|
|
3
6
|
interface CompiledTemplate {
|
|
4
7
|
(data: {path: string; params: Record<string, any>}): boolean
|
|
5
8
|
}
|
|
@@ -20,7 +23,7 @@ export default function compileTemplate(template: string): CompiledTemplate {
|
|
|
20
23
|
// `:lang-:locale` in routes. This is all merged into one pass so I
|
|
21
24
|
// don't also accidentally escape `-` and make it harder to detect it to
|
|
22
25
|
// ban it from template parameters.
|
|
23
|
-
|
|
26
|
+
TEMPLATE_PATH_TO_REGEXP,
|
|
24
27
|
function (m, key, extra) {
|
|
25
28
|
if (key == null) return '\\' + m
|
|
26
29
|
keys.push({k: key, r: extra === '...'})
|
package/render/ssrState.ts
CHANGED
|
@@ -100,8 +100,10 @@ export function serializeStore(state: State<any>): any {
|
|
|
100
100
|
continue
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
// For nested states, only serialize keys that belong to this state (not parent keys)
|
|
104
|
-
|
|
103
|
+
// For nested states, only serialize keys that belong to this state (not parent keys).
|
|
104
|
+
// When originalKeys is empty (e.g. lookup: {}), include all keys so dynamically
|
|
105
|
+
// added entries (e.g. $s.lookup[path] = data) are serialized.
|
|
106
|
+
if (nestedOriginalKeys && nestedOriginalKeys.size > 0 && !nestedOriginalKeys.has(key)) {
|
|
105
107
|
continue
|
|
106
108
|
}
|
|
107
109
|
|
package/store.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import {state, State, updateStateRegistry} from './state'
|
|
2
2
|
import {serializeStore, deserializeStore} from './render/ssrState'
|
|
3
|
-
import {ComputedSignal} from './signal'
|
|
4
3
|
|
|
5
4
|
// Helper function to restore computed properties (same as in ssrState.ts)
|
|
6
5
|
function restoreComputedProperties(state: State<any>, initial: any): void {
|
|
@@ -374,22 +373,26 @@ export class Store<T extends Record<string, any> = Record<string, any>> {
|
|
|
374
373
|
return
|
|
375
374
|
}
|
|
376
375
|
|
|
377
|
-
// Default to
|
|
378
|
-
const
|
|
379
|
-
const
|
|
380
|
-
const
|
|
376
|
+
// Default: write to localStorage and sessionStorage when no options; session is opt-in
|
|
377
|
+
const writeLocalStorage = options?.saved ?? options === undefined
|
|
378
|
+
const writeSessionStorage = options?.tab ?? options === undefined
|
|
379
|
+
const writeSessionApi = options?.session === true
|
|
381
380
|
|
|
382
|
-
// Use SSR serialization which properly handles State objects and skips ComputedSignal properties
|
|
383
|
-
// This is the same mechanism used for SSR, ensuring consistency
|
|
384
381
|
const statePlain = serializeStore(this.stateInstance)
|
|
385
382
|
|
|
386
|
-
//
|
|
387
|
-
if (
|
|
383
|
+
// Write to localStorage (persistent across browser restarts)
|
|
384
|
+
if (writeLocalStorage && this.templates.saved) {
|
|
388
385
|
this.set(this.storageKey, this.blueprint(statePlain, copy_object(this.templates.saved)))
|
|
389
386
|
}
|
|
390
387
|
|
|
391
|
-
//
|
|
392
|
-
|
|
388
|
+
// Lookup is always persisted to localStorage when present, regardless of writeLocalStorage.
|
|
389
|
+
// This ensures cached values (e.g. filter/sort state) are never lost when save() is called.
|
|
390
|
+
if ((statePlain as any).lookup) {
|
|
391
|
+
this.persist_lookup_to_local_storage(statePlain)
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Write to sessionStorage (tab-scoped, cleared when tab closes)
|
|
395
|
+
if (writeSessionStorage && this.templates.tab) {
|
|
393
396
|
const tabState = (this.stateInstance as any).tab
|
|
394
397
|
if (tabState) {
|
|
395
398
|
// Get the tab template - unwrap if it's nested under a 'tab' key
|
|
@@ -420,7 +423,7 @@ export class Store<T extends Record<string, any> = Record<string, any>> {
|
|
|
420
423
|
// Save to session API (session state) - async by nature
|
|
421
424
|
// Only save session on client side (not during SSR)
|
|
422
425
|
if (
|
|
423
|
-
|
|
426
|
+
writeSessionApi &&
|
|
424
427
|
this.templates.session &&
|
|
425
428
|
Object.keys(this.templates.session).length > 0 &&
|
|
426
429
|
typeof window !== 'undefined'
|
|
@@ -441,6 +444,27 @@ export class Store<T extends Record<string, any> = Record<string, any>> {
|
|
|
441
444
|
}
|
|
442
445
|
}
|
|
443
446
|
|
|
447
|
+
/**
|
|
448
|
+
* Merge lookup from state into localStorage and write. Ensures lookup is always persisted
|
|
449
|
+
* whenever save() is called and state contains lookup.
|
|
450
|
+
*/
|
|
451
|
+
private persist_lookup_to_local_storage(statePlain: Record<string, any>): void {
|
|
452
|
+
if (typeof window === 'undefined') return
|
|
453
|
+
try {
|
|
454
|
+
const existing = this.get(this.storageKey)
|
|
455
|
+
let storeData: Record<string, any> = {}
|
|
456
|
+
try {
|
|
457
|
+
storeData = JSON.parse(existing) || {}
|
|
458
|
+
} catch {
|
|
459
|
+
storeData = {}
|
|
460
|
+
}
|
|
461
|
+
storeData.lookup = copy_object(statePlain.lookup || {})
|
|
462
|
+
window.localStorage.setItem(this.storageKey, JSON.stringify(storeData))
|
|
463
|
+
} catch (err) {
|
|
464
|
+
console.error('Cannot persist lookup to Local Storage; continue without.', err)
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
444
468
|
set(key: string, item: object): void {
|
|
445
469
|
if (typeof window === 'undefined') return
|
|
446
470
|
try {
|