Skip to content

Commit 6a1f6a9

Browse files
NathanWalkersis0k0
authored andcommitted
feat(Modal): allow modal to be lazily loaded from a module on demand (#772)
This allows a NgComponent to be opened in a modal even if it was lazily loaded on demand. Example: https://github.com/NativeScript/tests-app-ng/blob/master/app/modal/lazy/lazy-load-modal.component.ts
1 parent 8d013e2 commit 6a1f6a9

File tree

11 files changed

+142
-82
lines changed

11 files changed

+142
-82
lines changed

Diff for: .travis.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,4 @@ deploy:
6161
api_key:
6262
secure: J88MqLAoZStZZ77AAf+wgaoZp+8zG3fOUHRneSe4x/yEzyUShS9SlGuq0TSkm9sJVX94iHJl1BQ4yjLshOPV9dkOg1+BB4PbsDTKPCAhPCZgpW7WKz6iImmuWHArchLIRtI1fp+UYi1+V6c7gLALQPY7qR2QJcDJdq1tdgORAyGySMis95ttVhnn6DWTBbs/ocu+IzgOyBSkIiZR0mGk7q/pmVQPy+XL5PQoyUOhD4MmvAAIeVr+XoZ5I8pAUwhi1/bZijXrzWe7LbXh8pTDlEWvYduzYYjJZqUrHiE/e1e8/DIPXGaBUQBj7LRxSqqO8AJXGeCg4DF1R9j4CSG5c0pAwQ/U6vOGu8duPEGaoKG5+HlrTav7gI/YbwFA5HKyh1uzQ5trZDJ4mMKUoB1+8/eL2cjLudtyBB2Kg28wH6f78A9mQC1EJcP7Jz3qJTSUyhczIvwSF8/EkD8xmeaoTi2e+4TNgf7pys1cp6c7m7zKZbvVy25lfyAfG1rCF5+rzKj+GnE9mtLaY6VvlKWjyxklh8hfRBC94TZ8K7PH0tmdgk2Jal+OCdm9FDdmNrBSC1G/gPS8PchtffIRprPhNAUfcVpdg0rlQ4dckbGRbB5UBgwHkpoKasaSTx/nO85AiK6USIYOIod19loXUBvN3QyHUX76w265UhmTnb8iojo=
6363
on:
64-
branch: master
64+
branch: master

Diff for: nativescript-angular/directives/dialogs.ts

+59-29
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,25 @@
11
import {
2-
ReflectiveInjector, ComponentFactoryResolver, ViewContainerRef,
3-
Type, Injectable, ComponentRef, Directive
2+
ComponentFactoryResolver,
3+
ComponentRef,
4+
Directive,
5+
Injectable,
6+
NgModuleRef,
7+
ReflectiveInjector,
8+
Type,
9+
ViewContainerRef,
410
} from "@angular/core";
11+
512
import { Page } from "tns-core-modules/ui/page";
613
import { View } from "tns-core-modules/ui/core/view";
14+
715
import { DetachedLoader } from "../common/detached-loader";
816
import { PageFactory, PAGE_FACTORY } from "../platform-providers";
917

1018
export interface ModalDialogOptions {
1119
context?: any;
1220
fullscreen?: boolean;
1321
viewContainerRef?: ViewContainerRef;
22+
moduleRef?: NgModuleRef<any>;
1423
}
1524

1625
export class ModalDialogParams {
@@ -20,42 +29,61 @@ export class ModalDialogParams {
2029
}
2130
}
2231

32+
interface ShowDialogOptions {
33+
containerRef: ViewContainerRef;
34+
context: any;
35+
doneCallback;
36+
fullscreen: boolean;
37+
pageFactory: PageFactory;
38+
parentPage: Page;
39+
resolver: ComponentFactoryResolver;
40+
type: Type<any>;
41+
}
42+
2343
@Injectable()
2444
export class ModalDialogService {
25-
public showModal(type: Type<any>, options: ModalDialogOptions): Promise<any> {
26-
if (!options.viewContainerRef) {
45+
public showModal(type: Type<any>,
46+
{viewContainerRef, moduleRef, context, fullscreen}: ModalDialogOptions
47+
): Promise<any> {
48+
if (!viewContainerRef) {
2749
throw new Error(
28-
"No viewContainerRef: Make sure you pass viewContainerRef in ModalDialogOptions.");
50+
"No viewContainerRef: " +
51+
"Make sure you pass viewContainerRef in ModalDialogOptions."
52+
);
2953
}
3054

31-
const viewContainerRef = options.viewContainerRef;
3255
const parentPage: Page = viewContainerRef.injector.get(Page);
33-
const resolver: ComponentFactoryResolver = viewContainerRef.injector.get(
34-
ComponentFactoryResolver);
3556
const pageFactory: PageFactory = viewContainerRef.injector.get(PAGE_FACTORY);
3657

37-
return new Promise((resolve) => {
38-
setTimeout(() => ModalDialogService.showDialog(
39-
type,
40-
options,
41-
resolve,
42-
viewContainerRef,
43-
resolver,
58+
// resolve from particular module (moduleRef)
59+
// or from same module as parentPage (viewContainerRef)
60+
const componentContainer = moduleRef || viewContainerRef;
61+
const resolver = componentContainer.injector.get(ComponentFactoryResolver);
62+
63+
return new Promise(resolve => {
64+
setTimeout(() => ModalDialogService.showDialog({
65+
containerRef: viewContainerRef,
66+
context,
67+
doneCallback: resolve,
68+
fullscreen,
69+
pageFactory,
4470
parentPage,
45-
pageFactory
46-
), 10);
71+
resolver,
72+
type,
73+
}), 10);
4774
});
4875
}
4976

50-
private static showDialog(
51-
type: Type<any>,
52-
options: ModalDialogOptions,
77+
private static showDialog({
78+
containerRef,
79+
context,
5380
doneCallback,
54-
containerRef: ViewContainerRef,
55-
resolver: ComponentFactoryResolver,
56-
parentPage: Page,
57-
pageFactory: PageFactory): void {
58-
81+
fullscreen,
82+
pageFactory,
83+
parentPage,
84+
resolver,
85+
type,
86+
}: ShowDialogOptions): void {
5987
const page = pageFactory({ isModal: true, componentType: type });
6088

6189
let detachedLoaderRef: ComponentRef<DetachedLoader>;
@@ -66,7 +94,7 @@ export class ModalDialogService {
6694
detachedLoaderRef.destroy();
6795
};
6896

69-
const modalParams = new ModalDialogParams(options.context, closeCallback);
97+
const modalParams = new ModalDialogParams(context, closeCallback);
7098

7199
const providers = ReflectiveInjector.resolve([
72100
{ provide: Page, useValue: page },
@@ -85,7 +113,7 @@ export class ModalDialogService {
85113
}
86114

87115
page.content = componentView;
88-
parentPage.showModal(page, options.context, closeCallback, options.fullscreen);
116+
parentPage.showModal(page, context, closeCallback, fullscreen);
89117
});
90118
}
91119
}
@@ -96,7 +124,9 @@ export class ModalDialogService {
96124
})
97125
export class ModalDialogHost { // tslint:disable-line:directive-class-suffix
98126
constructor() {
99-
throw new Error("ModalDialogHost is deprecated. Call ModalDialogService.showModal() " +
100-
"by passing ViewContainerRef in the options instead.");
127+
throw new Error("ModalDialogHost is deprecated. " +
128+
"Call ModalDialogService.showModal() " +
129+
"by passing ViewContainerRef in the options instead."
130+
);
101131
}
102132
}

Diff for: nativescript-angular/nativescript.module.ts

+17-10
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,25 @@ import "./polyfills/array";
88
import "./polyfills/console";
99

1010
import { CommonModule } from "@angular/common";
11-
import { NativeScriptRendererFactory } from "./renderer";
12-
import { DetachedLoader } from "./common/detached-loader";
13-
import { ModalDialogHost, ModalDialogService } from "./directives/dialogs";
1411
import {
1512
ApplicationModule,
1613
ErrorHandler,
14+
NO_ERRORS_SCHEMA,
15+
NgModule,
1716
RendererFactory2,
18-
NgModule, NO_ERRORS_SCHEMA,
17+
SystemJsNgModuleLoader,
1918
} from "@angular/core";
19+
20+
import { NativeScriptRendererFactory } from "./renderer";
21+
import { DetachedLoader } from "./common/detached-loader";
2022
import {
21-
defaultPageProvider,
23+
ModalDialogHost,
24+
ModalDialogService,
25+
} from "./directives/dialogs";
26+
import {
27+
defaultDeviceProvider,
2228
defaultFrameProvider,
23-
defaultDeviceProvider
29+
defaultPageProvider,
2430
} from "./platform-providers";
2531
import { NS_DIRECTIVES } from "./directives";
2632

@@ -35,13 +41,14 @@ export function errorHandlerFactory() {
3541
...NS_DIRECTIVES,
3642
],
3743
providers: [
38-
{ provide: ErrorHandler, useFactory: errorHandlerFactory },
44+
ModalDialogService,
45+
NativeScriptRendererFactory,
46+
SystemJsNgModuleLoader,
47+
defaultDeviceProvider,
3948
defaultFrameProvider,
4049
defaultPageProvider,
41-
defaultDeviceProvider,
42-
NativeScriptRendererFactory,
50+
{ provide: ErrorHandler, useFactory: errorHandlerFactory },
4351
{ provide: RendererFactory2, useClass: NativeScriptRendererFactory },
44-
ModalDialogService
4552
],
4653
entryComponents: [
4754
DetachedLoader,

Diff for: nativescript-angular/router.ts

-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import {
44
NO_ERRORS_SCHEMA,
55
Optional,
66
SkipSelf,
7-
SystemJsNgModuleLoader,
87
} from "@angular/core";
98
import { RouterModule, Routes, ExtraOptions } from "@angular/router";
109
import { LocationStrategy, PlatformLocation } from "@angular/common";
@@ -38,7 +37,6 @@ export type LocationState = LocationState;
3837
NativescriptPlatformLocation,
3938
{ provide: PlatformLocation, useClass: NativescriptPlatformLocation },
4039
RouterExtensions,
41-
SystemJsNgModuleLoader,
4240
],
4341
imports: [
4442
RouterModule,
+26-24
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import {
2-
Injectable,
32
Compiler,
3+
Injectable,
44
NgModuleFactory,
55
NgModuleFactoryLoader,
66
SystemJsNgModuleLoader,
7+
Type,
78
} from "@angular/core";
8-
99
import { path, knownFolders } from "tns-core-modules/file-system";
1010

1111
const SEPARATOR = "#";
@@ -14,48 +14,50 @@ const SEPARATOR = "#";
1414
export class NSModuleFactoryLoader implements NgModuleFactoryLoader {
1515
private offlineMode: boolean;
1616

17-
constructor(private compiler: Compiler, private ngModuleLoader: SystemJsNgModuleLoader) {
17+
constructor(
18+
private compiler: Compiler,
19+
private ngModuleLoader: SystemJsNgModuleLoader,
20+
) {
1821
this.offlineMode = compiler instanceof Compiler;
1922
}
2023

2124
load(path: string): Promise<NgModuleFactory<any>> {
22-
if (this.offlineMode) {
23-
return this.ngModuleLoader.load(path);
24-
} else {
25-
return this.loadAndCompile(path);
26-
}
25+
return this.offlineMode ?
26+
this.ngModuleLoader.load(path) :
27+
this.loadAndCompile(path);
2728
}
2829

2930
private loadAndCompile(path: string): Promise<NgModuleFactory<any>> {
30-
let {modulePath, exportName} = splitPath(path);
31+
const module = requireModule(path);
32+
return Promise.resolve(this.compiler.compileModuleAsync(module));
33+
}
34+
}
3135

32-
let loadedModule = global.require(modulePath)[exportName];
33-
checkNotEmpty(loadedModule, modulePath, exportName);
36+
function requireModule(path: string): Type<any> {
37+
const {modulePath, exportName} = splitPath(path);
3438

35-
return Promise.resolve(this.compiler.compileModuleAsync(loadedModule));
36-
}
39+
const loadedModule = global.require(modulePath)[exportName];
40+
checkNotEmpty(loadedModule, modulePath, exportName);
3741

42+
return loadedModule;
3843
}
3944

4045
function splitPath(path: string): {modulePath: string, exportName: string} {
41-
let [modulePath, exportName] = path.split(SEPARATOR);
42-
modulePath = getAbsolutePath(modulePath);
43-
44-
if (typeof exportName === "undefined") {
45-
exportName = "default";
46-
}
46+
const [relativeModulePath, exportName = "default"] = path.split(SEPARATOR);
47+
const absoluteModulePath = getAbsolutePath(relativeModulePath);
4748

48-
return {modulePath, exportName};
49+
return {modulePath: absoluteModulePath, exportName};
4950
}
5051

5152
function getAbsolutePath(relativePath: string) {
52-
return path.normalize(path.join(knownFolders.currentApp().path, relativePath));
53+
const projectPath = knownFolders.currentApp().path;
54+
const absolutePath = path.join(projectPath, relativePath);
55+
56+
return path.normalize(absolutePath);
5357
}
5458

55-
function checkNotEmpty(value: any, modulePath: string, exportName: string): any {
59+
function checkNotEmpty(value: any, modulePath: string, exportName: string): void {
5660
if (!value) {
5761
throw new Error(`Cannot find '${exportName}' in '${modulePath}'`);
5862
}
59-
60-
return value;
6163
}

Diff for: ng-sample/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,4 @@
6060
"scripts": {
6161
"tslint": "tslint --project tsconfig.json --config tslint.json"
6262
}
63-
}
63+
}

Diff for: tests/app/lazy-loaded.module.ts

+25-2
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,42 @@
1-
import { NgModule } from "@angular/core";
1+
import { Component, NgModule } from "@angular/core";
22
import { NativeScriptModule } from "nativescript-angular/nativescript.module";
33
import { NativeScriptRouterModule } from "nativescript-angular/router";
4+
import { ModalDialogParams } from "nativescript-angular/directives/dialogs";
5+
import { Page } from "ui/page";
46

7+
import { lazyLoadHooksLogProvider } from "./main";
58
import { SecondComponent } from "./second.component";
69

710
const routes = [
811
{ path: ":id", component: SecondComponent },
912
];
1013

14+
@Component({
15+
selector: "modal-lazy-comp",
16+
template: `<Label text="this is lazily loaded modal component"></Label>`
17+
})
18+
export class ModalLazyComponent {
19+
constructor(public params: ModalDialogParams, private page: Page) {
20+
page.on("shownModally", () => {
21+
const result = this.params.context;
22+
this.params.closeCallback(result);
23+
});
24+
}
25+
}
26+
1127
@NgModule({
12-
declarations: [SecondComponent],
28+
declarations: [
29+
SecondComponent,
30+
ModalLazyComponent
31+
],
32+
entryComponents: [ModalLazyComponent], // when lazily loaded and opened via modal on demand
1333
imports: [
1434
NativeScriptModule,
1535
NativeScriptRouterModule,
1636
NativeScriptRouterModule.forChild(routes)
37+
],
38+
providers: [
39+
lazyLoadHooksLogProvider
1740
]
1841
})
1942
export class SecondModule { }

Diff for: tests/app/main.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ const singlePageHooksLogProvider = { provide: HOOKS_LOG, useValue: singlePageHoo
4949
const multiPageHooksLog = new BehaviorSubject([]);
5050
const multiPageHooksLogProvider = { provide: HOOKS_LOG, useValue: multiPageHooksLog };
5151
const lazyLoadHooksLog = new BehaviorSubject([]);
52-
const lazyLoadHooksLogProvider = { provide: HOOKS_LOG, useValue: lazyLoadHooksLog };
52+
export const lazyLoadHooksLogProvider = { provide: HOOKS_LOG, useValue: lazyLoadHooksLog };
5353

5454
@NgModule({
5555
bootstrap: [

Diff for: tests/app/snippets/icon-font.component.html

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
<!-- >> icon-font-sample -->
22
<GridLayout>
33
<ListView [items]="glyphs">
4-
<template let-item="item">
4+
<ng-template let-item="item">
55
<StackLayout orientation="horizontal">
66
<Label [text]="item.icon" class="icon"></Label>
77
<Label [text]="item.code"></Label>
88
</StackLayout>
9-
</template>
9+
</ng-template>
1010
</ListView>
1111
</GridLayout>
1212
<!-- << icon-font-sample -->
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
<!-- >> list-view-template-selector -->
22
<ListView [items]="myItems" [itemTemplateSelector]="templateSelector">
3-
<template nsTemplateKey="header" let-item="item">
3+
<ng-template nsTemplateKey="header" let-item="item">
44
<header-component [data]="item"></header-component>
5-
</template>
5+
</ng-template>
66

7-
<template nsTemplateKey="item" let-item="item">
7+
<ng-template nsTemplateKey="item" let-item="item">
88
<item-component [data]="item"></item-component>
9-
</template>
9+
</ng-template>
1010
</ListView>
1111
<!-- << list-view-template-selector -->

0 commit comments

Comments
 (0)