5

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

1 Answer 1

2

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.

  1. Container / Presentation (also known as smart and dumb) components

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.

  1. Router navigation

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.

  1. The most important one - state management library - I use NgRx but have heard good things about Akira and NGXS.

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.

Sign up to request clarification or add additional context in comments.

10 Comments

Thanks for your answer. I've added the code here: stackblitz.com/edit/angular-tabbed-app I have to say I'm new to angular and never used @ng-rx but I think it could be a next step after having found a correct approach to save / load. I think it could be considered as "where to save". For router I think I cannot use navigation properties following this pattern, moreover I want to save arrays and so on, not just navigation properties
Checking whether or not a component's data has loaded is somewhere NgRx really excels. The flow generally goes - on ngOnInit check the store to see if the data exists (generally by ID if it's a dynamic component), if it does store it in an observable - and if it doesn't dispatch an action to load the data into the store.
If you do end up using routing you can take advantage of Guards, which allow you to load data on route navigate. I'm not sure what you mean by navigation properties - are your components static like a settings page or dynamic like displaying information about an object?
Well, put in other words I don't see how can I use routing with my current implementation, or how can I change it in order to benefit of routing. Maybe I just need to be pointed in the right direction. Components are dynamic, contain grids loaded through a filter dialog for example, and when you select an element from a grid a child component receives it as an @Input loading itself related data. I can provide you a sample image if it could help
I suppose the navigation discussion doesn't pertain to your question as much as NgRx. As you seem to be more interested in improving your working code I recommend you take the opportunity to check it out.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.