Aditya
Aditya

Reputation: 1261

Refresh a particular component in Angular

New to angular, so might sound a trivial question, however all solution provided on SO have failed to work so far. I have a simple login component wherein on submit, I redirect the user to profile page. I am able to send the user to the said component but the nav bar on top does not auto refresh, i.e. I have kept a session check, so when user logs in, the nav bar should automatically show the Logout button instead of Login/Register button. My code files are something like this:

login-page.component.html

<form #loginForm="ngForm" (ngSubmit)="loginUser(loginForm)" id="loginForm" class="loginbackground">
<input ngModel #emailAddress="ngModel" type="text" autocomplete="off" placeholder="Email" id="emailAddress" name="emailAddress" />
<button type="submit" id="submit">LOGIN</button>

login-page.component.ts

@Output() refreshEvent = new EventEmitter<any>();
loginUser(event) {
// Validations. If successful, proceed

const formData = event.value;
this.auth.loginUser(formData);
  .subscribe(data => {
    localStorage.setItem('loggedUser', JSON.stringify(data.userdata));
    // Form submit action here
    if (data.userdata.resMsg === 'Login failed') {
      this.errorPopup = true;
      this.errorText = 'Email Address and Password do not match';
    } else {
      this.refreshEvent.emit();
      this.emailAvailable = true;
      this.showLogin = false;
      this.showRegister = false;
      this.router.navigateByUrl('/404', { skipLocationChange: true }).then(() =>
        this.router.navigate(['user-profile']));
    }
  });
});
}

Problem
When I manually refresh the page, the nav bar reflects the changes as per written logic which works fine. However, I want this to happen when the user actually logs in and there shouldn't be a need to manually refresh the page.

What I've tried

Is there a way that this can be achieved cleanly?

Upvotes: 1

Views: 2673

Answers (2)

Stefan Szakacs
Stefan Szakacs

Reputation: 46

The solution for this is basic, you should use the most common features of Angular. I will get you through the thought process and then show you some sample code.

Thought process:

Problem: We need to know whether the user is logged in or not at all times.
Solution: We will have a service that tells us whether the user is logged in or not

Problem: The navigation bar should rely on the user's authentication status
Solution: We will use the status returned by the authentication service, to conditionally show one set of items or another set of items based on the user's authentication status

Code level issues:
You have certain issues within your code that will make your life difficult to further develop other features that rely on authentication status.

I've wrote two steps in how to improve your code, this first one is just improving the flow of your data and the code quality. The second step is completing the corrected code with a more dynamic data flow.


Step 1

Service
We will have a variable within the Authentication Service that tells us whether the user is already logged in or not:

private isUserLoggedIn: boolean = false;

We need to move all the authentication logic into the Authentication Service. Since I don't have the code for this.auth.loginUser(formData), I will call it myself from the new Authentication Service, but note, that the code from that function, should be in our new login function.
Also, there is no need to keep the HTTP call for login as an observable, as you only get one answer, so we can convert it to a promise with .toPromise().

The login function that calls the API will look like this:

private apiLogin(formData): Promise<any> {
        // the logic from your auth comes in here (use the content of this.auth.loginUser(formData) here)
        // let's presume that we got the response from your 'this.auth.loginUser(formData)' here as loginObservable 
        return new Promise((resolve, reject) => {
            this.auth.loginUser(formData);
                .toPromise()
                .then(data => {                   
                    // Form submit action here
                    if (data.userdata.resMsg === 'Login failed') {
                        // We clear the localStorage value, since the user is not logged in
                        localStorage.removeItem('loggedUser');
                        this.isUserLoggedIn = false;
                        reject('Email Address and Password do not match');
                    } else {
                         // We should update the localStorage                
                        localStorage.setItem('loggedUser', JSON.stringify(data.userdata));
                        this.isUserLoggedIn = true;
                        resolve();
                    }
                })
                .catch(error => {
                    this.isUserLoggedIn = false;
                    reject(error);
                });
        })
    }

We will also want to check if the user is logged by checking the localStorage (in case we want the user to not have to log in after each refresh):
The double negation !! tells us whether a value is truthy or falsy, so if we have something on the loggedUser key in the localStorage, we will take it as the user is logged in

