angular-cli: App Shell not generated when wildcard route (not found page) is present in app

Versions

Angular CLI: 1.6.1
Node: 8.9.0
OS: darwin x64
Angular: 5.1.1
... animations, common, compiler, compiler-cli, core, forms
... http, language-service, platform-browser
... platform-browser-dynamic, platform-server, router

@angular/cli: 1.6.1
@angular-devkit/build-optimizer: 0.0.36
@angular-devkit/core: 0.0.22
@angular-devkit/schematics: 0.0.42
@ngtools/json-schema: 1.1.0
@ngtools/webpack: 1.9.1
@schematics/angular: 0.1.11
@schematics/schematics: 0.0.11
typescript: 2.4.2
webpack: 3.10.0

Repro steps

  • Follow the steps to add an app shell to angular universal app as described here
  • Create a ‘not-found’ component with the following angular-cli command: ng generate component not-found --skip-import
  • Import and add the NotFoundComponent to app-routing.module.ts:
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { NotFoundComponent } from './not-found/not-found.component';

const routes: Routes = [
  { path: '**', component: NotFoundComponent }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }
  • Import and add NotFoundComponent to declarations array in app.module.ts
  • Run ng build --prod
  • Inspect dist/index.html

Observed behavior

dist/index.html includes the <app-not-found> tag for the NotFoundComponent. See the snippet below:

<router-outlet _ngcontent-c0=""></router-outlet><app-not-found _nghost-c1=""><p _ngcontent-c1="">
  not-found works!
</p>
</app-not-found>

Desired behavior

dist/index.html should include the <app-app-shell> tag for the AppShellComponent. See the snippet below:

<router-outlet _ngcontent-c0=""></router-outlet><app-app-shell _nghost-c1=""><p _ngcontent-c1="">
  app-shell works!
</p>
</app-app-shell>

Mention any other details that might be useful (optional)

Based on the documentation I could find about the App Shell feature introduced in Angular 5.1 (see here & here), I could not find anything detailing how the app shell should work when a wildcard route (aka ‘not found’ page) is present in an app’s routing config.

How should an app with a wildcard route be configured to support an app-shell and its accompanying route?

About this issue

  • Original URL
  • State: open
  • Created 7 years ago
  • Reactions: 17
  • Comments: 19 (3 by maintainers)

Commits related to this issue

Most upvoted comments

Sorry for what I posted earlier, I tested incorrectly and thought it was working. This time I investigated further and found a way to override the routes when rendering the app shell. Use the following workaround in your app.server.module.ts:

const routes: Routes = [{ path: 'shell', component: AppShellComponent }];

@NgModule({
  imports: [
    AppModule,
    ServerModule,
    RouterModule.forRoot(routes)
  ],
  bootstrap: [AppComponent],
  declarations: [AppShellComponent]
})
export class AppServerModule {
  // The important part:
  constructor(private router: Router) {
    this.router.resetConfig(routes);
  }
}

This removes all routes that are imported by your regular AppModule, and replaces it with the app shell route, so the wildcard route is not blocking it from loading.

@literalpie to your point, I think you are correct in that the ng serve --prod command can not be used to properly test app-shell in the browser. I just tried doing so and the app-shell is nowhere to be found.

You have to generate a build with the ng build --prod command, and use a static server like http-server to serve it.

After experimenting further, here is what ultimately worked for me:

app-routing.module.ts

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { NotFoundComponent } from './not-found/not-found.component';

const routes: Routes = [
  { path: '', redirectTo: '', pathMatch: 'full' },
  { path: '**', component: NotFoundComponent }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule {}

app.server.module.ts

const routes: Routes = [
  { path: 'app-shell-path', component: AppShellComponent }
];

@NgModule({
  imports: [AppModule, ServerModule, RouterModule.forRoot(routes)],
  bootstrap: [AppComponent],
  declarations: [AppShellComponent]
})
export class AppServerModule {
  constructor(private router: Router) {
    this.router.resetConfig(routes);
  }
}

Note the addition of { path: '', redirectTo: '', pathMatch: 'full' } in the routes. Although this is a simple solution and not something you would likely find in a real-world app, it is necessary for this example. Without it, Angular will not see the root url path (e.g. localhost:8080) as a valid route. And in that case, the not-found component will be rendered at the root url path (after app-shell first renders).

Additionally, this.router.resetConfig(routes); must be called in AppServerModule as @Manduro pointed out. Without it, app-shell is not added to index.html after ng build --prod is run and not-found is added instead.

In order to test app-shell in this example, it is best to use network throttling in Chrome or Firefox (I tested with 2G and 3G).

All this is to say that I think this should still remain as an open bug because of the requirement to call this.router.resetConfig(routes);, which is not documented. Perhaps as a fix for this, Angular could do this internally so that devs to not manually have to. Thoughts?

@Manduro I just tried your solution, but the issue still exists.

How shall we configure this for a standalone project? @Manduro thx ❤

app.config.server.ts file:

import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
import { provideServerRendering } from '@angular/platform-server';
import { appConfig } from './app.config';
import { ROUTES } from '@angular/router';
import { AppShellComponent } from './app-shell/app-shell.component';

const serverConfig: ApplicationConfig = {
  providers: [
    provideServerRendering(),
    {
      provide: ROUTES,
      multi: true,
      useValue: [
        {
          path: 'shell',
          component: AppShellComponent,
        },
      ],
    },
  ],
};

export const config = mergeApplicationConfig(appConfig, serverConfig);

I also experienced that if I use the APP_INITIALIZER and try to redirect to an error page (based on our logic), App-shell navigates to that error page and embeds that page into our HTML instead of embedding the app-shell page itself! How this can be resolved?!

app.config.ts file:

import { APP_INITIALIZER, ApplicationConfig, isDevMode } from '@angular/core';
import {
  Router,
  provideRouter,
  withEnabledBlockingInitialNavigation,
} from '@angular/router';
import { appRoutes } from './app.routes';

function initAppFactoryConfig(router: Router) {
  return () => new Promise((resolve, reject) => {

    // We resolve the promise whatsoever! Because we want the app to complete 
    // its initialization... BUT based on our logic (e.g., if the file we're 
    // trying to load at initialization, couldn't get loaded), we may like to 
    // redirect to one of our app's pages and then resolve... In this case 
    // app-shell embeds the error page into the HTML instead!
    router.navigate(['/error-loading']);
    resolve(true);
  });
}

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(appRoutes, withEnabledBlockingInitialNavigation()),
    {
      provide: APP_INITIALIZER,
      useFactory: initAppFactoryConfig,
      deps: [Router],
      multi: true,
    },
  ],
};

I did the this.router.resetConfig(routes); solution but instead of app-shell now nothing is presented in <app-root>. And CLI says that app-shell generated successfully.