Search code examples
angulartypescriptrouter-outlet

Why is <app-root> rendering another <app-root> within it, which contains my routed component?


I have a lazy-loaded, multi-module Angular 7.2.2 application that seems to be creating a copy of the main AppComponent's template within itself, then rendering the routed component within that.

Here is my attempt at a complete but concise preamble (I can't reproduce this in a plunkr). Please do ask for any details I have missed.

The Preamble

I have an index.html file that contains ordinary HTML, and an <app-root> element within its <body>. This is the only place in the project that app-root tag appears. This file is referenced in angular.json's projects.app.architect.build.options.index property.

The main AppComponent is declared as

@Component({
  selector: 'app-root',
  template: `
    <router-outlet></router-outlet>
    <message-banner [show]="showMessage" [message]="message"></message-banner>
  `,
  providers: [AuthenticationService, AuthenticationGuard, MessagingService]
})

The rest of the logic in AppComponent simply mediates the show and message content for app-wide notifications. I have no router-outlet hooking and no direct DOM manipulation.

My main.ts bootstraps the application as usual

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

import { AppModule } from './app.module';
import { environment } from './environments/environment';

import { hmrBootstrap } from './hmr';

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

const bootstrap = () => platformBrowserDynamic().bootstrapModule(AppModule);

if (environment.hmr && module['hot']) {
  hmrBootstrap(module, bootstrap);
}
else {
  bootstrap().catch(err => console.log(err));
}

From the following AppModule

@NgModule({
  imports: [
    appRoutes,
    BrowserAnimationsModule,
    BrowserModule,
    CommonModule,
    FormsModule,
    HttpClientModule,
    HttpClientXsrfModule,
    ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }),
    SharedGenericModule
  ],
  declarations: [
    AppComponent,
    MessageBannerComponent
  ],
  providers: [
    { provide: APP_BASE_HREF, useValue: '/' },
    appRoutingProviders,
    ChoosePasswordGuard,
    LoadingBarService,
    Title,
    UserService
  ],
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  bootstrap: [AppComponent]
})

export class AppModule {}

Using these app.routes (small excerpt)

const routes: Routes = [
  {
    path: '',
    redirectTo: 'shop',
    pathMatch: 'full'
  },
  {
    path: '',
    component: AppComponent,
    children: [
      {
        path: 'shop',
        data: { key: 'shop' },
        loadChildren: "./modules/shop.module#ShopModule",
        pathMatch: 'full'
      },
      {
        path: 'car/:id',
        data: { key: 'car' },
        loadChildren: "./modules/car.module#CarModule",
        pathMatch: 'full'
      },
      {
        // This one has child routes, which I will add if needed
        path: 'workOrder',
        data: { key: 'workOrder' },
        loadChildren: "./modules/workOrder.module#WorkOrderModule",
      }
      // And many more
    ]
  }
]

export const appRoutes: ModuleWithProviders = RouterModule.forRoot(routes, {
  enableTracing: false,
  onSameUrlNavigation: 'reload'
});

The Problem

Once the application has stabilized, the resulting DOM looks like this:

<body>
  <app-root ng-version="7.2.2">

    <router-outlet></router-outlet>

    <app-root>
      <router-outlet></router-outlet>
      <default-routed-component _nghost-c0>...</default-routed-component>
      <message-banner ng-reflect-show="true" ng-reflect-message="[object Object]">...</message-banner>
    </app-root>

    <message-banner ng-reflect-show="true" ng-reflect-message="[object Object]">...</message-banner>

  </app-root>
</body>

Note that the inner <app-root> is not decorated with an ng-version.

Also, I can only programmatically communicate with the inner <message-banner>, the outer one seems disconnected. But messages generated before the first route is loaded (before the application stabilizes) are sent to both instances of the message banner.

Questions

Why is my app instantiating two <app-root>s with two <router-outlets>, but only using one of them. How can I prevent it from doing so?

I need to retain one app-wide <message-banner>, but I obviously want exactly one. Were it not for the duplication of that component, it might simply be an mysterious inconvenience - the app works perfectly well in all respects otherwise.


Solution

  • The answer is extremely obvious in retrospect.

    The main AppComponent - which contained the application's only <router-outlet> and common code I wanted to use throughout the application - was bootstrapped from the main AppModule and instantiated a second time by the Router as the default route's component, which then loaded feature modules as child routes.

    The fix was simply to remove the AppComponent and child routing; so to change app.routes.ts from

    const routes: Routes = [
      {
        path: '',
        redirectTo: 'shop',
        pathMatch: 'full'
      },
      {
        path: '',
        component: AppComponent,
        children: [
          {
            path: 'shop',
            data: { key: 'shop' },
            loadChildren: "./modules/shop.module#ShopModule",
            pathMatch: 'full'
          },
      ...
      }
    ]
    

    to

    const routes: Routes = [
      {
        path: '',
        redirectTo: 'shop',
        pathMatch: 'full'
      },
      {
        path: 'shop',
        data: { key: 'shop' },
        loadChildren: "./modules/shop.module#ShopModule",
        pathMatch: 'full'
      },
      ...
    ]