vue3-router-tab 1.3.6 → 1.3.8

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.
@@ -117,14 +117,33 @@ function insertTab(tabs: TabRecord[], tab: TabRecord, position: 'last' | 'next',
117
117
  tabs.push(tab)
118
118
  }
119
119
 
120
- function enforceMaxAlive(tabs: TabRecord[], maxAlive: number, activeId: string | null) {
120
+ /**
121
+ * Enforces the maximum number of alive (cached) tabs.
122
+ * When exceeded, removes the oldest tabs from KeepAlive cache.
123
+ */
124
+ function enforceMaxAlive(
125
+ tabs: TabRecord[],
126
+ maxAlive: number,
127
+ activeId: string | null,
128
+ aliveCache: Set<string>
129
+ ) {
121
130
  if (!maxAlive || maxAlive <= 0) return
131
+
122
132
  const aliveTabs = tabs.filter(tab => tab.alive)
133
+
123
134
  while (aliveTabs.length > maxAlive) {
124
135
  const candidate = aliveTabs.shift()
125
136
  if (!candidate || candidate.id === activeId) continue
137
+
126
138
  const idx = tabs.findIndex(tab => tab.id === candidate.id)
127
- if (idx > -1) tabs[idx].alive = false
139
+ if (idx > -1) {
140
+ const tab = tabs[idx]
141
+ const cacheKey = `${tab.id}::${tab.renderKey ?? 0}`
142
+
143
+ // Remove from cache and mark as not alive
144
+ aliveCache.delete(cacheKey)
145
+ tab.alive = false
146
+ }
128
147
  }
129
148
  }
130
149
 
@@ -163,11 +182,16 @@ export function createRouterTabs(
163
182
  const activeId = ref<string | null>(null)
164
183
  const current = shallowRef<TabRecord>()
165
184
  const refreshingKey = ref<string | null>(null)
166
- const includeKeys = computed(() =>
167
- tabs
168
- .filter(tab => tab.alive)
169
- .map(tab => `${tab.id}::${tab.renderKey ?? 0}`)
170
- )
185
+
186
+ // Track which keys should be in KeepAlive cache
187
+ // This is the source of truth for KeepAlive's include prop
188
+ const aliveCache = reactive<Set<string>>(new Set())
189
+
190
+ const includeKeys = computed(() => {
191
+ // Convert Set to Array for KeepAlive's include prop
192
+ // Format: ['routeKey::renderKey', ...]
193
+ return Array.from(aliveCache)
194
+ })
171
195
 
172
196
  let isHydrating = false
173
197
 
@@ -186,31 +210,86 @@ export function createRouterTabs(
186
210
  }
187
211
  }
188
212
 
213
+ /**
214
+ * Ensures a tab exists for the given route and manages its KeepAlive cache state.
215
+ * This is called on every route navigation via the router watcher.
216
+ */
189
217
  function ensureTab(route: RouteLocationNormalizedLoaded) {
190
218
  const key = resolveKey(route)
191
219
  let tab = tabs.find(item => item.id === key)
220
+ const shouldBeAlive = resolveAlive(route, options.keepAlive)
192
221
 
193
222
  if (tab) {
223
+ // Tab exists - update its properties
194
224
  tab.fullPath = route.fullPath
195
225
  tab.to = route.fullPath
196
226
  tab.matched = route
197
- tab.alive = resolveAlive(route, options.keepAlive)
198
227
  tab.reusable = resolveReusable(route, tab.reusable)
228
+
199
229
  // Ensure renderKey is initialized
200
230
  if (typeof tab.renderKey !== 'number') {
201
231
  tab.renderKey = 0
202
232
  }
233
+
234
+ // Generate the current cache key for this tab
235
+ const currentCacheKey = `${key}::${tab.renderKey}`
236
+
237
+ // Debug logging for specific routes
238
+ if (key.includes('students') || key.includes('classroom') || key.includes('quiz')) {
239
+ console.log(`[ensureTab] EXISTING tab: ${route.fullPath}`, {
240
+ key,
241
+ shouldBeAlive,
242
+ currentRenderKey: tab.renderKey,
243
+ currentCacheKey,
244
+ isInCache: aliveCache.has(currentCacheKey),
245
+ aliveCacheSize: aliveCache.size,
246
+ cacheContents: Array.from(aliveCache)
247
+ })
248
+ }
249
+
250
+ // Manage KeepAlive cache state
251
+ if (shouldBeAlive) {
252
+ // Check if tab's current key is in the cache
253
+ if (!aliveCache.has(currentCacheKey)) {
254
+ // Tab was evicted or never added to cache - add it back
255
+ aliveCache.add(currentCacheKey)
256
+ tab.alive = true
257
+ if (key.includes('students') || key.includes('classroom') || key.includes('quiz')) {
258
+ console.log(`[ensureTab] ✅ Added to cache: ${currentCacheKey}`)
259
+ }
260
+ } else if (!tab.alive) {
261
+ // Tab is in cache but marked as not alive - just reactivate
262
+ tab.alive = true
263
+ if (key.includes('students') || key.includes('classroom') || key.includes('quiz')) {
264
+ console.log(`[ensureTab] ✅ Reactivated: ${currentCacheKey}`)
265
+ }
266
+ }
267
+ }
268
+
203
269
  Object.assign(tab, pickMeta(route))
204
270
  return tab
205
271
  }
206
272
 
273
+ // Create new tab
207
274
  tab = createTabFromRoute(route, {}, options.keepAlive)
275
+
276
+ // Add to cache if it should be alive
277
+ if (tab.alive) {
278
+ const cacheKey = `${key}::${tab.renderKey ?? 0}`
279
+ aliveCache.add(cacheKey)
280
+
281
+ if (key.includes('students') || key.includes('classroom') || key.includes('quiz')) {
282
+ console.log(`[ensureTab] NEW tab created and cached: ${cacheKey}`)
283
+ }
284
+ }
285
+
208
286
  insertTab(tabs, tab, options.appendPosition, activeId.value)
209
- enforceMaxAlive(tabs, options.maxAlive, activeId.value)
287
+ enforceMaxAlive(tabs, options.maxAlive, activeId.value, aliveCache)
288
+
210
289
  return tab
211
290
  }
212
291
 
213
- async function openTab(path: RouteLocationRaw, replace = false, refresh: boolean | 'sameTab' = true) {
292
+ async function openTab(path: RouteLocationRaw, replace = false, refresh: boolean | 'sameTab' = 'sameTab') {
214
293
  const target = resolveRoute(router, path)
215
294
  const targetKey = resolveKey(target)
216
295
  const sameKey = activeId.value === targetKey
@@ -278,37 +357,50 @@ export function createRouterTabs(
278
357
  }
279
358
  }
280
359
 
360
+ /**
361
+ * Refreshes a tab by incrementing its renderKey and cycling its KeepAlive state.
362
+ * This forces the component to unmount and remount, clearing all internal state.
363
+ *
364
+ * @param id - The tab ID to refresh. Defaults to the currently active tab.
365
+ * @param force - If true, skips transition delays for immediate refresh.
366
+ */
281
367
  async function refreshTab(id: string | undefined = activeId.value ?? undefined, force = false) {
282
368
  if (!id) return
369
+
283
370
  const tab = tabs.find(item => item.id === id)
284
371
  if (!tab) return
285
372
 
286
- const wasAlive = options.keepAlive && tab.alive
373
+ const shouldRestoreCache = options.keepAlive && tab.alive
374
+ const oldCacheKey = `${id}::${tab.renderKey ?? 0}`
287
375
 
288
- // Remove from KeepAlive cache if it was alive
289
- if (wasAlive) {
376
+ // Step 1: Remove from KeepAlive cache to prepare for fresh mount
377
+ if (shouldRestoreCache) {
378
+ aliveCache.delete(oldCacheKey)
290
379
  tab.alive = false
291
380
  await nextTick()
292
381
  }
293
382
 
294
- // Increment render key to force re-render
383
+ // Step 2: Increment renderKey to generate new cache key (e.g., /quiz::0 → /quiz::1)
384
+ // This ensures KeepAlive treats it as a completely new component instance
295
385
  tab.renderKey = (tab.renderKey ?? 0) + 1
386
+ const newCacheKey = `${id}::${tab.renderKey}`
296
387
 
297
- // Restore to KeepAlive cache
298
- if (wasAlive) {
388
+ // Step 3: Restore to KeepAlive cache with new renderKey
389
+ if (shouldRestoreCache) {
390
+ aliveCache.add(newCacheKey)
299
391
  tab.alive = true
300
392
  }
301
393
 
302
- // Set refreshing state to trigger component unmount with transition
394
+ // Step 4: Trigger transition by marking tab as refreshing
303
395
  refreshingKey.value = id
304
396
  await nextTick()
305
397
 
306
- // Wait for unmount transition and new component mount
398
+ // Step 5: Allow transition to complete unless force refresh
307
399
  if (!force) {
308
400
  await nextTick()
309
401
  }
310
402
 
311
- // Clear refreshing state to show the new component
403
+ // Step 6: Clear refreshing state to render the refreshed component
312
404
  refreshingKey.value = null
313
405
  }
314
406
 
@@ -322,22 +414,43 @@ export function createRouterTabs(
322
414
  function setTabAlive(id: string, alive: boolean) {
323
415
  const tab = tabs.find(t => t.id === id)
324
416
  if (!tab) return
325
- tab.alive = Boolean(alive)
326
- // enforce max alive if turning alive on
327
- if (tab.alive) enforceMaxAlive(tabs, options.maxAlive, activeId.value)
417
+
418
+ const cacheKey = `${id}::${tab.renderKey ?? 0}`
419
+
420
+ if (alive) {
421
+ aliveCache.add(cacheKey)
422
+ tab.alive = true
423
+ // enforce max alive if turning alive on
424
+ enforceMaxAlive(tabs, options.maxAlive, activeId.value, aliveCache)
425
+ } else {
426
+ aliveCache.delete(cacheKey)
427
+ tab.alive = false
428
+ }
328
429
  }
329
430
 
330
- // Evict a tab from the KeepAlive cache and increment renderKey so it mounts fresh
431
+ /**
432
+ * Evicts a tab from KeepAlive cache and increments its renderKey.
433
+ * When the tab is re-activated, it will mount as a fresh component instance.
434
+ *
435
+ * @param id - The tab ID to evict from cache.
436
+ */
331
437
  function evictCache(id: string) {
332
438
  const tab = tabs.find(t => t.id === id)
333
439
  if (!tab) return
334
- if (tab.alive) tab.alive = false
335
- // bump renderKey to ensure a fresh key when re-enabled
440
+
441
+ const oldCacheKey = `${id}::${tab.renderKey ?? 0}`
442
+
443
+ // Remove from cache
444
+ aliveCache.delete(oldCacheKey)
445
+ tab.alive = false
446
+
447
+ // Increment renderKey to ensure fresh mount on next activation
336
448
  tab.renderKey = (tab.renderKey ?? 0) + 1
337
449
  }
338
450
 
339
451
  // Clear keep-alive for all tabs
340
452
  function clearCache() {
453
+ aliveCache.clear()
341
454
  tabs.forEach(tab => {
342
455
  tab.alive = false
343
456
  })
@@ -420,7 +533,7 @@ export function createRouterTabs(
420
533
  const tab = ensureTab(route as RouteLocationNormalizedLoaded)
421
534
  activeId.value = tab.id
422
535
  current.value = tab
423
- enforceMaxAlive(tabs, options.maxAlive, activeId.value)
536
+ enforceMaxAlive(tabs, options.maxAlive, activeId.value, aliveCache)
424
537
  },
425
538
  { immediate: true }
426
539
  )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vue3-router-tab",
3
- "version": "1.3.6",
3
+ "version": "1.3.8",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",
@@ -9,16 +9,7 @@
9
9
  ],
10
10
  "main": "dist/vue3-router-tab.umd.cjs",
11
11
  "module": "dist/vue3-router-tab.js",
12
- "exports": {
13
- ".": {
14
- "types": "./index.d.ts",
15
- "import": "./dist/vue3-router-tab.js",
16
- "require": "./dist/vue3-router-tab.umd.cjs",
17
- "default": "./dist/vue3-router-tab.js"
18
- },
19
- "./style": "./dist/vue3-router-tab.css",
20
- "./package.json": "./package.json"
21
- },
12
+ "types": "index.d.ts",
22
13
  "scripts": {
23
14
  "build": "vite build",
24
15
  "prepublishOnly": "npm run build"
@@ -60,7 +51,6 @@
60
51
  "bugs": {
61
52
  "url": "https://github.com/anilshr25/vue3-router-tab/issues"
62
53
  },
63
- "types": "index.d.ts",
64
54
  "pnpm": {
65
55
  "onlyBuiltDependencies": [
66
56
  "@parcel/watcher",