Vidar Kongsli
Vidar Kongsli

Reputation: 836

View not updated when model changed

I have an Angular (5.2.3) app where I want to display a login/logout button in the upper right corner of the page. Behind the scenes, I try to log the user in silently using an external Open ID Connect provider. When the callback arrives, I want to display the user's name and a logout button. But alas, the view is never updated to reflect this.

Here's the component's view:

<ul class="navbar-nav">
  <li class="nav-item" *ngIf="!loggedIn">
    <a href="#" class="nav-link" (click)="login()">Login</a>
  </li>
  <li class="nav-item" *ngIf="loggedIn">
    <a href="#" class="nav-link disabled" disabled>Hello, {{ name }}</a>
  </li>
  <li class="nav-item" *ngIf="loggedIn">
    <a href="#" class="nav-link" (click)="logoff()">Logout</a>
  </li>
</ul>
I have tried various approaches to resolve the problem, based on various questions on StackOverflow. Here is what the component looks like now:

import {
  Component,
  OnInit,
  SimpleChanges,
  ApplicationRef,
  ChangeDetectorRef,
  NgZone
} from '@angular/core';
import {
  OAuthService
} from 'angular-oauth2-oidc';
import {
  SessionService
} from '../session.service';

@Component({
  selector: 'app-navigation',
  templateUrl: './navigation.component.html',
  styleUrls: ['./navigation.component.css']
})
export class NavigationComponent implements OnInit {
  name: string;
  loggedIn: boolean;

  constructor(private oauthService: OAuthService,
    private sessionService: SessionService,
    private applicationRef: ApplicationRef,
    private zone: NgZone,
    private cd: ChangeDetectorRef) {
    //this.loggedIn = false;
  }

  ngOnInit() {
    this.sessionService.state.subscribe(isLoggedIn => {
      console.log('Detected a state change! User is logged in: ' + isLoggedIn);
      this.zone.run(() => {
        if (!isLoggedIn) {
          this.name = null;
          this.loggedIn = false;
          console.log('User not logged in. Name is set to null');
        } else {
          const claims: any = this.oauthService.getIdentityClaims();
          console.log(`Got claims. Name is now ${claims.name}`);
          this.name = claims.name;
          this.loggedIn = true;
        }
      });
      this.cd.detectChanges();
      this.applicationRef.tick();
    });
    this.sessionService.configureWithNewConfigApi();
  }

  public login() {}

  public logoff() {}
}
All the console.log calls are executed as expected, only the view is never updated.

Upvotes: 4

Views: 1058

Answers (5)

Maciej Treder
Maciej Treder

Reputation: 12342

Your problem is similar to another one, on which I placed answer in the past.

Trigger update of component view from service - No Provider for ChangeDetectorRef

What you need to do is force Angular to update your view. You can do that with ApplicationRef. This is how your service could look like:

import {Injectable, ApplicationRef } from '@angular/core';

@Injectable()
export class UserService {
  private isLoggedIn: boolean = false;
  constructor(private ref: ApplicationRef) {}

  checkLogin() {
    let stillLogged = //some logic making API calls, or anything other.
    if (this.isLoggedIn != stillLogged) {
        this.isLoggedIn = stillLogged;
        this.ref.tick(); //trigger Angular to update view.
    }
  }
}

In this documentation page you can find out more about ApplicationRef

Live example - here I am using AppRef to trigger view update when client subscribes to push on Safari browser.

You could try also move this.applicationRef.tick(); inside this.zone.run(()=>{}); (if you really need NgZone).

Upvotes: 1

Roger Jacob
Roger Jacob

Reputation: 360

You can run the code inside the ngZone so as to make the view updated according to the logged in status. Follow as mentioned below:

this.ngZone.run(() => {
  this.loggedIn = true;
});

So the code becomes as follows:

import {
Component,
OnInit,
SimpleChanges,
ApplicationRef,
ChangeDetectorRef,
NgZone } from '@angular/core';
import {  OAuthService} from 'angular-oauth2-oidc';
import { SessionService } from '../session.service';

@Component({
 selector: 'app-navigation',
 templateUrl: './navigation.component.html',
 styleUrls: ['./navigation.component.css']
})
export class NavigationComponent implements OnInit {
  name: string;
  loggedIn: boolean;

  constructor(private oauthService: OAuthService,
    private sessionService: SessionService,
    private zone: NgZone) {
  }

