Reputation: 14418
I'm developing an angular 7 app with a multi-tab layout. Each tab contains a component, that can reference other nested components.
When the user select a new / another tab, the components displayed on the current tab are destroyed (I'm not just hiding them and I've followed this pattern since the opened tabs could be many and it seems a faster solution to me). Since loading a component can be costly (possibly lot of data retrieved from API) I'm trying to persist each component state in the tab container (it stays alive for the entire application lifetime).
Everything is working as expected but I think I've used a lot of code to accomplish my needs and I hope that everything could be simpler (and less prone to errors).
Here below an extract about how I managed to do it. First of all I created an abstract class that every component in the tab should extend.
export abstract class TabbedComponent implements OnInit, OnDestroy {
abstract get componentName(): string;
tab: LimsTab;
host: TabComponent;
private _componentInitialized = false;
constructor(
private _host: TabComponent,
) {
this.host = _host;
}
get componentInitialized(): boolean {
return this._componentInitialized;
}
ngOnInit(): void {
this.tab = this.host.tab;
this.loadDataContext();
this._componentInitialized = true;
}
ngOnDestroy(): void {
this.saveDataContext();
}
get dataContext() {
return this.tab.dataContext;
}
protected abstract saveDataContext(): void;
protected abstract loadDataContext(): void;
}
loadDataContext
and saveDataContext
are implemented in the concrete component and contains the logic to save / retrieve the component instance values. Moreover I've used the _componentInitialized
to understand when the component has called its life cycle hook OnInit: components can receive @Inputs from their father and ngOnChanges
is triggered before OnInit
from what I saw.
Here an example of the concrete implementation:
@Component({
...
})
export class MyComponent extends TabbedComponent implements OnChanges {
private qualityLotTests: QualityLotTestListItem[];
private _selectedTest: QualityLotTestListItem;
@Input()
selectedQualityLot: string;
@Output()
selectedTest: EventEmitter<QualityLotTestListItem> = new EventEmitter<QualityLotTestListItem>();
constructor(_host: TabComponent, private qualityLotService: QualityLotService) {
super(_host);
}
get componentName(): string {
return 'QualityLotTestListComponent';
}
ngOnChanges(changes: SimpleChanges): void {
if (this.componentInitialized) {
// before calling loadData I need to restore previous data (if any)
// this allow me to undestrand if loaded data filters has changed
// I know I restored it if onInit has executed
// if componentInitialized -> onInit has been executed and input parameters passed from parent are changed
this.loadData();
}
}
protected saveDataContext(): void {
this.dataContext[this.componentName].selectedQualityLot = this.selectedQualityLot;
this.dataContext[this.componentName].state = this.state;
this.dataContext[this.componentName].qualityLotTests = this.qualityLotTests;
this.dataContext[this.componentName].selectedSampleTestIDs
= this.selectedSampleTestIDs;
this.dataContext[this.componentName]._selectedTest = this._selectedTest;
}
protected loadDataContext(): void {
let previouslySelectedQualityLot: string;
if (this.dataContext[this.componentName]) {
previouslySelectedQualityLot = this.dataContext[this.componentName].selectedQualityLot;
this.state = this.dataContext[this.componentName].state || this.state;
this.qualityLotTests = this.dataContext[this.componentName].qualityLotTests;
this._selectedTest = this.dataContext[this.componentName]._selectedTest;
} else {
this.dataContext[this.componentName] = {};
}
this.selectedTest.emit(this._selectedTest);
if (this.qualityLotTests && previouslySelectedQualityLot === this.selectedQualityLot) {
this.dataStateChange(<DataStateChangeEvent>this.state);
} else {
this.loadData();
}
}
loadData(): void {
if (this.selectedQualityLot) {
this.loading = true;
this.qualityLotService
.getQualityLotTests(this.selectedQualityLot)
.subscribe(
qualityLotTests => {
this.qualityLotTests = qualityLotTests;
this.gridData = process(this.qualityLotTests, this.state);
this.loading = false;
},
error => (this.errorMessage = <any>error)
);
} else {
this.qualityLotTests = null;
this.gridData = null;
this.state = {
skip: 0,
take: 10
};
this.selectedSampleTestIDs = [];
this.selectedTest.emit(null);
}
}
public dataStateChange(state: DataStateChangeEvent): void {
this.loading = true;
this.state = state;
this.gridData = process(this.qualityLotTests, this.state);
this.loading = false;
}
}
How do you think I can improve the code above?
I've loaded an extract of my app here https://stackblitz.com/edit/angular-tabbed-app
Upvotes: 5
Views: 4750
Reputation: 99
Can we see a Plunkr for for clarification?
I think there are three common patterns that you could take advantage to keep state without all the object inheritance.
Here you would get and store data in the container components and pass them to presentation components via @Input()
. You can destroy the presentation components here and not worry about losing state.
It's better to use the concept of router navigation to display the desired container component to not only leverage existing practices but let the user know where they are in the application at all times.
While there is somewhat of a learning curve if you've never used the Redux pattern, you can store your application state in one place, make it immutable and reference it from any component.
Upvotes: 2