4

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.
Maciej Treder
  • 11,866
  • 5
  • 51
  • 74
Vidar Kongsli
  • 826
  • 2
  • 9
  • 20
  • Can you share SessionService code? – axl-code Feb 02 '18 at 09:11
  • I am not sure if it is relevant to share all the code, but I think the key part is `state` which I subscribe to. It's definition is `public state = new EventEmitter();` – Vidar Kongsli Feb 02 '18 at 10:03
  • something to do with the zone is guess the cd is not picking up changes – Rahul Singh Feb 02 '18 at 10:26
  • If you initialize `name` and `loggedIn` with hard-coded values, and you comment out the code in `ngOnInit`, is the view showing the data correctly? And then if you change the values after a `setTimeout` in `ngOnInit`, without the `zone.run` command? – ConnorsFan Feb 05 '18 at 21:28
  • Another suggestion: the component may be created more than once. If the login event is caught by a first instance, and then another instance replaces it, name will not be set in this second one. You can: (1) put a `console.log` in the constructor to check that; (2) replace name with a property getter: `get name(): string { const claims: any = this.oauthService.getIdentityClaims(); return claims ? claims.name : null }` (set `loggedIn` to `true` for that test; you can also remove the "zone" stuff to simplify things). – ConnorsFan Feb 06 '18 at 01:56
  • You pointed me in the right direction, I think, @ConnorsFan. I added a `console.log` statement to the constructor of the navigation component. It is indeed constructed more than once. I have to figure out why... – Vidar Kongsli Feb 07 '18 at 15:23
  • You could subscribe to the `state` observable in `SessionService` and store `loggedIn` and `name` in the service. In the component, you would implement `loggedIn` and `name` as simple property getters, which would return the values stored in the service. Since the service is a singleton, you would not have to worry about the creation/destruction of the component. And you could remove all the zone and change detection stuff from your component code. – ConnorsFan Feb 07 '18 at 15:30
  • You could check if on a upper component change detection is not on push, or I don't know maybe you run angular without zones, also try to console.log(this) maybe the build tool does something weird and your context is not the right one, can you put this on a plunk or something so we can have a look over it it sounds interesting ? – Nicu Feb 11 '18 at 17:34

5 Answers5

1

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).

Maciej Treder
  • 11,866
  • 5
  • 51
  • 74
0

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) {}

  ...
}
axl-code
  • 2,192
  • 1
  • 14
  • 24
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.

0

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.

0

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.

Roger Jacob
  • 350
  • 2
  • 11