  ngOnInit() {
    this.sessionService.state.subscribe(isLoggedIn => {
      console.log('Detected a state change! User is logged in: ' + isLoggedIn);

        if (!isLoggedIn) {
          this.name = null;
          this.loggedIn = false;
          console.log('User not logged in. Name is set to null');
        } else {
          const claims: any = this.oauthService.getIdentityClaims();
          console.log(`Got claims. Name is now ${claims.name}`);
          this.name = claims.name;
          this.zone.run(() => {
          this.loggedIn = true;
          });
        }
    });
    this.sessionService.configureWithNewConfigApi();
  }

  public login() {}

  public logoff() {}
}

I hope this can solve your issue regarding the view updation.

Upvotes: 0

Giannis Faropoulos
Giannis Faropoulos

Reputation: 886

I can't seem to find any issues with the Code that your provided here. All i can think of is some kind of conflict with the ngIf directive. If all the console logs are as expected, all i can suggest is that you initialize the loggedIn value at the start and then try using an *ngIF else template in your Html file.

loggedIn: boolean = false;

And the html is this:

<ul class="navbar-nav">
    <li class="nav-item" *ngIf="!loggedIn; else loggedInTemplate">
        <a href="#" class="nav-link" (click)="login()">Login</a>
    </li>
    <ng-template #loggedInTemplate>
        <li class="nav-item">
          <a href="#" class="nav-link disabled" disabled>Hello, {{ name }}</a>
        </li>
        <li class="nav-item">
          <a href="#" class="nav-link" (click)="logoff()">Logout</a>
        </li>
    </ng-template>
  </ul>

Other than that, there isn't something i can actually detect here. Also i can't simulate the error because there is no code provided for the sessionService. Hope i helped.

Upvotes: 0

I think your error is to put all you have seen on the internet in order to detect changes etc and maybe something weird is happening. Can you just try this? It should work using only the detectChanges() method. It may not be the best solution, but it should work...

Better approach, if the subscribe() is inside the zone, will be putting a markForCheck()... but as I said, detectchanges alone should work.

@Component({
  selector: 'app-navigation',
  templateUrl: './navigation.component.html',
  styleUrls: ['./navigation.component.css']
})
export class NavigationComponent implements OnInit {
  name: string;
  loggedIn: boolean;

  constructor(private oauthService: OAuthService,
    private sessionService: SessionService,
    private applicationRef: ApplicationRef,
    private zone: NgZone,
    private cd: ChangeDetectorRef) {
    //this.loggedIn = false;
  }

  ngOnInit() {
    this.sessionService.state.subscribe(isLoggedIn => {
      console.log('Detected a state change! User is logged in: ' + isLoggedIn);
      if (!isLoggedIn) {
        this.name = null;
        this.loggedIn = false;
        console.log('User not logged in. Name is set to null');
      } else {
        const claims: any = this.oauthService.getIdentityClaims();
        console.log(`Got claims. Name is now ${claims.name}`);
        this.name = claims.name;
        this.loggedIn = true;
      }
      this.cd.detectChanges();
    });
    this.sessionService.configureWithNewConfigApi();
  }

  public login() {}

  public logoff() {}
}

Hope this helps.

Upvotes: 0

axl-code
axl-code

Reputation: 2274

This is my suggestion about I'll do it using observables.

In my service:

export class MyLoginService {
  loginSubject: ReplaySubject<any>;
  login$: Observable<any>;

  constructor(private http: HttpClient) {
    this.loginSubject = new ReplaySubject<any>();
    this.login$ = this.loginSubject.asObservable();
  }

  login(): void {
    // do your login logic and once is done successfully
    this.loginSubject.next({loggedIn: true});
  }
}

In my html:

<ul class="navbar-nav">
  <li class="nav-item" *ngIf="!(myLoginSrv.login$ | async)?.loggedIn">
    <a href="#" class="nav-link" (click)="login()">Login</a>
  </li>
  <li class="nav-item" *ngIf="(myLoginSrv.login$ | async)?.loggedIn">
    <a href="#" class="nav-link disabled" disabled>Hello, {{ name }}</a>
  </li>
  <li class="nav-item" *ngIf="(myLoginSrv.login$ | async)?.loggedIn">
    <a href="#" class="nav-link" (click)="logoff()">Logout</a>
  </li>
</ul>

In my component:

@Component({
  ...
})
export class MyComponent {
  constructor(public myLoginSrv: MyLoginService) {}

  ...
}

Upvotes: 0

Related Questions