Nativescript-angular: Is possible to use dynamic routing(lazy loading) feature of Angular2 in NativeScript + Angular2

Created on 14 Nov 2016  路  10Comments  路  Source: NativeScript/nativescript-angular

_From @wantaek on November 14, 2016 8:42_

Did you verify this is a real problem by searching [Stack Overflow]

Yes

Tell us about the problem

I want to use dynamic routing(lazy loading) feature of Angular2 in NativeScript + Angular2

This is a dynamic routing(lazy loading) of Angular2 sample source.
http://plnkr.co/edit/fBqrEpqafiVSB2Vvapzb?p=preview

I use this source to my NativeScript + Angular2 project.
But when i try to navigate 'lazy' page, error occurred.

CONSOLE ERROR file:///app/tns_modules/nativescript-angular/zone.js/dist/zone-nativescript.js:344:22: Error: Uncaught (in promise): ReferenceError: Can't find variable: System

Which platform(s) does your issue occur on?

I test in iOS. It may occur both.

Please provide the following version numbers that your issue occurs with:

  • CLI: (run tns --version to fetch it)
    2.4.0
  • Cross-platform modules: (check the 'version' attribute in the
    node_modules/tns-core-modules/package.json file in your project)
    2.4.0
  • Runtime(s): (look for the "tns-android" and "tns-ios" properties in the
    package.json file of your project)
    2.4.0
  • Plugin(s): (look for the version number in the package.json file of your
    project)

Please tell us how to recreate the issue in as much detail as possible.

When I try to navigate to 'lazy' page, error ocurred:

CONSOLE ERROR file:///app/tns_modules/nativescript-angular/zone.js/dist/zone-nativescript.js:344:22: Error: Uncaught (in promise): ReferenceError: Can't find variable: System

Is there code involved? If so, please share the minimal amount of code needed to recreate the problem.

import {Component} from '@angular/core';
import {Router} from '@angular/router';

@Component({
    selector: "my-app",
    // templateUrl: "app.component.html",
    template: `
        <StackLayout>
            <StackLayout class="nav">
                <Button text="First" 
                    [nsRouterLink]="['/']"></Button>
                <Button text="Second"
                    [nsRouterLink]="['/app']"></Button>
                <Button text="Lazy"
                    [nsRouterLink]="['/lazy']"></Button>
            </StackLayout>

            <router-outlet></router-outlet>
        </StackLayout>
    `
})
export class AppComponent {
    loaded: boolean = false;
    constructor(private router: Router) {}

    ngOnInit() {
        let routerConfig = this.router.config;

        if (!this.loaded) {
            routerConfig.unshift({
                path: `lazy`,
                loadChildren: 'app/portal.module#PortalModule'
            });

            console.log('/app route (Portal) added', JSON.stringify(routerConfig));

            this.router.resetConfig(routerConfig);
            this.loaded = true;
        }
    }
}

_Copied from original issue: NativeScript/NativeScript#3085_

question

Most helpful comment

For those of you who googled for Angular Router lazy loading and is wondering why you can't just use string-based routes like loadChildren: './secure/secure.module#SecureModule', well... you can! Just inject NSModuleFactoryLoader from nativescript-angular/router in your AppModule:

import { NSModuleFactoryLoader } from 'nativescript-angular/router';

@NgModule({
    ...
    providers: [
        { provide: NgModuleFactoryLoader, useClass: NSModuleFactoryLoader }
    ]
})
export class AppModule { }

NSModuleFactoryLoader is available since at least [email protected].

All 10 comments

Hey @wantaek

Yes, you can use lazy loading for routes - We are currently working on the lazy loading implementation in this branch of the nativescript-sdk-examples-ng. Once our implementation is ready, tested and clean we will update the master branch as well and you can use it as a reference for out project.

I will update the info here once we have a final solution.

