Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit f0768cb

Browse files
committedFeb 10, 2016
Merge pull request #61 from NativeScript/page-router-outlet
Page router outlet and router directives
2 parents 1f16b62 + f202846 commit f0768cb

File tree

8 files changed

+584
-2
lines changed

8 files changed

+584
-2
lines changed
 

‎ng-sample/app/app.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,23 @@
77

88
// this import should be first in order to load some required settings (like globals and reflect-metadata)
99
import { nativeScriptBootstrap } from "./nativescript-angular/application";
10+
import { NS_ROUTER_PROVIDERS, routerTraceCategory } from "./nativescript-angular/router/ns-router";
11+
12+
import trace = require("trace");
13+
trace.setCategories(routerTraceCategory);
14+
trace.enable();
1015

1116
//import {RendererTest} from './examples/renderer-test';
1217
//import {Benchmark} from './performance/benchmark';
1318
//import {ListTest} from './examples/list/list-test';
14-
import {ListTestAsync} from "./examples/list/list-test-async";
19+
// import {ListTestAsync} from "./examples/list/list-test-async";
1520
// import {ImageTest} from "./examples/image/image-test";
21+
import {NavigationTest} from "./examples/navigation/navigation-test";
22+
1623

1724
//nativeScriptBootstrap(RendererTest);
1825
//nativeScriptBootstrap(Benchmark);
1926
//nativeScriptBootstrap(ListTest);
20-
nativeScriptBootstrap(ListTestAsync);
27+
// nativeScriptBootstrap(ListTestAsync);
2128
// nativeScriptBootstrap(ImageTest);
29+
nativeScriptBootstrap(NavigationTest, [NS_ROUTER_PROVIDERS]);
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import {Component} from 'angular2/core';
2+
import {OnActivate, OnDeactivate, LocationStrategy, RouteParams, ComponentInstruction } from 'angular2/router';
3+
import {topmost} from "ui/frame";
4+
import {NS_ROUTER_DIRECTIVES} from "../../nativescript-angular/router/ns-router";
5+
6+
7+
@Component({
8+
selector: 'example-group',
9+
directives: [NS_ROUTER_DIRECTIVES],
10+
template: `
11+
<GridLayout rows="auto, auto, auto" columns="*, *" margin="10">
12+
<Label [text]="'Componenet ID: ' + compId" colSpan="2" row="0"
13+
style="font-size: 30; horizontal-align: center; margin: 10"></Label>
14+
15+
<Label [text]="'Depth: ' + depth" colSpan="2" row="1"
16+
style="font-size: 30; horizontal-align: center; margin: 10"></Label>
17+
18+
<Button text="BACK" row="2" col="0" width="150"
19+
(tap)="goBack()"></Button>
20+
21+
<Button text="FORWARD" row="2" col="1" width="150"
22+
[nsRouterLink]="['/Nav', { depth: depth + 1 }]"></Button>
23+
</GridLayout>
24+
`
25+
})
26+
export class NavComponent implements OnActivate, OnDeactivate {
27+
static counter: number = 0;
28+
29+
public compId: number;
30+
public depth: number;
31+
32+
constructor(params: RouteParams, private location: LocationStrategy) {
33+
NavComponent.counter++;
34+
35+
this.compId = NavComponent.counter;
36+
this.depth = parseInt(params.get("depth"));
37+
38+
console.log("NavComponent.constructor() componenetID: " + this.compId)
39+
}
40+
41+
public goBack() {
42+
// this.location.back();
43+
topmost().goBack();
44+
}
45+
46+
routerOnActivate(nextInstruction: ComponentInstruction, prevInstruction: ComponentInstruction): any {
47+
console.log("NavComponent.routerOnActivate() componenetID: " + this.compId)
48+
}
49+
50+
routerOnDeactivate(nextInstruction: ComponentInstruction, prevInstruction: ComponentInstruction): any {
51+
console.log("NavComponent.routerOnDeactivate() componenetID: " + this.compId)
52+
}
53+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import {Component} from 'angular2/core';
2+
import {RouteConfig, ROUTER_PROVIDERS, ROUTER_DIRECTIVES} from 'angular2/router';
3+
4+
import {NavComponent} from "./nav-component";
5+
import {NS_ROUTER_DIRECTIVES} from "../../nativescript-angular/router/ns-router";
6+
7+
@Component({
8+
selector:"start-nav-test",
9+
directives: [NS_ROUTER_DIRECTIVES],
10+
template:`<Button horizontalAlignment="center" verticalAlignment="center" text="start" [nsRouterLink]="['/Nav', { depth: 1 }]"></Button>`
11+
})
12+
class StartComponent {
13+
14+
}
15+
16+
17+
@Component({
18+
selector: 'navigation-test',
19+
directives: [NS_ROUTER_DIRECTIVES],
20+
template: "<GridLayout><page-router-outlet></page-router-outlet></GridLayout>"
21+
})
22+
@RouteConfig([
23+
{ path: '/', component: StartComponent, as: 'Start' },
24+
{ path: '/nav/:depth', component: NavComponent, as: 'Nav' },
25+
])
26+
export class NavigationTest {
27+
28+
}
29+
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import trace = require("trace");
2+
3+
export const CATEGORY = "ns-router";
4+
5+
export function log(message: string) {
6+
trace.write(message, CATEGORY);
7+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import application = require("application");
2+
import { LocationStrategy } from 'angular2/router';
3+
import { NgZone, ApplicationRef, Inject, forwardRef } from 'angular2/core';
4+
import { log } from "./common";
5+
6+
interface LocationState
7+
{
8+
state: any,
9+
title: string,
10+
url: string,
11+
queryParams: string
12+
}
13+
14+
export class NSLocationStrategy extends LocationStrategy {
15+
private states = new Array<LocationState>();
16+
private popStateCallbacks = new Array<(_: any) => any>();
17+
private ngZone: NgZone;
18+
constructor(@Inject(forwardRef(() => NgZone)) zone: NgZone){
19+
super();
20+
21+
this.ngZone = zone;
22+
//if(application.android){
23+
//application.android.on("activityBackPressed", (args: application.AndroidActivityBackPressedEventData) => {
24+
//this.ngZone.run( () => {
25+
//if(this.states.length > 1){
26+
//this.back();
27+
//args.cancel = true;
28+
//}
29+
//});
30+
//})
31+
//}
32+
}
33+
34+
path(): string {
35+
log("NSLocationStrategy.path()");
36+
if(this.states.length > 0){
37+
return this.states[this.states.length - 1].url;
38+
}
39+
return "/";
40+
}
41+
prepareExternalUrl(internal: string): string {
42+
log("NSLocationStrategy.prepareExternalUrl() internal: " + internal);
43+
return internal;
44+
}
45+
pushState(state: any, title: string, url: string, queryParams: string): void {
46+
log(`NSLocationStrategy.pushState state: ${state}, title: ${title}, url: ${url}, queryParams: ${queryParams}`);
47+
48+
this.states.push({
49+
state: state,
50+
title: title,
51+
url: url,
52+
queryParams: queryParams });
53+
54+
}
55+
replaceState(state: any, title: string, url: string, queryParams: string): void {
56+
log(`NSLocationStrategy.replaceState state: ${state}, title: ${title}, url: ${url}, queryParams: ${queryParams}`);
57+
58+
this.states.pop()
59+
this.states.push({
60+
state: state,
61+
title: title,
62+
url: url,
63+
queryParams: queryParams });
64+
}
65+
forward(): void {
66+
log("NSLocationStrategy.forward");
67+
throw new Error("Not implemented");
68+
}
69+
back(): void {
70+
log("NSLocationStrategy.back");
71+
72+
var state = this.states.pop();
73+
this.callPopState(state, true);
74+
}
75+
onPopState(fn: (_: any) => any): void {
76+
log("NSLocationStrategy.onPopState");
77+
this.popStateCallbacks.push(fn);
78+
}
79+
getBaseHref(): string {
80+
log("NSLocationStrategy.getBaseHref()");
81+
return "";
82+
}
83+
84+
private callPopState(state:LocationState, pop: boolean = true){
85+
var change = { url: state.url, pop: pop};
86+
for(var fn of this.popStateCallbacks){
87+
fn(change);
88+
}
89+
}
90+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import {Directive, Input} from 'angular2/core';
2+
import {isString} from 'angular2/src/facade/lang';
3+
import {Router, Location, Instruction} from 'angular2/router';
4+
import { log } from "./common";
5+
6+
/**
7+
* The NSRouterLink directive lets you link to specific parts of your app.
8+
*
9+
* Consider the following route configuration:
10+
* ```
11+
* @RouteConfig([
12+
* { path: '/user', component: UserCmp, as: 'User' }
13+
* ]);
14+
* class MyComp {}
15+
* ```
16+
*
17+
* When linking to this `User` route, you can write:
18+
*
19+
* ```
20+
* <a [nsRouterLink]="['./User']">link to user component</a>
21+
* ```
22+
*
23+
* RouterLink expects the value to be an array of route names, followed by the params
24+
* for that level of routing. For instance `['/Team', {teamId: 1}, 'User', {userId: 2}]`
25+
* means that we want to generate a link for the `Team` route with params `{teamId: 1}`,
26+
* and with a child route `User` with params `{userId: 2}`.
27+
*
28+
* The first route name should be prepended with `/`, `./`, or `../`.
29+
* If the route begins with `/`, the router will look up the route from the root of the app.
30+
* If the route begins with `./`, the router will instead look in the current component's
31+
* children for the route. And if the route begins with `../`, the router will look at the
32+
* current component's parent.
33+
*/
34+
@Directive({
35+
selector: '[nsRouterLink]',
36+
inputs: ['params: nsRouterLink'],
37+
host: {
38+
'(tap)': 'onTap()'
39+
}
40+
})
41+
export class NSRouterLink {
42+
private _routeParams: any[];
43+
44+
// the instruction passed to the router to navigate
45+
private _navigationInstruction: Instruction;
46+
47+
constructor(private _router: Router, private _location: Location) { }
48+
49+
// get isRouteActive(): boolean { return this._router.isRouteActive(this._navigationInstruction); }
50+
51+
set params(changes: any[]) {
52+
this._routeParams = changes;
53+
this._navigationInstruction = this._router.generate(this._routeParams);
54+
}
55+
56+
onTap(): void {
57+
log("NSRouterLink onTap() instruction: " + JSON.stringify(this._navigationInstruction))
58+
this._router.navigateByInstruction(this._navigationInstruction);
59+
}
60+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import {Type} from 'angular2/src/facade/lang';
2+
import {NSRouterLink} from './ns-router-link';
3+
import {PageRouterOutlet} from './page-router-outlet';
4+
import {NSLocationStrategy} from './ns-location-strategy';
5+
import {ROUTER_PROVIDERS, LocationStrategy} from 'angular2/router';
6+
import {provide} from 'angular2/core';
7+
import { CATEGORY } from "./common";
8+
9+
export const NS_ROUTER_PROVIDERS: any[] = [
10+
ROUTER_PROVIDERS,
11+
provide(LocationStrategy, {useClass: NSLocationStrategy})
12+
];
13+
14+
export const NS_ROUTER_DIRECTIVES: Type[] = [
15+
NSRouterLink,
16+
PageRouterOutlet
17+
];
18+
19+
export const routerTraceCategory = CATEGORY;
Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
import {PromiseWrapper} from 'angular2/src/facade/async';
2+
import {isBlank, isPresent} from 'angular2/src/facade/lang';
3+
4+
import {Directive, Attribute, DynamicComponentLoader, ComponentRef, ElementRef,
5+
Injector, provide, Type, Component, OpaqueToken, Inject} from 'angular2/core';
6+
7+
import * as routerHooks from 'angular2/src/router/lifecycle_annotations';
8+
import { hasLifecycleHook} from 'angular2/src/router/route_lifecycle_reflector';
9+
10+
import { ComponentInstruction, RouteParams, RouteData, RouterOutlet, LocationStrategy, Router,
11+
OnActivate, OnDeactivate } from 'angular2/router';
12+
13+
import { topmost } from "ui";
14+
import { Page, NavigatedData } from "ui/page";
15+
import { log } from "./common";
16+
17+
let COMPONENT = new OpaqueToken("COMPONENT");
18+
let _resolveToTrue = PromiseWrapper.resolve(true);
19+
let _resolveToFalse = PromiseWrapper.resolve(false);
20+
21+
/**
22+
* Reference Cache
23+
*/
24+
class RefCache {
25+
private cache: Array<ComponentRef> = new Array<ComponentRef>();
26+
27+
public push(comp: ComponentRef) {
28+
this.cache.push(comp);
29+
}
30+
31+
public pop(): ComponentRef {
32+
return this.cache.pop();
33+
}
34+
35+
public peek(): ComponentRef {
36+
return this.cache[this.cache.length - 1];
37+
}
38+
}
39+
40+
var _isGoingBack = false;
41+
function startGoBack() {
42+
log("startGoBack()");
43+
if (_isGoingBack) {
44+
throw new Error("Calling startGoBack while going back.")
45+
}
46+
_isGoingBack = true;
47+
}
48+
49+
function endGoBack() {
50+
log("endGoBack()");
51+
if (!_isGoingBack) {
52+
throw new Error("Calling endGoBack while not going back.")
53+
}
54+
_isGoingBack = false;
55+
}
56+
57+
function isGoingBack() {
58+
return _isGoingBack;
59+
}
60+
61+
62+
/**
63+
* A router outlet that does page navigation in NativeScript
64+
*
65+
* ## Use
66+
*
67+
* ```
68+
* <page-router-outlet></page-router-outlet>
69+
* ```
70+
*/
71+
@Directive({ selector: 'page-router-outlet' })
72+
export class PageRouterOutlet extends RouterOutlet {
73+
private isInitalPage: boolean = true;
74+
private refCache: RefCache = new RefCache();
75+
76+
private componentRef: ComponentRef = null;
77+
private currentComponentType: ComponentRef = null;
78+
private currentInstruction: ComponentInstruction = null;
79+
80+
constructor(private elementRef: ElementRef,
81+
private loader: DynamicComponentLoader,
82+
private parentRouter: Router,
83+
@Attribute('name') nameAttr: string) {
84+
super(elementRef, loader, parentRouter, nameAttr)
85+
}
86+
87+
/**
88+
* Called by the Router to instantiate a new component during the commit phase of a navigation.
89+
* This method in turn is responsible for calling the `routerOnActivate` hook of its child.
90+
*/
91+
activate(nextInstruction: ComponentInstruction): Promise<any> {
92+
this.log("activate", nextInstruction);
93+
94+
let previousInstruction = this.currentInstruction;
95+
let componentType = nextInstruction.componentType;
96+
this.currentInstruction = nextInstruction;
97+
98+
if (isGoingBack()) {
99+
log("PageRouterOutlet.activate() - Back naviation, so load from cache: " + componentType.name);
100+
101+
endGoBack();
102+
103+
// Get Component form ref and just call the activate hook
104+
this.componentRef = this.refCache.peek();
105+
this.currentComponentType = componentType;
106+
this.checkComponentRef(this.componentRef, nextInstruction);
107+
108+
if (hasLifecycleHook(routerHooks.routerOnActivate, componentType)) {
109+
return (<OnActivate>this.componentRef.instance)
110+
.routerOnActivate(nextInstruction, previousInstruction);
111+
}
112+
}
113+
else {
114+
let childRouter = this.parentRouter.childRouter(componentType);
115+
let providers = Injector.resolve([
116+
provide(RouteData, { useValue: nextInstruction.routeData }),
117+
provide(RouteParams, { useValue: new RouteParams(nextInstruction.params) }),
118+
provide(Router, { useValue: childRouter }),
119+
provide(COMPONENT, { useValue: componentType }),
120+
]);
121+
122+
// TODO: Is there a better way to check first load?
123+
if (this.isInitalPage) {
124+
log("PageRouterOutlet.activate() inital page - just load component: " + componentType.name);
125+
this.isInitalPage = false;
126+
}
127+
else {
128+
log("PageRouterOutlet.activate() forward navigation - wrap component in page: " + componentType.name);
129+
componentType = PageShim;
130+
}
131+
132+
return this.loader.loadNextToLocation(componentType, this.elementRef, providers)
133+
.then((componentRef) => {
134+
this.componentRef = componentRef;
135+
this.currentComponentType = componentType;
136+
this.refCache.push(componentRef);
137+
138+
if (hasLifecycleHook(routerHooks.routerOnActivate, componentType)) {
139+
return (<OnActivate>this.componentRef.instance)
140+
.routerOnActivate(nextInstruction, previousInstruction);
141+
}
142+
});
143+
}
144+
}
145+
146+
/**
147+
* Called by the {@link Router} when an outlet disposes of a component's contents.
148+
* This method in turn is responsible for calling the `routerOnDeactivate` hook of its child.
149+
*/
150+
deactivate(nextInstruction: ComponentInstruction): Promise<any> {
151+
this.log("deactivate", nextInstruction);
152+
var instruction = this.currentInstruction;
153+
var compType = this.currentComponentType;
154+
155+
var next = _resolveToTrue;
156+
if (isPresent(this.componentRef) &&
157+
isPresent(instruction) &&
158+
isPresent(compType) &&
159+
hasLifecycleHook(routerHooks.routerOnDeactivate, compType)) {
160+
next = PromiseWrapper.resolve(
161+
(<OnDeactivate>this.componentRef.instance).routerOnDeactivate(nextInstruction, this.currentInstruction));
162+
}
163+
164+
if (isGoingBack()) {
165+
log("PageRouterOutlet.deactivate() while going back - should dispose: " + instruction.componentType.name)
166+
return next.then((_) => {
167+
let popedRef = this.refCache.pop();
168+
169+
if (this.componentRef !== popedRef) {
170+
throw new Error("Current componentRef is different for cached componentRef");
171+
}
172+
this.checkComponentRef(popedRef, instruction);
173+
174+
if (isPresent(this.componentRef)) {
175+
this.componentRef.dispose();
176+
this.componentRef = null;
177+
}
178+
});
179+
}
180+
else {
181+
return next;
182+
}
183+
}
184+
185+
/**
186+
* Called by the {@link Router} during recognition phase of a navigation.
187+
* PageRouterOutlet will aways return true as cancelling navigation
188+
* is currently not supported in NativeScript.
189+
*/
190+
routerCanDeactivate(nextInstruction: ComponentInstruction): Promise<boolean> {
191+
return _resolveToTrue;
192+
}
193+
194+
/**
195+
* Called by the {@link Router} during recognition phase of a navigation.
196+
* For PageRouterOutlet it always reurns false, as there is no way to reuse
197+
* the same componenet between two pages.
198+
*/
199+
routerCanReuse(nextInstruction: ComponentInstruction): Promise<boolean> {
200+
return _resolveToFalse;
201+
}
202+
203+
/**
204+
* Called by the {@link Router} during the commit phase of a navigation when an outlet
205+
* reuses a component between different routes.
206+
* For PageRouterOutlet this method should never be called,
207+
* because routerCanReuse always returns false.
208+
*/
209+
reuse(nextInstruction: ComponentInstruction): Promise<any> {
210+
throw new Error("reuse() method should never be called for PageRouterOutlet.")
211+
return _resolveToFalse;
212+
}
213+
214+
private checkComponentRef(popedRef: ComponentRef, instruction: ComponentInstruction) {
215+
if (popedRef.instance instanceof PageShim) {
216+
var shim = <PageShim>popedRef.instance;
217+
if (shim.componentType !== instruction.componentType) {
218+
throw new Error("ComponentRef value is different form expected!");
219+
}
220+
}
221+
}
222+
223+
private log(method: string, nextInstruction: ComponentInstruction) {
224+
log("PageRouterOutlet." + method + " isBack: " + isGoingBack() + " nextUrl: " + nextInstruction.urlPath);
225+
}
226+
}
227+
228+
@Component({
229+
selector: 'nativescript-page-shim',
230+
template: `
231+
<StackLayout visibility="collapse" style="background-color: hotpink">
232+
<Placeholder #content></Placeholder>
233+
<StackLayout>
234+
`
235+
})
236+
class PageShim implements OnActivate, OnDeactivate {
237+
private static pageShimCount: number = 0;
238+
private id: number;
239+
private isInitialized: boolean;
240+
private componentRef: ComponentRef;
241+
242+
constructor(
243+
private element: ElementRef,
244+
private loader: DynamicComponentLoader,
245+
private locationStrategy: LocationStrategy,
246+
@Inject(COMPONENT) public componentType: Type
247+
) {
248+
this.id = PageShim.pageShimCount++;
249+
this.log("constructor");
250+
}
251+
252+
routerOnActivate(nextInstruction: ComponentInstruction, prevInstruction: ComponentInstruction): any {
253+
this.log("routerOnActivate");
254+
let result = PromiseWrapper.resolve(true);
255+
256+
// On first activation:
257+
// 1. Load componenet using loadIntoLocation.
258+
// 2. Hijack its native element.
259+
// 3. Put that element into a new page and navigate to it.
260+
if (!this.isInitialized) {
261+
result = new Promise((resolve, reject) => {
262+
this.isInitialized = true;
263+
this.loader.loadIntoLocation(this.componentType, this.element, 'content')
264+
.then((componentRef) => {
265+
this.componentRef = componentRef;
266+
267+
//Component loaded. Find its root native view.
268+
const viewContainer = this.componentRef.location.nativeElement;
269+
//Remove from original native parent.
270+
//TODO: assuming it's a Layout.
271+
(<any>viewContainer.parent).removeChild(viewContainer);
272+
273+
topmost().navigate({
274+
animated: true,
275+
create: () => {
276+
const page = new Page();
277+
page.on('loaded', () => {
278+
// Finish activation when page is fully loaded.
279+
resolve()
280+
});
281+
282+
page.on('navigatingFrom', global.zone.bind((args: NavigatedData) => {
283+
if (args.isBackNavigation) {
284+
startGoBack();
285+
this.locationStrategy.back();
286+
}
287+
}));
288+
289+
// Add to new page.
290+
page.content = viewContainer;
291+
return page;
292+
}
293+
});
294+
});
295+
});
296+
}
297+
298+
if (hasLifecycleHook(routerHooks.routerOnActivate, this.componentType)) {
299+
result = result.then(() => {
300+
return (<OnActivate>this.componentRef.instance).routerOnActivate(nextInstruction, prevInstruction);
301+
});
302+
}
303+
return result;
304+
}
305+
306+
routerOnDeactivate(nextInstruction: ComponentInstruction, prevInstruction: ComponentInstruction): any {
307+
this.log("routerOnDeactivate");
308+
if (hasLifecycleHook(routerHooks.routerOnDeactivate, this.componentType)) {
309+
return (<OnDeactivate>this.componentRef.instance).routerOnDeactivate(nextInstruction, prevInstruction);
310+
}
311+
}
312+
313+
private log(methodName: string) {
314+
log("PageShim(" + this.id + ")." + methodName)
315+
}
316+
}

0 commit comments

Comments
 (0)
Please sign in to comment.