diff --git a/packages/abc/reuse-tab/reuse-tab.component.spec.ts b/packages/abc/reuse-tab/reuse-tab.component.spec.ts index 22027021fc..efe6e30e33 100644 --- a/packages/abc/reuse-tab/reuse-tab.component.spec.ts +++ b/packages/abc/reuse-tab/reuse-tab.component.spec.ts @@ -139,6 +139,26 @@ describe('abc: reuse-tab', () => { .expectCount(3) .end(); })); + it('should be keep open order', fakeAsync(() => { + srv.max = 10; + page + .to('#b') + .expectUrl(0, '/a') + .expectUrl(1, '/b/1') + .to('#a') + .expectUrl(0, '/a') + .expectUrl(1, '/b/1') + .to('#c') + .expectUrl(0, '/a') + .expectUrl(1, '/c') + .expectUrl(2, '/b/1') + .to('#d') + .expectUrl(0, '/a') + .expectUrl(1, '/c') + .expectUrl(2, '/d') + .expectUrl(3, '/b/1') + .end(); + })); }); describe('#close', () => { @@ -149,7 +169,6 @@ describe('abc: reuse-tab', () => { })); it('should show next tab when closed a has next tab', fakeAsync(() => { srv.max = 10; - // debugger; page.to('#b'); page.to('#c'); page.go(1); @@ -210,14 +229,7 @@ describe('abc: reuse-tab', () => { fixture.detectChanges(); }); it('should working', fakeAsync(() => { - page - .to('#b') - .expectCount(MAX) - .to('#c') - .expectCount(MAX + 1) // +1 => current page - .to('#d') - .expectCount(MAX + 1) - .end(); + page.to('#b').expectCount(MAX).to('#c').expectCount(MAX).to('#d').expectCount(MAX).end(); })); }); describe('#allowClose', () => { @@ -798,7 +810,7 @@ describe('abc: reuse-tab', () => { this.cd(); return this; } - openContextMenu(pos: number, eventArgs?: NzSafeAny): this { + openContextMenu(pos: number, eventArgs?: MouseEventInit): this { const ls = document.querySelectorAll('.reuse-tab__name'); if (pos > ls.length) { expect(false).withContext(`the pos muse be 0-${ls.length}`).toBe(true); diff --git a/packages/abc/reuse-tab/reuse-tab.component.ts b/packages/abc/reuse-tab/reuse-tab.component.ts index dfa2aa45b9..611a2a075d 100644 --- a/packages/abc/reuse-tab/reuse-tab.component.ts +++ b/packages/abc/reuse-tab/reuse-tab.component.ts @@ -160,14 +160,13 @@ export class ReuseTabComponent implements OnInit, OnChanges { ({ url: item.url, title: this.genTit(item.title), - closable: this.allowClose && item.closable && this.srv.count > 0, + closable: this.allowClose && this.srv.count > 0 && this.srv.getClosable(item.url, item._snapshot), position: item.position, index, active: false, last: false }) as ReuseItem ); - // debugger; const url = this.curUrl; let addCurrent = ls.findIndex(w => w.url === url) === -1; @@ -186,7 +185,10 @@ export class ReuseTabComponent implements OnInit, OnChanges { } if (addCurrent) { - ls.splice(this.pos + 1, 0, this.genCurItem()); + const addPos = this.pos + 1; + ls.splice(addPos, 0, this.genCurItem()); + // Attach to cache + this.srv.saveCache(this.route.snapshot, null, addPos); } ls.forEach((item, index) => (item.index = index)); @@ -256,9 +258,7 @@ export class ReuseTabComponent implements OnInit, OnChanges { if (!res) return; this.item = item; this.change.emit(item); - if (cb) { - cb(); - } + cb?.(); }); } diff --git a/packages/abc/reuse-tab/reuse-tab.service.spec.ts b/packages/abc/reuse-tab/reuse-tab.service.spec.ts index 909f960a30..b725dac834 100644 --- a/packages/abc/reuse-tab/reuse-tab.service.spec.ts +++ b/packages/abc/reuse-tab/reuse-tab.service.spec.ts @@ -17,7 +17,7 @@ class MockMenuService { } } class MockRouter { - navigateByUrl = jasmine.createSpy(); + navigateByUrl = jasmine.createSpy().and.returnValue(Promise.resolve(true)); get events(): NzSafeAny { return { subscribe: () => { @@ -69,7 +69,7 @@ describe('abc: reuse-tab(service)', () => { Array(count) .fill({}) .forEach((_item: NzSafeAny, index: number) => { - srv.store(getSnapshot(index + 1, urlTpl), { a: 1 }); + srv.saveCache(getSnapshot(index + 1, urlTpl), { a: 1 }); }); } @@ -95,21 +95,21 @@ describe('abc: reuse-tab(service)', () => { }); it('should be close oldest page', () => { srv.max = 2; - srv.store(getSnapshot(1), {}); - srv.store(getSnapshot(2), {}); - srv.store(getSnapshot(3), {}); + srv.saveCache(getSnapshot(1), {}); + srv.saveCache(getSnapshot(2), {}); + srv.saveCache(getSnapshot(3), {}); expect(srv.count).toBe(2); - srv.store(getSnapshot(4), {}); + srv.saveCache(getSnapshot(4), {}); expect(srv.count).toBe(2); }); it('should be ingore close when all is not closable', () => { srv.max = 2; - srv.store(getSnapshot(1), {}); - srv.store(getSnapshot(2), {}); + srv.saveCache(getSnapshot(1), {}); + srv.saveCache(getSnapshot(2), {}); srv.items.forEach(i => (i.closable = false)); - srv.store(getSnapshot(3), {}); + srv.saveCache(getSnapshot(3), {}); expect(srv.count).toBe(3); - srv.store(getSnapshot(4), {}); + srv.saveCache(getSnapshot(4), {}); expect(srv.count).toBe(3); }); }); @@ -313,7 +313,7 @@ describe('abc: reuse-tab(service)', () => { destroy: jasmine.createSpy('destroy') } }; - srv.store(getSnapshot(3), instance); + srv.saveCache(getSnapshot(3), instance); srv.close('/a/3'); expect(instance.componentRef.destroy).toHaveBeenCalled(); }); @@ -377,17 +377,19 @@ describe('abc: reuse-tab(service)', () => { srv.refresh(true); }); describe('#replace', () => { - it('should be navigate to new url', () => { + it('should be navigate to new url', fakeAsync(() => { expect(router.navigateByUrl).not.toHaveBeenCalled(); srv.replace('/a/1'); + tick(); expect(router.navigateByUrl).toHaveBeenCalled(); - }); - it('should be closed current router after navigate to new url', () => { + })); + it('should be closed current router after navigate to new url', fakeAsync(() => { genCached(1, ''); expect(router.navigateByUrl).not.toHaveBeenCalled(); srv.replace('/b'); + tick(); expect(router.navigateByUrl).toHaveBeenCalled(); - }); + })); }); describe('#keepingScroll', () => { it('should get keepingScroll from service', () => { @@ -451,7 +453,7 @@ describe('abc: reuse-tab(service)', () => { it(`can't hit when remove current page`, () => { const snapshot = getSnapshot(1); expect(srv.shouldDetach(snapshot)).toBe(true); - srv.store(snapshot, {}); + srv.saveCache(snapshot, {}); srv.close(srv.getUrl(snapshot)); expect(srv.shouldDetach(snapshot)).toBe(false); }); @@ -465,18 +467,18 @@ describe('abc: reuse-tab(service)', () => { }); it(`should be store a new route`, () => { expect(srv.count).toBe(2); - srv.store(getSnapshot(3), {}); + srv.saveCache(getSnapshot(3)); expect(srv.count).toBe(3); }); it(`should be store a exists route`, () => { expect(srv.count).toBe(2); - srv.store(getSnapshot(1), {}); + srv.saveCache(getSnapshot(1)); expect(srv.count).toBe(2); }); it(`should be store a route when out of cache count`, () => { srv.max = 2; expect(srv.count).toBe(2); - srv.store(getSnapshot(3), { componentRef: {} }); + srv.saveCache(getSnapshot(3), { componentRef: {} }); expect(srv.count).toBe(2); }); it(`should be run _onReuseDestroy event hook`, () => { @@ -487,7 +489,9 @@ describe('abc: reuse-tab(service)', () => { } } }; - srv.store(getSnapshot(3), handle); + const snapshot = getSnapshot(3); + srv.saveCache(snapshot, handle); + srv.store(snapshot, handle); expect(handle.componentRef.instance._onReuseDestroy).toHaveBeenCalled(); }); }); @@ -537,7 +541,7 @@ describe('abc: reuse-tab(service)', () => { }; const snapshot = getSnapshot(3); // handle - srv.store(snapshot, handle); + srv.saveCache(snapshot, handle); // mock activate router srv.store(snapshot, null); tick(101); diff --git a/packages/abc/reuse-tab/reuse-tab.service.ts b/packages/abc/reuse-tab/reuse-tab.service.ts index cc63f57253..27d3a6a9e0 100644 --- a/packages/abc/reuse-tab/reuse-tab.service.ts +++ b/packages/abc/reuse-tab/reuse-tab.service.ts @@ -8,7 +8,7 @@ import { Router, ROUTER_CONFIGURATION } from '@angular/router'; -import { BehaviorSubject, Observable, timer, Unsubscribable } from 'rxjs'; +import { BehaviorSubject, Observable, take, timer, Unsubscribable } from 'rxjs'; import { Menu, MenuService } from '@delon/theme'; import { ScrollService } from '@delon/util/browser'; @@ -219,12 +219,16 @@ export class ReuseTabService implements OnDestroy { */ replace(newUrl: string): void { const url = this.curUrl; - if (this.exists(url)) { - this.close(url, true); - } else { - this.removeUrlBuffer = url; - } - this.injector.get(Router).navigateByUrl(newUrl); + this.injector + .get(Router) + .navigateByUrl(newUrl) + .then(() => { + if (this.exists(url)) { + this.close(url, true); + } else { + this.removeUrlBuffer = url; + } + }); } /** * 获取标题,顺序如下: @@ -437,13 +441,48 @@ export class ReuseTabService implements OnDestroy { return this.can(route); } + saveCache(snapshot: ActivatedRouteSnapshot, _handle?: NzSafeAny, pos?: number): void { + const snapshotTrue = this.getTruthRoute(snapshot); + const url = this.getUrl(snapshot); + const idx = this.index(url); + const item: ReuseTabCached = { + title: this.getTitle(url, snapshotTrue), + url, + closable: this.getClosable(url, snapshot), + _snapshot: snapshot, + _handle + }; + if (idx < 0) { + this.items.splice(pos ?? this.items.length, 0, item); + if (this.count > this._max) { + // Get the oldest closable location + const closeIdx = this.items.findIndex(w => w.url !== url && w.closable!); + if (closeIdx !== -1) { + const closeItem = this.items[closeIdx]; + this.remove(closeIdx, false); + timer(1) + .pipe(take(1)) + .subscribe(() => this._cachedChange.next({ active: 'close', url: closeItem.url, list: this.cached.list })); + } + } + } else { + this.items[idx] = item; + } + } + /** * 存储 */ store(_snapshot: ActivatedRouteSnapshot, _handle: NzSafeAny): void { const url = this.getUrl(_snapshot); const idx = this.index(url); - const isAdd = idx === -1; + if (idx === -1) return; + + if (_handle != null) { + this.saveCache(_snapshot, _handle); + } + + const list = this.cached.list; const item: ReuseTabCached = { title: this.getTitle(url, _snapshot), @@ -453,33 +492,24 @@ export class ReuseTabService implements OnDestroy { _snapshot, _handle }; - if (isAdd) { - if (this.count >= this._max) { - // Get the oldest closable location - const closeIdx = this.cached.list.findIndex(w => w.closable!); - if (closeIdx !== -1) this.remove(closeIdx, false); - } - this.cached.list.push(item); - } else { - // Current handler is null when activate routes - // For better reliability, we need to wait for the component to be attached before call _onReuseInit - const cahcedComponentRef = this.cached.list[idx]._handle?.componentRef; - if (_handle == null && cahcedComponentRef != null) { - timer(100).subscribe(() => this.runHook('_onReuseInit', cahcedComponentRef)); - } - this.cached.list[idx] = item; + // Current handler is null when activate routes + // For better reliability, we need to wait for the component to be attached before call _onReuseInit + const cahcedComponentRef = list[idx]._handle?.componentRef; + if (_handle == null && cahcedComponentRef != null) { + timer(100) + .pipe(take(1)) + .subscribe(() => this.runHook('_onReuseInit', cahcedComponentRef)); } + list[idx] = item; this.removeUrlBuffer = null; - this.di('#store', isAdd ? '[new]' : '[override]', url); + this.di('#store', '[override]', url); if (_handle && _handle.componentRef) { this.runHook('_onReuseDestroy', _handle.componentRef); } - if (!isAdd) { - this._cachedChange.next({ active: 'override', item, list: this.cached.list }); - } + this._cachedChange.next({ active: 'override', item, list }); } /** diff --git a/src/dev/pages/page.component.ts b/src/dev/pages/page.component.ts index d3f644ad21..32a9e22413 100644 --- a/src/dev/pages/page.component.ts +++ b/src/dev/pages/page.component.ts @@ -1,10 +1,12 @@ import { JsonPipe } from '@angular/common'; -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, inject } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { format } from 'date-fns'; import { PageHeaderModule } from '@delon/abc/page-header'; +import { ReuseTabService } from '@delon/abc/reuse-tab'; +import { NzButtonModule } from 'ng-zorro-antd/button'; @Component({ selector: 'dev-page', @@ -13,17 +15,20 @@ import { PageHeaderModule } from '@delon/abc/page-header';

first: {{ first | json }},now: {{ now | json }}

id: {{ id | json }}

page: {{ route.url | json }} +
+ +
`, standalone: true, - imports: [PageHeaderModule, JsonPipe] + imports: [PageHeaderModule, JsonPipe, NzButtonModule] }) export class DevPageComponent implements OnInit { + readonly route = inject(ActivatedRoute); + readonly srv = inject(ReuseTabService); first = format(new Date(), 'yyyy-MM-dd HH:mm:ss'); now = format(new Date(), 'yyyy-MM-dd HH:mm:ss'); id = 0; - constructor(public route: ActivatedRoute) {} - ngOnInit(): void { this.route.params.subscribe(params => (this.id = +params.id)); } @@ -32,4 +37,8 @@ export class DevPageComponent implements OnInit { this.now = format(new Date(), 'yyyy-MM-dd HH:mm:ss'); console.log('by _onReuseInit', this.id); } + + replace(url: string): void { + this.srv.replace(url); + } } diff --git a/src/dev/router.ts b/src/dev/router.ts index dbf94ec4d8..e802cf446c 100644 --- a/src/dev/router.ts +++ b/src/dev/router.ts @@ -24,6 +24,7 @@ export const routes: Routes = [ { path: 'l7', component: DevPageComponent }, { path: 'l8', component: DevPageComponent }, { path: 'login', component: DevPageComponent }, + { path: 'view/1', component: DevPageComponent, data: { reuseClosable: false } }, { path: 'view/:id', component: DevPageComponent }, { path: 'lazy',