@NickIliev, I saw that there was a commit (#530) to support lazy loading for the page-router-outlet. My use case is for lazy loading components using the standard router-outlet. It seems that @wantaek indicated that this was his use case also based on the example template he included even though he mentioned 'lazy' page. Will this also be supported?

Hey @fricker

Yes, you can already use router-outlet with lazy loading.
For example, if we take the nativescript-sdk-examples-ng application and look where the page-router-outlet is used you can replace it with router-outlet and again use the lazy loading for the inner examples.

e.g in app.component.ts.:
instead of this:

<GridLayout>
    <page-router-outlet></page-router-outlet>
</GridLayout>

you can change it like this:

<StackLayout>
    <StackLayout class="nav">
        <Button text="animations" [nsRouterLink]="['/animations']"></Button>
        <Button text="button" [nsRouterLink]="['/button']"></Button>
    </StackLayout>
    <router-outlet></router-outlet>
</StackLayout>

Both animations and button are sections which leads to three more sub-sections for which we are using lazy loading. If you open _app/ui-category/button_ you will notice a module file and a component file . In button-example.module.ts we are lazy loading the childrens of Button example like this:

import { NgModule } from "@angular/core";
import { NativeScriptRouterModule } from "nativescript-angular/router";
import { NativeScriptModule } from "nativescript-angular/platform";
import { ButtonExamplesComponent } from "./button-examples.component";
import { ButtonBindingTextComponent } from "./binding-text/binding-text.component";
import { ButtonTapEventComponent } from "./tap-event/tap-event.component";
import { ButtonTextComponent } from "./text/text.component";
import { NativeScriptFormsModule } from "nativescript-angular/forms";
import { TitleAndNavButtonModule } from "../../directives/title-and-nav-button.module";

export const routerConfig = [
    {
        path: '',
        component: ButtonExamplesComponent
    },
    {
        path: 'binding-text',
        component: ButtonBindingTextComponent,
        data: { title: "Binding text" }
    },
    {
        path: 'tap-event',
        component: ButtonTapEventComponent,
        data: { title: "Tap event" }
    },
    {
        path: 'text',
        component: ButtonTextComponent,
        data: { title: "Text" }
    }
];

@NgModule({
    imports: [TitleAndNavButtonModule, NativeScriptModule, NativeScriptRouterModule, NativeScriptFormsModule, NativeScriptRouterModule.forChild(routerConfig)],
    declarations: [ButtonExamplesComponent, ButtonBindingTextComponent, ButtonTapEventComponent, ButtonTextComponent]
})

export class ButtonExamplesModule {
    constructor() { }
}

Note that here we are using forChild method with NativeScriptRouterModule.

The final part where we are actually loading those children is placed in app.routes.ts.
Instead of cheating there paths for all of your component we are using loadChildren

    {
        path: "button",
        loadChildren: () => require("./ui-category/button/button-examples.module")["ButtonExamplesModule"],
        data: { title: "Button" }
    },

Again the whole architecture is virtually the same for page-router-outlet and for router-outlet except for the actual place where you are placing the outlets (in our case the app.component.ts)

@NickIliev,

Thank you very much for providing some much needed clarity to how loadChildren is supported in nativescript-angular. I was not aware Angular router's loadChildren could be configured using a call to require wrapped in a function. The Angular documentation that I have been referencing makes no mention of this. Additionally, there is no mention in the NativeScript documentation that loadChildren was supported or how it needs to be configured.

When I tried using the string syntax indicated in Angular's docs and got the same System not found error that @wantaek encountered. I even tried installing webpack to see if that might work. Now that I know that this alternate loadChildren configuration exists I went back to the web implementation of my app and changed it to use the wrapped require() implementation to maintain some consistency.

BTW, your link to the nativescript-sdk-examples-ng repo in your initial reply to @wantaek is broken. I'll definitely be using that project to supplement the NativeScript docs going forward.

@fricker Good to know that I've managed to help you guys! I have updated the link to our sample app - the lazy load is now implemented in the master branch.

Hi NickIliev,
Thank you for your post, it works perfectly
However, I tried to add "canActivate" as follow:

const APP_ROUTES: any = [
  { path: '', redirectTo: '/client', pathMatch: 'full'},
  { path: 'secure', loadChildren: () => require("./secure/secure.module")["SecureModule"], canActivate: [AuthGuard]},
  { path: 'signin', component: SigninComponent },
];

And it doesn't seem to work. As loadChildren is called asynchronously in your example, canActivate seems to resolve before and although the redirect I have in my AuthGuard component is fired correctly, the loadChildren seems to bypass my redirection. Any idea how to make it work?

@Injectable()
export class AuthGuard implements CanActivate {

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

  canActivate(): Observable<boolean> | boolean {
    if (!this.authService.isAuthenticated()) {
      this.router.navigate(['/signin']);
      return false;
    } else {
      return true;
    }
  }  

}

Hey @astrobob you can use canActivateChild

For example, I have tried it in our nativescript-sdk-examples-ng application like this:

  • create the guard (I've placed mine in the root dir as _auth.ts_)
import { CanActivateChild, RouterStateSnapshot, ActivatedRouteSnapshot } from "@angular/router";
import { Injectable } from "@angular/core";
import { Observable as RxObservable } from "rxjs";

export class UserToken {}
export class Permissions {
  canActivate(user: UserToken, id: string): boolean {
    return false; // or return true if you want to activate the route
  }
}
@Injectable()
export class AuthGuard implements CanActivateChild {
  constructor(private permissions: Permissions, private currentUser: UserToken) {}
  canActivateChild(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): RxObservable<boolean>|Promise<boolean>|boolean {
    return this.permissions.canActivate(this.currentUser, "action-bar");
  }
}
  • I've tested it for ActionBar examples like this:
    in _app.routes.ts_ added the canActivateChild
import { AuthGuard } from "./auth";
c
    {
        path: "action-bar",
        loadChildren: () => require("./ui-category/action-bar/action-bar-examples.module")["ActionBarExamplesModule"],
        canActivateChild: [AuthGuard],
        data: { title: "ActionBar" }
    },
  • and register the guard as providers for the respective NgModule in this case in _main.ts_
import { AuthGuard, UserToken, Permissions } from "./auth";

@NgModule({
    declarations: [
        // >> (hide)
        AppComponent,
        // << (hide)
    ],

    bootstrap: [AppComponent],
    imports: [
        NativeScriptModule,
        NativeScriptFormsModule,
        NativeScriptRouterModule,
        NativeScriptRouterModule.forRoot(routes),
    ],
    providers: [AuthGuard, UserToken, Permissions]
})

note that the above implementation is defaulted to return false (the route is not activated)

Hi @NickIliev

Thank you very much! It works perfectly! In didn't know about CanActivateChild

For those who want to see the code difference, I put below:

in my app.router:

const APP_ROUTES: any = [
  { path: '', redirectTo: '/client', pathMatch: 'full'},
  { path: 'secure', loadChildren: () => require("./secure/secure.module")["SecureModule"], canActivateChild: [AuthGuard]},
  { path: 'signin', component: SigninComponent },
];

in my auth.guard.ts (that I import in my app.router.ts)

import { Injectable } from "@angular/core";
import { CanActivateChild, ActivatedRouteSnapshot, RouterStateSnapshot } from "@angular/router";
import { Observable as RxObservable } from "rxjs/Rx";

import { AuthService } from "./auth.service";

@Injectable()
export class AuthGuard implements CanActivateChild {
  constructor(private authService: AuthService) {}
  canActivateChild(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): RxObservable<boolean>|Promise<boolean>|boolean {
    return this.authService.isAuthenticated();
  }
}

Cheers

For those of you who googled for Angular Router lazy loading and is wondering why you can't just use string-based routes like loadChildren: './secure/secure.module#SecureModule', well... you can! Just inject NSModuleFactoryLoader from nativescript-angular/router in your AppModule:

import { NSModuleFactoryLoader } from 'nativescript-angular/router';

@NgModule({
    ...
    providers: [
        { provide: NgModuleFactoryLoader, useClass: NSModuleFactoryLoader }
    ]
})
export class AppModule { }

NSModuleFactoryLoader is available since at least [email protected].

@otaran remember to import NgModuleFactoryLoader

import {NgModuleFactoryLoader} from "@angular/core";
Was this page helpful?
0 / 5 - 0 ratings