Resetting Angular 2 App
Asked Answered
C

4

18

My Angular 2 app has a logout feature. We want to avoid doing a page reload if we can (i.e. document.location.href = '/';), but the logout process needs to reset the app so when another user logs in there's no residual data from the previous session.

Here's our main.ts file:

import 'es6-shim/es6-shim';
import './polyfills';    
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { ComponentRef, enableProdMode } from '@angular/core';
import { environment } from '@environment';
import { AppModule } from './app/app.module';

if (environment.production === true) {
    enableProdMode();
}

const init = () => {
  platformBrowserDynamic().bootstrapModule(AppModule)
  .then(() => (<any>window).appBootstrap && (<any>window).appBootstrap())
  .catch(err => console.error(err));
};

init();

platformBrowserDynamic().onDestroy(() => {
  init();
});

You can see that I'm trying to call the init() method when the application is destroyed. The logout method in our user-authentication.service initiates destroy:

logout() {   
  this.destroyAuthToken();  
  this.setLoggedIn(false);
  this.navigateToLogin()
  .then(() => {
    platformBrowserDynamic().destroy();
  });
}

This gives the following error:

The selector "app-root" did not match any elements

Any help appreciated.

Coriecorilla answered 5/4, 2017 at 14:40 Comment(1)
I think when you call platformBrowserDynamic() you will get a new platform. You may need to store a reference from the first time you call it and then call destroy() on that one.Delirium
C
35

I ended up figuring this out in the end. This could be done more simply than my implementation, but I wanted to keep the bootstrapping in main.ts rather than stick it in the service that requests the restart.

  1. Create a singleton that provides a way for Angular and non-Angular (main.ts) to communicate:

boot-control.ts:

import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
export class BootController {
  private static instance: BootController;
  private _reboot: Subject<boolean> = new Subject();
  private reboot$ = this._reboot.asObservable();

  static getbootControl() {
    if (!BootController.instance) {
      BootController.instance = new BootController();
    }
    return BootController.instance;
  }

  public watchReboot() {
    return this.reboot$;
  }

  public restart() {
    this._reboot.next(true);
  }
}
  1. Adjust main.ts to subscribe to the reboot request:

main.ts:

import { enableProdMode, NgModuleRef, NgModule } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
import { BootController } from './boot-control';

if (environment.production) {
  enableProdMode();
}

const init = () => {
  platformBrowserDynamic().bootstrapModule(AppModule)
  .then(() => (<any>window).appBootstrap && (<any>window).appBootstrap())
  .catch(err => console.error('NG Bootstrap Error =>', err));
}

// Init on first load
init();

// Init on reboot request
const boot = BootController.getbootControl().watchReboot().subscribe(() => init());
  1. Add NgZone to the service that triggers the logout:

user-auth.service.ts:

import { BootController } from '@app/../boot-control';
import { Injectable, NgZone } from '@angular/core';

@Injectable()
export class UserAuthenticationService {
    constructor (
        private ngZone: NgZone,
        private router: Router
    ) {...}

    logout() {
        // Removes auth token kept in local storage (not strictly relevant to this demo)
        this.removeAuthToken();

        // Triggers the reboot in main.ts        
        this.ngZone.runOutsideAngular(() => BootController.getbootControl().restart());

        // Navigate back to login
        this.router.navigate(['login']);
    }
}

The NgZone requirement is to avoid the error:

Expected to not be in Angular Zone, but it is!

Coriecorilla answered 2/8, 2017 at 9:28 Comment(7)
Great piece of code, worked really well. I think one addition to this would be to add a logout screen - putting if you rebootstrap on a button click, the entire application freezes for 2-3 seconds, not a great user experience.Macri
where is the destroy part in the answer ? it appears in the original question, but then disappeared. are you sure you dont have memory leakage problems ?Schoolgirl
Still works with Angular 8. Good Solution to clear the userdata on logout, but depends heavily on size of the application. calling appbootstrapModule again is a costly function. But in the most small to medium size application this will probably do the trick. Thank you.Subeditor
Still works in Angular 10. Could you please explain how it's work. I am new to angular.Impracticable
There is a caveat with this approach, every time the app is restarted all CSS styles are getting cloned and appended to the HEAD tag. This will result in some weird behaviors in the app, at least in my case.Hospers
Working great in Angular 11, also I changed a little bit the code in main.ts in order to destroy the platform before bootstraping it again. Don't know if that changes much but it felt the good thing to do !Yolanda
This always worked for me but now its getting into an infinite loopSubsist
P
13

I came across the same issue. A simpler way is to use location.reload()

The function in your App.component which is called when the user clicks the logout button should look like this.

