angularlayoutdata-bindingstorageangular2-changedetection

Angular change detection between layouts requires reload


I was required to update a project's UI without breaking any logic(I broke). In the old UI authentication(login page) and the rest(other resources that you get access to after authentication) were in the same layout(header + content + footer), but in the new one authentication page has a different layout than the applications layout(after logging in). So I separated layouts in such way:

RouterModule.forRoot([
      { path: 'auth', component: AuthLayoutComponent,
        children: [
          { path: '', component: AuthHomeComponent },
          { path: 'login', component: LoginComponent},
          { path: 'terms-of-use', component: TermsOfUseComponent},
          { path: 'contact', component: AuthContactFormComponent}
        ]
      },
      { path: '', component: HomeComponent,
        children: [
          { path: 'contact', component: ContactFormComponent },...]
bootstrap: [AppComponent]

HomeComponent is a layout for application while AuthLayoutComponent is layout for authentication part.

Old configuration:

RouterModule.forRoot([
      { path: '', component: HomeComponent, pathMatch: 'full' },

      { path: 'contact', component: ContactFormComponent },
      ...]
bootstrap: [AppComponent]

New AppComponent:

<ng-container>
  <router-outlet></router-outlet>
  <sac-alert></sac-alert>
  <sac-loader></sac-loader>
</ng-container>

Old AppComponent:

<app-header></app-header>
  <div class="body-content">
    <router-outlet></router-outlet>
  </div>
  <app-footer></app-footer>

New HomeComponent:

<div class="admin-container">
  <app-navigation></app-navigation>
  <div class="admin-section-container">
    <app-header></app-header>
    <main>
      <ng-container>
        <router-outlet></router-outlet>
      </ng-container>
    </main>
  </div>
</div>

Everything worked as expected, until I noticed that HeaderComponent doesn't show logged user's name and surname after logging in, while in the old version it would immediately appear on the right top corner. HeaderComponent:

<div ngbDropdown class="d-inline-block dropdown" *ngIf="isLoggedIn && user">
      <button type="button" class="btn" ngbDropdownToggle id="userDropdown">
        <img src="assets/icons/combo shape.svg" alt=""> <span *ngIf="user.surname">{{user.surname}}</span>
        <span *ngIf="user.name">{{user.name}}</span>
      </button>

HeaderComponent.ts:

constructor(
    private apiService: ApiService,
    public entityMapService: EntityMapService,
    public changeNotificationsService: ChangeNotificationsService,
    public router: Router,
    public cdr: ChangeDetectorRef
  ) {
    this.isProduction = environment.production === true;
    this.apiService.currentUserChanged.subscribe(user => {
      this.user = user;      
      this.isLoggedIn = this.user != null;
    });
    }

currentUserChanged in ApiService:

public currentUser: UserInfo;
  private currentUserChangeObservers: Observer<UserInfo>[] = [];
  public currentUserChanged = new Observable<UserInfo>(observer => {
    this.currentUserChangeObservers.push(observer);
  });

UserInfo is just a interface.

Now as I said in old version as soon as you logged in your name would appear in the right top corner of HeaderComponent, but after moving HeaderComponent to different layout without changing any of its typescript code, it doesn't show username and surname without a page reload(f5). So after logging in I need to refresh the page for changes to take place. Using console.log I saw that subscription logic in HeaderComponent is working well(user is not null), but when console.log(this.user) within ngOnInit I noticed that it gives null.

But in HeaderComponent's constructor:

this.apiService.currentUserChanged.subscribe(user => {
      this.user = user;      
      console.log(this.user)//gives actual object
      this.isLoggedIn = this.user != null;
    });
    }

After refresh it works fine, but it is not supposed to work that way, is it ?

I tried:

  1. Manual change detection with ChangeDetectionRef. detectChanges and markForCheck both were used.
  2. NgZone.run() in HeaderComponent. Run subscription logic within it to relfect changes.
  3. Applicatoin.ref() in HeaderComponent
  4. Wrote the same subscription logic in AfterViewInit, OnInit, NgDoCheck. Nothing worked.
  5. No ChangeDetectionStrategy is NOT set to OnPush anywhere in parent components.

After trying and testing the code I saw that when I moved the HeaderComponent into AuthLayoutComponent's children section(or just pasted it in AuthLayoutComponent) it worked without any problem with the same typescript. From here I decide that the issue is due to using different Layouts and children properties on Route configuration. I know the problem but I don't know how to resolve it while keeping the new UI. Anyone who knows why ChangeDetection doesn't work without refreshing page when different Layouts are used ?(services are shared so they should detect changes).

I tried to change RouteConfiguration to this, but ChangeDetection still requires page refresh:

const routes: Routes = [
  {
    path: '',
    children: [
      {
        path: 'auth',
        component: AuthLayoutComponent,
        children: [
          { path: '', redirectTo: 'login', pathMatch: 'full' }, 
          { path: 'login', component: LoginComponent },
          { path: 'terms-of-use', component: TermsOfUseComponent },
          { path: 'contact', component: AuthContactFormComponent }
        ]
      },
      {
        path: 'home',
        component: HomeComponent,
        children: [
        ]
      },
      { path: '', redirectTo: 'home', pathMatch: 'full' }, 
    ]
  },

Angular 17.3.0, Zone.js 0.14.4


Solution

  • I would set this up more like - using Subjects to create a global state management.

    // ApiService file
    @Injectable({providedIn: root})
    export class ApiService {
    
      // properties for the current user
      private _currentUser$ = new ReplaySubject<User>(1);
      public readonly currentUser$ = this._currentUser$.asObservable();
    
      // a method that updates the currentUser, wherever this gets called ...
      updateUserMethod(user: User) {
         this._currentUsers$.next(user);
      }
    }
    

    The above, is set as providedIn: root should be available everywhere. Then you can in your HeaderComponent constructor you can have:

    this.apiService.currentUser$.subscribe(user => {
      // do with the user what you need
      this.user = user;
    })