Skip to content

Commit 94f2986

Browse files
authoredAug 14, 2022
feat(abc:reuse-tab): support status of the last browser closed (#1493)
1 parent 42b9a1e commit 94f2986

11 files changed

+177
-8
lines changed
 

‎packages/abc/reuse-tab/index.en-US.md

+1
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ Turning on `keepingScroll` will restore the previous scrollbar position after re
224224
| `[routeParamMatchMode]` | Match the pattern when routing parameters are included, for example:`/view/:id`<br> - `strict` Strict mode `/view/1`, `/view/2` Different pages<br> - `loose` Loose mode `/view/1`, `/view/2` Same page and only one tab of component | `strict,loose` | `strict` |
225225
| `[disabled]` | Whether to disabled | `boolean` | `false` |
226226
| `[titleRender]` | Custom rendering of the title | `TemplateRef<{ $implicit: ReuseItem }>` | - |
227+
| `[storageState]` | Whether to store the state, keep the last browser state | `boolean` | `false` |
227228
| `(close)` | Close callback event | `EventEmitter` | - |
228229
| `(change)` | Callback when switching | `EventEmitter` | - |
229230

‎packages/abc/reuse-tab/index.zh-CN.md

+1
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ export class DemoComponent implements OnReuseInit, OnReuseDestroy {
226226
| `[routeParamMatchMode]` | 包含路由参数时匹配模式,例如:`/view/:id`<br> - `strict` 严格模式 `/view/1``/view/2` 不同页<br> - `loose` 宽松模式 `/view/1``/view/2` 相同页且只呈现一个标签 | `strict,loose` | `strict` |
227227
| `[disabled]` | 是否禁用 | `boolean` | `false` |
228228
| `[titleRender]` | 自定义标题渲染 | `TemplateRef<{ $implicit: ReuseItem }>` | - |
229+
| `[storageState]` | 是否存储状态,保持最后一次浏览器的状态 | `boolean` | `false` |
229230
| `(close)` | 关闭回调 | `EventEmitter` | - |
230231
| `(change)` | 切换时回调,接收的参数至少包含:`active``list` 两个参数 | `EventEmitter` | - |
231232

‎packages/abc/reuse-tab/public_api.ts

+1
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ export { ReuseTabStrategy } from './reuse-tab.strategy';
88
export { ReuseTabModule } from './reuse-tab.module';
99
export * from './reuse-tab.interfaces';
1010
export * from './lifecycle_hooks';
11+
export * from './reuse-tab.state';

‎packages/abc/reuse-tab/reuse-tab.component.spec.ts

+13
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
} from './reuse-tab.interfaces';
2020
import { ReuseTabModule } from './reuse-tab.module';
2121
import { ReuseTabService } from './reuse-tab.service';
22+
import { REUSE_TAB_STORAGE_STATE } from './reuse-tab.state';
2223
import { ReuseTabStrategy } from './reuse-tab.strategy';
2324

2425
let i18nResult = 'zh';
@@ -644,6 +645,16 @@ describe('abc: reuse-tab', () => {
644645
}));
645646
});
646647
});
648+
649+
it('#storageState', fakeAsync(() => {
650+
layoutComp.storageState = true;
651+
page.cd();
652+
const stateSrv = TestBed.inject(REUSE_TAB_STORAGE_STATE);
653+
spyOn(stateSrv, 'update');
654+
page.to('#b');
655+
expect(stateSrv.update).toHaveBeenCalled();
656+
page.end();
657+
}));
647658
});
648659

