Angular删除数据后维持当前Tab选中状态的技术咨询
我使用mat-tab-group实现了多Tab切换功能,在第二个Tab中删除数据后,页面重新加载并自动切回第一个Tab。我需要实现删除操作后仍停留在当前Tab的效果。请问仅修改deleteViolation函数即可实现该需求,还是需要其他额外操作?
相关代码如下:
Tab组HTML代码
<div class="card-header" *ngIf="!loading"> <mat-tab-group (selectedTabChange)="changeActiveTab($event)"> <mat-tab *ngFor="let tab of topTabs" [label]="tab"> </mat-tab> </mat-tab-group> </div>
删除操作菜单HTML代码
<mat-menu #rowActions> <button mat-menu-item (click)="navigate(['/app/violations/detail/' + violation.id])">View</button> <button *ngIf="hasWriteAccess" mat-menu-item (click)="deleteViolation(violation)">Delete</button> </mat-menu>
TypeScript代码
export class UnitViolationListComponent implements OnInit, AfterViewInit { @Input() unitId: number = null; @Input() unit: Unit; searchValue: string = ''; // Tabs activeTab: string = 'All Outstanding'; topTabs: string[] = [ 'All Outstanding', 'Completed', ]; downloadingPdf: boolean = false; tags: any[] = []; unitTags: any[] = []; unitOrgTags: Tag[]; completeViolations: ViolationStatement[] = []; notCompleteViolations: ViolationStatement[] = []; violations: ViolationStatement[] = []; tableDataSource: MatTableDataSource<ViolationStatement> = new MatTableDataSource<ViolationStatement>(); displayedColumns: string[] = [ 'unit', 'title', 'createdAt', 'resolutionTime', 'completedTime', 'actions', ]; pageSizeOptions: number[] = [25, 50, 100, 200]; orgViolationStatuses: ViolationStatus[] = []; @ViewChild(MatTable) table: MatTable<any>; @ViewChild(MatPaginator) matpaginator: MatPaginator; @ViewChild(MatSort) sort: MatSort; // Component State uploading: boolean = false; loading: boolean = true; hasWriteAccess: boolean = false; _jwt: JwtLegFiClaims; constructor( private _dialog: MatDialog, private _fb: FormBuilder, private _growler: GrowlerService, private _router: Router, private _scrollService: ScrollService, private _violationsService: ViolationsService, private _csvExportService: CsvExportService, ) {} async ngOnInit() { this._scrollService.scrollToTop(); this._jwt = LegFiJwtService.read(); this.hasWriteAccess = LegFiJwtService.doesUserHaveModulePermission( 'violation', true, ); if (this.unitId) { this.displayedColumns = this.displayedColumns.filter(c => c !== 'unit'); } if (this._jwt !== null) { if (this._jwt.superUser || this._jwt.admin) { this.hasWriteAccess = true; } } await this.getOrgViolationStatuses(); this.getUnitViolations(); } ngAfterViewInit() { this.tableDataSource.sort = this.sort; this.tableDataSource.paginator = this.matpaginator; const originalFilterFunction = this.tableDataSource.filterPredicate; this.tableDataSource.filterPredicate = (data: ViolationStatement) => { if (originalFilterFunction(data.unit as any, this.searchValue)) { return true; } return originalFilterFunction(data, this.searchValue); }; } /** Get the available statuses for violations for this org */ async getOrgViolationStatuses() { await this._violationsService .getViolationStatusesPromise() .then( async (statuses: ViolationStatus[]) => { this.orgViolationStatuses = statuses; if (this.orgViolationStatuses.length) { this.displayedColumns.unshift('status'); this.topTabs = [ 'All Outstanding', ...this.orgViolationStatuses.map(s => s.title), 'Completed', ]; } }, (err: any) => { console.error('cant get template: ', err); }, ); } parseTableDataByStatus() { if (this.activeTab === 'Completed') { this.tableDataSource.data = this.completeViolations; } else if (this.activeTab === 'All Outstanding') { this.tableDataSource.data = this.notCompleteViolations; } else if (this.orgViolationStatuses.length) { this.tableDataSource.data = this.notCompleteViolations.filter(s => { return s.status === this.activeTab; }); } } getUnitViolations() { this.loading = true; this._violationsService .getUnitViolations(null, this.unitId) .pipe(untilDestroyed(this)) .subscribe(async (violations: ViolationStatement[]) => { this.completeViolations = violations.filter(v => v.completedTime); this.notCompleteViolations = violations.filter(v => !v.completedTime); this.parseTableDataByStatus(); this.updateFilter(); this.loading = false; }, () => { this.loading = false; this._growler.error('Error', 'There was an error loading violations for this unit.'); }); } /** * Trigger a re-filter when any of the things we filter by change */ updateFilter() { this.tableDataSource.filter = this.searchValue; if (this.tags.length > 0) { this.tableDataSource.filter += '//TAGS//'; } if (this.unitTags.length > 0) { this.tableDataSource.filter += '//UNITTAGS//'; } } changeActiveTab(event: MatTabChangeEvent) { this.activeTab = event.tab.textLabel; if (this.activeTab === 'Completed') { this.displayedColumns = [ 'unit', 'title', 'createdAt', 'resolutionTime', 'completedTime', 'actions', ]; } else { this.displayedColumns = [ 'unit', 'title', 'createdAt', 'resolutionTime', 'actions', ]; } if (this.unitId) { this.displayedColumns = this.displayedColumns.filter(c => c !== 'unit'); } if (this.orgViolationStatuses.length) { this.displayedColumns.unshift('status'); } this.parseTableDataByStatus(); this.updateFilter(); } /** * Navigate to Request Detail Page * @param {any[]} routerLink */ navigate(routerLink: any[]) { if (this._jwt !== null) { this._router.navigate(routerLink); } } deleteViolation(violation: ViolationStatement) { const baseDialog = this._dialog.open(ConfirmDeleteModalComponent, { width: MatDialogSizes.XS, data: 'violation', }); baseDialog.afterClosed().subscribe((confirmation: boolean) => { if (confirmation) { this._violationsService .deleteViolation([violation.id]) .subscribe(() => { this.getUnitViolations(); }); } }); } exportCsv() { const c = this.tableDataSource.filteredData.map((v: ViolationStatement) => { return new ViolationExportListItem(v); }); const options = { headers: [ 'status', 'unit', 'title', 'message', 'created', 'resolveBy', 'completed', 'address', 'city', 'state', 'zip', 'comments', ], showLabels: true, }; this._csvExportService.generateCsv(c, 'violation-export', options); } exportPdf() { this.downloadingPdf = true; this._violationsService.getUnitViolationListPdf(this.unitId, this.activeTab) .pipe(untilDestroyed(this)) .subscribe( response => { this._csvExportService.downloadFile(response, (this.unitId ? this.unitId + '-' : '') + this.activeTab + '-violations.pdf', 'application/pdf'); this.downloadingPdf = false; }, () => { this.downloadingPdf = false; }, ); } /** * Handle Toggle of Modals * @param {boolean} state * @param {string} modal */ toggleModal(state: boolean, modal: string) { this[modal] = state; } }
仅修改deleteViolation函数无法解决问题,需要额外调整HTML和组件逻辑。核心原因是当前代码中mat-tab-group通过*ngIf="!loading"控制显示:删除数据后调用getUnitViolations()时,loading会被设为true,导致Tab组被销毁;加载完成后loading设为false,Tab组重新渲染,但此时未绑定选中状态,默认会切回第一个Tab。
需要做以下两步修改:
1. 给mat-tab-group绑定选中索引
修改Tab组的HTML代码,添加[selectedIndex]绑定,根据当前activeTab计算对应的索引:
<div class="card-header" *ngIf="!loading"> <mat-tab-group (selectedTabChange)="changeActiveTab($event)" [selectedIndex]="getActiveTabIndex()"> <mat-tab *ngFor="let tab of topTabs" [label]="tab"> </mat-tab> </mat-tab-group> </div>
然后在组件中添加getActiveTabIndex()方法:
getActiveTabIndex(): number { return this.topTabs.findIndex(tab => tab === this.activeTab); }
2. 确保activeTab在Tab列表更新后依然有效(可选,增强健壮性)
由于getOrgViolationStatuses()会动态修改topTabs列表,若删除操作后当前activeTab不在新的topTabs中(比如自定义状态被删除的极端情况),需要设置回退值。在getUnitViolations()的订阅回调中添加检查:
getUnitViolations() { this.loading = true; this._violationsService .getUnitViolations(null, this.unitId) .pipe(untilDestroyed(this)) .subscribe(async (violations: ViolationStatement[]) => { this.completeViolations = violations.filter(v => v.completedTime); this.notCompleteViolations = violations.filter(v => !v.completedTime); // 检查activeTab是否存在于当前topTabs中,不存在则回退到默认值 if (!this.topTabs.includes(this.activeTab)) { this.activeTab = 'All Outstanding'; } this.parseTableDataByStatus(); this.updateFilter(); this.loading = false; }, () => { this.loading = false; this._growler.error('Error', 'There was an error loading violations for this unit.'); }); }
可选:优化deleteViolation函数的用户体验
目前deleteViolation函数逻辑正常,但可以添加成功/失败提示,提升用户体验:
deleteViolation(violation: ViolationStatement) { const baseDialog = this._dialog.open(ConfirmDeleteModalComponent, { width: MatDialogSizes.XS, data: 'violation', }); baseDialog.afterClosed().subscribe((confirmation: boolean) => { if (confirmation) { this._violationsService .deleteViolation([violation.id]) .subscribe(() => { this._growler.success('Success', 'Violation deleted successfully'); this.getUnitViolations(); }, () => { this._growler.error('Error', 'Failed to delete violation'); }); } }); }
核心问题是Tab组在加载时被销毁重建,导致选中状态丢失。通过绑定selectedIndex到当前activeTab的索引,就能确保重建后选中正确的Tab。仅修改deleteViolation无法解决这个问题,必须配合HTML和组件逻辑的调整。
内容的提问来源于stack exchange,提问作者developer8492