logout() {
  //Auth Logout service call
  this.auth.logout();
  //Router Navigation to Login Page
  this.router.navigate(['login']);
  //Reload Angular to refresh components and prevent old data from loading up for a 
  //another user after login. This especially applies lazy loading cases. 
  location.reload(); 
}
Pool answered 31/1, 2019 at 3:5 Comment(1)
this does not work for me. i always get the question dialog. tested only on chrome though.Monteiro
V
1

I had the same problem when I updated a project from Angular 7 to Angular 14. The error

The selector "app-root" did not match any elements Would appear only on reset.

When destroyPlatform() or platformBrowserDynamic().destroy() is called it will also destroy the <app-root></app-root> element from the document's body. In version 7 the root tag is still present after destroyPlatform() or platformBrowserDynamic().destroy() are called, but in version 14 it is not, I don't know starting with which version this behavior emerged.

My solution to this was to check if an <app-root></app-root> element exists in the body, and create it if it doesn't. This is my main.ts:

import { destroyPlatform, enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
import { BootController } from './boot-controller';

if (environment.production) {
    enableProdMode();
}

const init = () => {
   const rootSelector = document.getElementsByTagName('app-root');
    // console.log(rootSelector);
    // console.log(document);
    if (!rootSelector || rootSelector.length === 0) {
        const root = document.createElement('app-root');
        document.body.appendChild(root);
    }
    platformBrowserDynamic()
        .bootstrapModule(AppModule)
        .catch((err) => console.error(err));
};

init();
const boot = BootController.getbootControl()
    .watchReboot()
    .subscribe(() => {
        destroyPlatform();
        init();
    });

BootController.ts:

import { Subject } from 'rxjs';

export class BootController {
    private static instance: BootController;
    private _reboot: Subject<boolean> = new Subject();
    private reboot$ = this._reboot.asObservable();

    static getbootControl() {
        if (!BootController.instance) {
            BootController.instance = new BootController();
        }
        return BootController.instance;
    }

    public watchReboot() {
        return this.reboot$;
    }

    public restart() {
        this._reboot.next(true);
    }
}

and the logout handler :

logOut(event?: MouseEvent) {
        if (event) {
            event.stopPropagation();
        }
        this.resetToken();
        this.router.navigate(['/login']);
        this.ngZone.runOutsideAngular(() => BootController.getbootControl().restart());
    }
Ventriloquism answered 6/7, 2022 at 11:31 Comment(0)
R
0

@benh80, thanks for the proposed solution.

I wanted to reset lady loaded modules, once the user's password expired, thus triggering assigned canLoad guard. This solution made it possible.

Btw, I modified the proposed solution, to work with latest Angular 17 version.

  1. Create app-boot.service.ts inside app directory.

app-boot.service.ts:

import { BehaviorSubject, Observable } from 'rxjs';

export class AppBootService {
  private static instance: AppBootService;
  private reboot: BehaviorSubject<boolean> = new BehaviorSubject(true);
  private reboot$ = this.reboot.asObservable();

  public static getBootControl(): AppBootService {
    if (!AppBootService.instance) {
      AppBootService.instance = new AppBootService();
    }
    return AppBootService.instance;
  }

  public watchReboot(): Observable<boolean> {
    return this.reboot$;
  }

  public restart() {
    this.reboot.next(true);
  }
}
  1. Modify main.ts to subscribe to the reboot request:

main.ts:

// Bootstrap app initially and on subsequential reboot requests
AppBootService.getBootControl()
  .watchReboot()
  .subscribe(() => {
    platformBrowserDynamic()
      .bootstrapModule(AppModule)
      .then(() => (<any>window).appBootstrap && (<any>window).appBootstrap())
      .catch((err) => console.error('Angular Bootstrap Error =>', err));
  });
  1. Modify account.service.ts logout() method to trigger reboot action.

account.service.ts:

  logout() {
    // Clears the local storage states
    this.tokenService.reset();
    this.sidebarService.reset();
    this.chartService.reset();
    this.accountStore.reset();

    // https://mcmap.net/q/651088/-resetting-angular-2-app
    // Triggers the reboot in main.ts
    // This forces Angular application to reboot, forcing lazy loaded module to reload
    // This is necessary because of password expiration routing
    this.ngZone.runOutsideAngular(() => AppBootService.getBootControl().restart());

    // Navigate back to login
    this.router.navigate(['/login']);
  }
Reggiereggis answered 27/11, 2023 at 13:24 Comment(1)
@Hospers has got the point, the caveat of duplicating <style> tags inside <head> section after each reboot, remains an issue.Reggiereggis

© 2022 - 2024 — McMap. All rights reserved.