649660
describe('[refresh]', () => {
@@ -846,6 +857,7 @@ class AppComponent {}
846857
[routeParamMatchMode]="routeParamMatchMode"
847858
[disabled]="disabled"
848859
[titleRender]="titleRender"
860+
[storageState]="storageState"
849861
(change)="change($event)"
850862
(close)="close($event)"
851863
>
@@ -871,6 +883,7 @@ class LayoutComponent {
871883
routeParamMatchMode: ReuseTabRouteParamMatchMode = 'strict';
872884
disabled = false;
873885
titleRender?: TemplateRef<{ $implicit: ReuseItem }>;
886+
storageState = false;
874887
change(): void {}
875888
close(): void {}
876889
}

‎packages/abc/reuse-tab/reuse-tab.component.ts

+15-1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import {
4040
ReuseTitle
4141
} from './reuse-tab.interfaces';
4242
import { ReuseTabService } from './reuse-tab.service';
43+
import { ReuseTabStorageState, REUSE_TAB_STORAGE_KEY, REUSE_TAB_STORAGE_STATE } from './reuse-tab.state';
4344

4445
@Component({
4546
selector: 'reuse-tab, [reuse-tab]',
@@ -64,6 +65,7 @@ export class ReuseTabComponent implements OnInit, OnChanges, OnDestroy {
6465
static ngAcceptInputType_allowClose: BooleanInput;
6566
static ngAcceptInputType_keepingScroll: BooleanInput;
6667
static ngAcceptInputType_disabled: BooleanInput;
68+
static ngAcceptInputType_storageState: BooleanInput;
6769

6870
@ViewChild('tabset') private tabset!: NzTabSetComponent;
6971
private destroy$ = new Subject<void>();
@@ -83,6 +85,7 @@ export class ReuseTabComponent implements OnInit, OnChanges, OnDestroy {
8385
@Input() excludes?: RegExp[];
8486
@Input() @InputBoolean() allowClose = true;
8587
@Input() @InputBoolean() keepingScroll = false;
88+
@Input() @InputBoolean() storageState = false;
8689
@Input()
8790
set keepingScrollContainer(value: string | Element) {
8891
this._keepingScrollContainer = typeof value === 'string' ? this.doc.querySelector(value) : value;
@@ -108,7 +111,9 @@ export class ReuseTabComponent implements OnInit, OnChanges, OnDestroy {
108111
@Optional() @Inject(ALAIN_I18N_TOKEN) private i18nSrv: AlainI18NService,
109112
@Inject(DOCUMENT) private doc: NzSafeAny,
110113
private platform: Platform,
111-
@Optional() private directionality: Directionality
114+
@Optional() private directionality: Directionality,
115+
@Optional() @Inject(REUSE_TAB_STORAGE_KEY) private stateKey: string,
116+
@Optional() @Inject(REUSE_TAB_STORAGE_STATE) private stateSrv: ReuseTabStorageState
112117
) {}
113118

114119
private genTit(title: ReuseTitle): string {
@@ -139,6 +144,7 @@ export class ReuseTabComponent implements OnInit, OnChanges, OnDestroy {
139144
url: item.url,
140145
title: this.genTit(item.title),
141146
closable: this.allowClose && item.closable && this.srv.count > 0,
147+
position: item.position,
142148
index,
143149
active: false,
144150
last: false
@@ -186,6 +192,12 @@ export class ReuseTabComponent implements OnInit, OnChanges, OnDestroy {
186192
this.srv.runHook('_onReuseInit', this.pos === item.index ? this.srv.componentRef : item.index, 'refresh');
187193
}
188194

195+
private saveState(): void {
196+
if (!this.srv.inited || !this.storageState) return;
197+
198+
this.stateSrv.update(this.stateKey, this.list);
199+
}
200+
189201
// #region UI
190202

191203
contextMenuChange(res: ReuseContextCloseEvent): void {
@@ -267,6 +279,7 @@ export class ReuseTabComponent implements OnInit, OnChanges, OnDestroy {
267279
this.tabset.nzSelectedIndex = pos;
268280
this.list = ls;
269281
this.cdr.detectChanges();
282+
this.saveState();
270283
}
271284

272285
// #endregion
@@ -321,6 +334,7 @@ export class ReuseTabComponent implements OnInit, OnChanges, OnDestroy {
321334
this.srv.keepingScroll = this.keepingScroll;
322335
this.srv.keepingScrollContainer = this._keepingScrollContainer;
323336
}
337+
if (changes.storageState) this.srv.storageState = this.storageState;
324338

325339
this.srv.debug = this.debug;
326340

‎packages/abc/reuse-tab/reuse-tab.interfaces.ts

+15-3
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,24 @@ export interface ReuseTabCached {
5454
/** 当前滚动条位置 */
5555
position?: [number, number] | null;
5656

57-
_snapshot: ActivatedRouteSnapshot;
57+
_snapshot?: ActivatedRouteSnapshot;
5858

59-
_handle: ReuseComponentHandle;
59+
_handle?: ReuseComponentHandle;
6060
}
6161

6262
export interface ReuseTabNotify {
6363
/** 事件类型 */
64-
active: 'add' | 'override' | 'title' | 'clear' | 'closable' | 'close' | 'closeRight' | 'move' | 'refresh';
64+
active:
65+
| 'add'
66+
| 'override'
67+
| 'title'
68+
| 'clear'
69+
| 'closable'
70+
| 'close'
71+
| 'closeRight'
72+
| 'move'
73+
| 'refresh'
74+
| 'loadState';
6575
url?: string;
6676
title?: ReuseTitle;
6777
item?: ReuseTabCached;
@@ -76,6 +86,8 @@ export interface ReuseItem {
7686
index: number;
7787
active: boolean;
7888
last: boolean;
89+
/** 当前滚动条位置 */
90+
position?: [number, number] | null;
7991
}
8092

8193
export interface ReuseContextEvent {

‎packages/abc/reuse-tab/reuse-tab.module.ts

+11
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,24 @@ import { ReuseTabContextMenuComponent } from './reuse-tab-context-menu.component
1212
import { ReuseTabContextComponent } from './reuse-tab-context.component';
1313
import { ReuseTabContextDirective } from './reuse-tab-context.directive';
1414
import { ReuseTabComponent } from './reuse-tab.component';
15+
import { ReuseTabLocalStorageState, REUSE_TAB_STORAGE_KEY, REUSE_TAB_STORAGE_STATE } from './reuse-tab.state';
1516

1617
const COMPONENTS = [ReuseTabComponent];
1718
const NOEXPORTS = [ReuseTabContextMenuComponent, ReuseTabContextComponent, ReuseTabContextDirective];
1819

1920
@NgModule({
2021
imports: [CommonModule, RouterModule, DelonLocaleModule, NzMenuModule, NzTabsModule, NzIconModule, OverlayModule],
2122
declarations: [...COMPONENTS, ...NOEXPORTS],
23+
providers: [
24+
{
25+
provide: REUSE_TAB_STORAGE_KEY,
26+
useValue: '_reuse-tab-state'
27+
},
28+
{
29+
provide: REUSE_TAB_STORAGE_STATE,
30+
useFactory: () => new ReuseTabLocalStorageState()
31+
}
32+
],
2233
exports: COMPONENTS
2334
})
2435
export class ReuseTabModule {}

‎packages/abc/reuse-tab/reuse-tab.service.spec.ts

+23-1
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ import { filter } from 'rxjs';
55
import { MenuService } from '@delon/theme';
66
import { NzSafeAny } from 'ng-zorro-antd/core/types';
77

8-
import { ReuseTabMatchMode, ReuseTitle } from './reuse-tab.interfaces';
8+
import { ReuseItem, ReuseTabMatchMode, ReuseTitle } from './reuse-tab.interfaces';
99
import { ReuseTabService } from './reuse-tab.service';
10+
import { ReuseTabLocalStorageState, REUSE_TAB_STORAGE_KEY, REUSE_TAB_STORAGE_STATE } from './reuse-tab.state';
1011
import { ReuseTabStrategy } from './reuse-tab.strategy';
1112

1213
class MockMenuService {
@@ -41,6 +42,14 @@ describe('abc: reuse-tab(service)', () => {
4142
useClass: ReuseTabStrategy,
4243
deps: [ReuseTabService]
4344
},
45+
{
46+
provide: REUSE_TAB_STORAGE_KEY,
47+
useValue: '_reuse-tab-state'
48+
},
49+
{
50+
provide: REUSE_TAB_STORAGE_STATE,
51+
useFactory: () => new ReuseTabLocalStorageState()
52+
},
4453
{ provide: ActivatedRoute, useValue: { snapshot: { url: [] } } },
4554
{ provide: Router, useFactory: () => new MockRouter() }
4655
].concat(providers)
@@ -530,4 +539,17 @@ describe('abc: reuse-tab(service)', () => {
530539
expect(handle.componentRef.instance._onReuseInit).toHaveBeenCalled();
531540
}));
532541
});
542+
543+
it('#storageState', () => {
544+
genModule();
545+
const stateSrv = TestBed.inject(REUSE_TAB_STORAGE_STATE);
546+
spyOn(stateSrv, 'get').and.returnValue([
547+
{ title: 'ti1', url: '/a' } as ReuseItem,
548+
{ title: 'ti2', url: '/b' } as ReuseItem,
549+
{ title: 'ti3', url: '/c' } as ReuseItem
550+
]);
551+
srv.storageState = true;
552+
srv.init();
553+
expect(srv.items.length).toBe(3);
554+
});
533555
});

‎packages/abc/reuse-tab/reuse-tab.service.ts

+22-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Injectable, Injector, OnDestroy } from '@angular/core';
1+
import { Inject, Injectable, Injector, OnDestroy, Optional } from '@angular/core';
22
import {
33
ActivatedRoute,
44
ActivatedRouteSnapshot,
@@ -24,6 +24,7 @@ import {
2424
ReuseTabRouteParamMatchMode,
2525
ReuseTitle
2626
} from './reuse-tab.interfaces';
27+
import { ReuseTabStorageState, REUSE_TAB_STORAGE_KEY, REUSE_TAB_STORAGE_STATE } from './reuse-tab.state';
2728

2829
@Injectable({ providedIn: 'root' })
2930
export class ReuseTabService implements OnDestroy {
@@ -43,6 +44,7 @@ export class ReuseTabService implements OnDestroy {
4344
mode = ReuseTabMatchMode.Menu;
4445
/** 排除规则,限 `mode=URL` */
4546
excludes: RegExp[] = [];
47+
storageState = false;
4648

4749
private get snapshot(): ActivatedRouteSnapshot {
4850
return this.injector.get(ActivatedRoute).snapshot;
@@ -365,11 +367,28 @@ export class ReuseTabService implements OnDestroy {
365367

366368
// #endregion
367369

368-
constructor(private injector: Injector, private menuService: MenuService) {}
370+
constructor(
371+
private injector: Injector,
372+
private menuService: MenuService,
373+
@Optional() @Inject(REUSE_TAB_STORAGE_KEY) private stateKey: string,
374+
@Optional() @Inject(REUSE_TAB_STORAGE_STATE) private stateSrv: ReuseTabStorageState
375+
) {}
369376

370377
init(): void {
371378
this.initScroll();
372379
this._inited = true;
380+
this.loadState();
381+
}
382+
383+
private loadState(): void {
384+
if (!this.storageState) return;
385+
386+
this._cached = this.stateSrv.get(this.stateKey).map(v => ({
387+
title: { text: v.title },
388+
url: v.url,
389+
position: v.position
390+
}));
391+
this._cachedChange.next({ active: 'loadState' });
373392
}
374393

375394
private getMenu(url: string): Menu | null | undefined {
@@ -385,7 +404,7 @@ export class ReuseTabService implements OnDestroy {
385404
): void {
386405
if (typeof comp === 'number') {
387406
const item = this._cached[comp];
388-
comp = item._handle.componentRef;
407+
comp = item._handle?.componentRef;
389408
}
390409
if (comp == null || !comp.instance) {
391410
return;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { NzSafeAny } from 'ng-zorro-antd/core/types';
2+
3+
import { ReuseItem } from './reuse-tab.interfaces';
4+
import { ReuseTabLocalStorageState } from './reuse-tab.state';
5+
6+
describe('abc: reuse-tab(state)', () => {
7+
const store = new ReuseTabLocalStorageState();
8+
const KEY = 'state';
9+
const VALUES = [
10+
{
11+
title: 'tit'
12+
} as ReuseItem
13+
];
14+
15+
beforeEach(() => {
16+
let data: { [key: string]: NzSafeAny } = {};
17+
18+
spyOn(localStorage, 'getItem').and.callFake((key: string): string => {
19+
return data[key] || null;
20+
});
21+
spyOn(localStorage, 'removeItem').and.callFake((key: string): void => {
22+
delete data[key];
23+
});
24+
spyOn(localStorage, 'setItem').and.callFake((key: string, value: string): string => {
25+
return (data[key] = value as string);
26+
});
27+
spyOn(localStorage, 'clear').and.callFake(() => {
28+
data = {};
29+
});
30+
});
31+
32+
it('should be working', () => {
33+
store.get(KEY);
34+
expect(store.get(KEY)).not.toBeNull();
35+
store.update(KEY, VALUES);
36+
const ret = store.get(KEY);
37+
expect(ret).not.toBeNull();
38+
expect(ret.length).toBe(VALUES.length);
39+
store.remove(KEY);
40+
expect(store.get(KEY).length).toBe(0);
41+
// when set null
42+
store.update(KEY, null as NzSafeAny);
43+
expect(store.get(KEY).length).toBe(0);
44+
});
45+
});
+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { InjectionToken } from '@angular/core';
2+
3+
import type { ReuseItem } from './reuse-tab.interfaces';
4+
5+
export const REUSE_TAB_STORAGE_KEY = new InjectionToken<string>('REUSE_TAB_STORAGE_KEY');
6+
7+
export const REUSE_TAB_STORAGE_STATE = new InjectionToken<ReuseTabStorageState>('REUSE_TAB_STORAGE_STATE');
8+
9+
export interface ReuseTabStorageState {
10+
get(key: string): ReuseItem[];
11+
12+
update(key: string, value: ReuseItem[]): boolean;
13+
14+
remove(key: string): void;
15+
}
16+
17+
export class ReuseTabLocalStorageState implements ReuseTabStorageState {
18+
get(key: string): ReuseItem[] {
19+
return JSON.parse(localStorage.getItem(key) || '[]') || [];
20+
}
21+
22+
update(key: string, value: ReuseItem[]): boolean {
23+
localStorage.setItem(key, JSON.stringify(value));
24+
return true;
25+
}
26+
27+
remove(key: string): void {
28+
localStorage.removeItem(key);
29+
}
30+
}

0 commit comments

Comments
 (0)
Please sign in to comment.