#Create a generic component to handle some recurrent UI logics.

31 messages Β· Page 1 of 1 (latest)

proven meadow
#

That component should:

  • display a loading when the data is loading
  • display an error message when the data retrieval failed
  • display a message when the data retrieved is empty (empty object or empty array for instance)
  • display the retrieved data in a child component wrapped in the "generic" component
  • be a dumb component (it should be driven by the parent (smart) component)

The idea is to create a wrapper that I can use to wrap any content that should be displayed after the data is loaded and ready.

#

I have created a POC. Here are my tests so far:The component:

export interface MyData {
  id: number;
  label: string;
}

export class AppComponent implements OnInit, OnDestroy {
  private routeIdSimulation: number = 1;
  private activatedRouteSubject = new BehaviorSubject<number>(
    this.routeIdSimulation
  );

  data$: Observable<MyData>;
  isDataLoading: boolean;
  isDataLoadingError: boolean;
  isDataLoaded: boolean;
  isData$: Observable<boolean>;

  private destroySubject = new Subject<void>();
  private destroyed$ = this.destroySubject.asObservable();

  ngOnInit() {
    // This is the simulation of an angular route change (cf. this.activatedRoute.params.pipe(...))
    const routeId$ = this.activatedRouteSubject
      .asObservable()
      .pipe(filter((id) => id != null));

    this.data$ = routeId$.pipe(
      tap(() => {
        this.isDataLoading = true;
        this.isDataLoadingError = false;
        this.isDataLoaded = false;
      }),
      switchMap((id) =>
        this.getAsyncData(id).pipe(
          finalize(() => {
            this.isDataLoading = false;
            this.isDataLoaded = true;
          }),
          catchError((error) => {
            this.isDataLoadingError = true;
            return of(null);
          }),
          takeUntil(this.destroyed$)
        )
      ),
      shareReplay()
    );
    this.isData$ = this.data$.pipe(map((data) => data != null));
  }

  ngOnDestroy() {
    this.destroySubject.next();
  }

  simulateRouteChange() {
    this.routeIdSimulation++;
    this.activatedRouteSubject.next(this.routeIdSimulation);
  }

  // Simulate a remote query
  private getAsyncData(id: number): Observable<MyData> {
    return of({
      id,
      label: `test ${id}`,
    }).pipe(
      delay(2000) // Simulate delay from backend
    );
  }
}
#

The view:

<!-- Test with plain html -->
<ng-container *ngIf="isDataLoading">Loading...</ng-container>
<ng-container *ngIf="isDataLoadingError">
  Error: Cannot load the data
</ng-container>
<ng-container *ngIf="data$ | async as data">
  <ng-container *ngIf="data != null; else empty">
    <pre>{{ data | json }}</pre>
  </ng-container>
</ng-container>
<ng-template #empty>No data</ng-template>

<!-- Test with component -->
<app-loading-container
  errorMessage="Error: Cannot load the data"
  emptyMessage="No data"
  [loading]="isDataLoading"
  [loaded]="isDataLoaded"
  [error]="isDataLoadingError"
  [isEmpty]="false"
>
  <pre>{{ data$ | async | json }}</pre>
</app-loading-container>

<input type="button" value="Get Data" (click)="simulateRouteChange()" />
#

The full code of this POC is available here: https://stackblitz.com/edit/angular-ivy-bdfu4b?file=src/app/app.component.html

You will probably notice that the following error appears in the console when the app is loading.

Error: NG0100: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'undefined'. Current value: 'true'...

I do understand why this error occurs but I'm in an egg or chicken problem -> The stream is subscribed inside the wrapper but the content should be shown only when the loading succeed. -> πŸ₯š πŸ”

Again, I understand that this cannot work properly but I can't find the right design for this problem.

How would you achieve this?
Any help would be appreciated

shell trout
#

since you use: changeDetection: ChangeDetectionStrategy.OnPush

the automatic detection is disabled

ExpressionChangedAfterItHasBeenCheckedError means that angular doesn't know what to do because it has 2 values and it doesn't know which one to display.

when you set a value, you have to force the detection with ChangeDetectorRef (to indicate this is the value to use)

#

or remove onPush

#
  • onPush is useful to disable the automatic detection and manage the detection by yourself. onPush is used only when you have a lot of data to manage on the screen and thus gain in performance
