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

Created on 18 Dec 2017  路  16Comments  路  Source: angular/angular-cli

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?

devkibuild-angular medium broken bufix

Most helpful comment

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.

All 16 comments

I'm experiment something related. In my case I've added the wildcard route after have created the app-shell.

{ path: '', redirectTo: '/home', pathMatch: 'full'},
{ path: '**', component: NotFoundComponent }

Then the app-shell component is not being show before redirecting to the home-page anymore, instead the NotFoundComponent is being showed before home.

I've also run into the same thing. Its as if the server build doesn't ignore the routes added in App module when you import it into AppServerModule.

I thought maybe adding a wild card in the server routes that redirected to the app-shell path would maybe help, but alas. It did not.

This might be a problem with @angular/platform-server since all the cli does is call renderModuleFactory
https://github.com/angular/angular-cli/blob/cfc58a02660d6159db8da20ceb1fbe18cfee2e45/packages/%40angular/cli/tasks/render-universal.ts#L30

Is there a way to unregister a route?

The problem lies in that both AppModule and AppShellModule register routes, and that routes defined in the AppShellModule somehow end up registered last, independent of import order.

The complete route config ends up looking like this:

[
  { ...otherRoutes },
  { path: '**', redirectTo: '/' },
  { path: 'shell', component: [] }
];

Because of this the shell route will never be matched due to the wildcard route that is registered before it.

Edit: the possible solution (changing import order) I posted earlier does not work unfortunately.

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

same here - mine is already configured that way and doesn't work:


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

@NgModule({
  imports: [
    RouterModule.forRoot(routes),
    ServerModule,
    AppModule
  ],
  bootstrap: [AppComponent],
  declarations: [AppShellComponent],
})
export class AppServerModule {}

That didn't help me either. For some context, here's my browser routes:

const routes: Routes = [
  {
    path: '',
    component: LayoutComponent,
    children: [
      {
        path: '',
        component: RouteAComponent,
        canActivate: [ AuthGuard ]
      },
      {
        path: '**',
        component: PageNotFoundComponent
      }
    ]
  }
];

So I was thinking that perhaps the top level path: '' was catching the 'app-shell' route so i changed my server routes to const routes: Routes = [ { path: '', children: [ {path: 'app-shell', component: AppShellComponent } ] } ];

That did not work regardless of whether AppModule was imported before or after RouterModule in AppServerModule

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.

@Manduro I just tried your override solution, and while the app-shell component now appears in index.html after running ng-build --prod, the not-found component is rendered on the home page when running ng-serve --prod.

@Manduro 's solution works for me as well. @Brocco should this be added to the blueprint? Or the documentation? I'd be happy to submit a PR for either depending on the "route" the team wants to take (See what I did there?)

@bellizio This is really late, but that is expected behavior. App-shell is not supposed to work for ng-serve even with --prod. To test it, you have to do a build, and then use something like http-server ./dist (assuming you have http-server installed)

@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?

Heya, is this still a problem in the latest CLI versions?

it's still an issue in the last CLI version. Same thing the app shell generate the page not found instead of the shell component. I still need to add the following code in the AppServerModule export class.

constructor(private router: Router) {
    this.router.resetConfig(routes);
}

my solution would be to split the app.module into two, one app.module that has everything except route, and a app.browser.module that imports it and adds the route, and of course the rest of the client app would use the app.browser.module... I did this once with universal to cut out the AnimationModule

But today, I just added the "shell" route to the routing module, just before the catch all, I thought I do not wish to lose my main components, so why not? It is just one extra component to the client side.

Yes, there's options to get around this issue. But, I only wanted to notice that it is still an issue in the last version of Angular CLI. @filipesilva @ayyash

Was this page helpful?
0 / 5 - 0 ratings

Related issues

IngvarKofoed picture IngvarKofoed  路  3Comments

daBishMan picture daBishMan  路  3Comments

MateenKadwaikar picture MateenKadwaikar  路  3Comments

rajjejosefsson picture rajjejosefsson  路  3Comments

JanStureNielsen picture JanStureNielsen  路  3Comments