// Check if the user is logged in by checking the localStorage
    private isAlreadyLoggedIn(): boolean {
        return !!localStorage.getItem('loggedUser');
    }

We will also need the login function that we invoke when we press the login button (we invoke it from the service, through the component):

public login(formData): Promise<any> {        
        // If the user is logged in, send a promise resolvation, otherwise, send the promise of the apiLogin
        if (this.isAlreadyLoggedIn) {
            return Promise.resolve();
        } else {
            return this.apiLogin(formData);
        }
}

And to make it complete, we will first check if the user is logged in (we do this by invoking isAlreadyLoggedIn() in the constructor of the service. Also, we will have a public function through which we can check if the user is already logged in:

constructor() {
        // On initialization, check whether the user is already logged in or not
        this.isUserLoggedIn = this.isAlreadyLoggedIn()
}
public isLoggedIn(): boolean {
        return this.isUserLoggedIn;
}

The complete service looks like this:

@Injectable()
export class AuthService {
    private isUserLoggedIn: boolean = false;

    constructor() {
        // On initialization, check whether the user is already logged in or not
        this.isUserLoggedIn = this.isAlreadyLoggedIn()
    }

    public login(formData): Promise<any> {        
        // If the user is logged in, send a promise resolvation, otherwise, send the promise of the apiLogin
        if (this.isAlreadyLoggedIn) {
            return Promise.resolve();
        } else {
            return this.apiLogin(formData);
        }
    }

    public isLoggedIn(): boolean {
        return this.isUserLoggedIn;
    }

    // Check if the user is logged in by checking the localStorage
    private isAlreadyLoggedIn(): boolean {
        return !!localStorage.getItem('loggedUser');
    }

    // Use this function to check if the user is already logged in

    // Use this function to login on the server
    private apiLogin(formData): Promise<any> {
        // the logic from your auth comes in here (use the content of this.auth.loginUser(formData) here)
        // let's presume that we got the response from your 'this.auth.loginUser(formData)' here as loginObservable 
        return new Promise((resolve, reject) => {
            this.auth.loginUser(formData);
                .toPromise()
                .then(data => {                   
                    // Form submit action here
                    if (data.userdata.resMsg === 'Login failed') {
                        // We clear the localStorage value, since the user is not logged in
                        localStorage.removeItem('loggedUser');
                        this.isUserLoggedIn = false;
                        reject('Email Address and Password do not match');
                    } else {
                         // We should update the localStorage                
                        localStorage.setItem('loggedUser', JSON.stringify(data.userdata));
                        this.isUserLoggedIn = true;
                        resolve();
                    }
                })
                .catch(error => {
                    this.isUserLoggedIn = false;
                    reject(error);
                });
        })
    }
}

Login Component:
This will check if the user is already logged in on initialization, if it is, then we redirect the user to the profile page, otherwise we show the login form. (The HTML remains the same as you have it, I would also add an error into a span tag). Please note that there are missing properties that you have in your login.ts, I just did the authentication part, add the form related variables to make the component complete and functional.

@Component({
    selector: 'app-login'
})
export class LoginComponent implements OnInit {
    public isLoggedIn: boolean = false;
    public error: string = '';

    constructor(authService: AuthService, router: Router) {}

    ngOnInit() {
        this.isLoggedIn = this.authService.isLoggedIn();

        if (this.isLoggedIn) {
            this.router.navigate(['user-profile']);
        }
    }

    loginUser(event) {
        const formData = event.value;
        this.authService.login(formData)
            .then(res => this.router.navigate(['user-profile']))
            .catch(error => this.error = error);
    }
}

Navigation Component:
The component gets the login status of the user, and renders its items accordingly:

@Component({
    selector: 'app-nav'
})
export class NavComponent implements OnInit {
    public isLoggedIn: boolean = false;

    constructor(authService: AuthService) {}

    ngOnInit() {
        this.isLoggedIn = this.authService.isLoggedIn();        
    }
}

Nav Template:
ng-template is a container that we are going to show, in case the user is not logged in.

<ul *ngIf="isLoggedIn; else notLoggedIn">
    <li>Home</li>
    <li>Profile</li>
    <li>Log Out</li>
</ul>

<ng-template #notLoggedIn>
    <ul>
        <li>Home</li>
        <li>Log In</li>
    </ul>
</ng-template>

This approach would be the basic, using redirects.


Step 2

We can complete this now with a more dynamic way (although I would personally stick to redirects):

We will add the following variables to our service:

private subject = new Subject();
private observable = this.subject.asObservable();

What this does, is that we can subscribe to observable from any component, and from the service, we can pass data live through the subject to the observable's subscribers. More about these, here

Now, whenever we update the login status, we invoke the following:

this.subject.next(this.isUserLoggedIn);

And this way, all the subscribers will be notified of this change.

We need a function that returns the observable to which the components can subscribe:

public isLoggedInObservable(): Observable<boolean> {
        return this.observable;
}

And all that's left, is to subscribe to this observable from the components that need live updates regarding the authentication status, in our case, the nav component (within ngOnInit):

this.authService.isLoggedInObservable.subscribe(isLoggedIn => this.isLoggedIn = isLoggedIn);

This is how the final service looks like:

@Injectable()
export class AuthService {
    private isUserLoggedIn: boolean = false;
    private subject = new Subject();
    private observable = this.subject.asObservable();

    constructor() {
        // On initialization, check whether the user is already logged in or not
        this.isUserLoggedIn = this.isAlreadyLoggedIn()
    }

    public login(formData): Promise<any> {        
        // If the user is logged in, send a promise resolvation, otherwise, send the promise of the apiLogin
        if (this.isAlreadyLoggedIn) {
            return Promise.resolve();
        } else {
            return this.apiLogin(formData);
        }
    }

    public isLoggedIn(): boolean {
        return this.isUserLoggedIn;
    }

    public isLoggedInObservable(): Observable<boolean> {
        return this.observable;
    }

    // Check if the user is logged in by checking the localStorage
    private isAlreadyLoggedIn(): boolean {
        return !!localStorage.getItem('loggedUser');
    }

    // Use this function to check if the user is already logged in

    // Use this function to login on the server
    private apiLogin(formData): Promise<any> {
        // the logic from your auth comes in here (use the content of this.auth.loginUser(formData) here)
        // let's presume that we got the response from your 'this.auth.loginUser(formData)' here as loginObservable 
        return new Promise((resolve, reject) => {
            this.auth.loginUser(formData);
                .toPromise()
                .then(data => {                   
                    // Form submit action here
                    if (data.userdata.resMsg === 'Login failed') {
                        // We clear the localStorage value, since the user is not logged in
                        localStorage.removeItem('loggedUser');
                        this.isUserLoggedIn = false;
                        this.subject.next(this.isUserLoggedIn);
                        reject('Email Address and Password do not match');
                    } else {
                         // We should update the localStorage                
                        localStorage.setItem('loggedUser', JSON.stringify(data.userdata));
                        this.isUserLoggedIn = true;
                        this.subject.next(this.isUserLoggedIn);
                        resolve();
                    }
                })
                .catch(error => {
                    this.isUserLoggedIn = false;
                    reject(error);
                });
        })
    }
}

And this is how the final Nav Component looks like:

@Component({
    selector: 'app-nav'
})
export class NavComponent implements OnInit {
    public isLoggedIn: boolean = false;

    constructor(authService: AuthService) {}

    ngOnInit() {
        this.isLoggedIn = this.authService.isLoggedIn();   

        this.authService.isLoggedInObservable.subscribe(isLoggedIn => this.isLoggedIn = isLoggedIn);
    }
}

I hope this clarifies how the code should look like. As a recap, you should handle all your login within the service, and expose a boolean that you can get from any component, so that you know the status of the authentication and act based upon it, and with the observables, you will get the latest status at all times.

Upvotes: 1

Nico Velazquez
Nico Velazquez

Reputation: 29

This is how I solved it:

nav.component.html

...
<li *ngIf="!auth.isLoggedIn()">
  ...
</li>
<li *ngIf="auth.isLoggedIn()">
  ...
</li>
...

nav.component.ts

export class NavComponent implements OnInit {

  constructor(public auth: AuthService) {
  }
  ...

auth.service.ts

export class AuthService {
  ...
  public isLoggedIn() {
    return this.getId() !== null;
  }
  ...

In this last method, 'this.getId()' could be to get the token from localStorage.

Upvotes: 2

Related Questions