proven meadow
#

Thanks for your reply, but the changeDetection is not my main issue. Like I said, I understand why I have this error. As you can see, I made a test with a component (app-loading-container) and a test with plain html.

#

My main problem is that I want to find the right way to manage the UI logics in a generic way

mild hornet
#

"Create a generic component" just doesn't seem like the sort of thing I would do. Components, IMO, should be specific.

proven meadow
#

I probably didn't use the right words (English is not my mother tongue).
I just don't want to reinvent the wheel and create something that could manage the recurrent UI stuff in the views (display a loading, error, empty, ...).
We have this scenario in many places in our application.

mild hornet
#

loading:

<div *ngIf="vm$ | async as vm; else loading">
  <pre>For Debugging: {{ vm | json }}</pre>
</div>
<ng-template #loading><app-spinner></app-spinner></ng-template>

I know of no better pattern than this for handling loading.

proven meadow
#

Ok but loading is the first step.

#

But regarding the loading, the problem is that there could be several loadings. I mean what if there is a "refresh" button? The whole stack (call to api, variable that drives the UI) should be called again.
You can see in my POC (< https://stackblitz.com/edit/angular-ivy-bdfu4b?file=src/app/app.component.html >) that I'm trying to manage different scenarios. How about the data to be refreshed when a route param changed?

A angular-cli project based on @angular/animations, @angular/compiler, @angular/core, @angular/common, @angular/platform-browser-dynamic, @angular/forms, @angular/platform-browser, rxjs, tslib, zone.js and @angular/router.

mild hornet
#

generally is yes...

proven meadow
#

😁

mild hornet
#

I mean what if there is a "refresh" button?
Generally State Management is part of this solution. Have you looked at NgRx, NGXS or the like?

#

How about the data to be refreshed when a route param changed?
That is what Observables are for...

this.vm$ = this.route.paramMap.pipe(
proven meadow
#

And that's where things aren't easy as it seems

mild hornet
#

(because there is no need at all to use that technology in basic components)
Violent disagreement πŸ™‚

proven meadow
#

When build the new feature using NgRx. I create a component that could manage all that UI stuff. I then compose a new object from different selectors of the state and everything works perfectly.
I thought it was a good idea to use this component to replace all the repeated stuff in the rest of the application. But it's not that easy using "the old way".

#

As it seems a common problem IMHO, I thought I could get good advices from more experienced guys from this channel.

#

But I'm perfectly fine if you say that there is probably not a good solution for this case.

mild hornet
#

I'm saying the specifics matter and since you are only discussing generics I cannot give specific advice.
All the things you listed are things that you should solve specifically:

  1. display a loading when the data is loading
    I suggest as I showed
  2. display an error message when the data retrieval failed
    Do you want/need specific handling of error for specific data or are you just wanting little popups? A UserErrorHandlingService and Component possibly
  3. display a message when the data retrieved is empty (empty object or empty array for instance)
    Similar to 1
  4. display the retrieved data in a child component wrapped in the "generic" component
    I would never do that...
  5. be a dumb component (it should be driven by the parent (smart) component)
    Ok, then do that?
#

Your question is quite large and wide. We aren't your co-workers, we aren't building your app, we don't have your institutional knowledge.
IMPO, you should be having this conversation with your team, not strangers πŸ˜‰

proven meadow
#

Thanks a lot for your answer Rob.

  1. So far so good
  2. I just need to display a message configured in the component with the Input errorMessage. The message is displayed when the Input error is true
  3. same to 2 but with emptyMessage and isEmpty
  4. Ok thats a good point!
  5. It was just to state that the component does not do any custom logic
    Our requirements are always the same when it comes to display data (manage loading, error, empty) thats why I try to use the DRY concept here.
    I know the question is large. It's more a "design" question.
    you should be having this conversation with your team, not strangers -> The more I can say is that we are not talking about the same level of knowledge at all (sadly). That's why I'm trying to talk to experienced guys here.
#

But thanks a lot for your answer Rob!

mild hornet
#

The message is displayed when the Input error is true
https://formly.dev/ Would be how I would handle Forms in Angular

JSON powered / Dynamic forms in Angular

proven meadow
#

Sorry Rob but by input I mean @Input of the component