diff --git a/ng-sample/app/app.ts b/ng-sample/app/app.ts index 34faafe71..ad35174c2 100644 --- a/ng-sample/app/app.ts +++ b/ng-sample/app/app.ts @@ -7,15 +7,23 @@ // this import should be first in order to load some required settings (like globals and reflect-metadata) import { nativeScriptBootstrap } from "./nativescript-angular/application"; +import { NS_ROUTER_PROVIDERS, routerTraceCategory } from "./nativescript-angular/router/ns-router"; + +import trace = require("trace"); +trace.setCategories(routerTraceCategory); +trace.enable(); //import {RendererTest} from './examples/renderer-test'; //import {Benchmark} from './performance/benchmark'; //import {ListTest} from './examples/list/list-test'; -import {ListTestAsync} from "./examples/list/list-test-async"; +// import {ListTestAsync} from "./examples/list/list-test-async"; // import {ImageTest} from "./examples/image/image-test"; +import {NavigationTest} from "./examples/navigation/navigation-test"; + //nativeScriptBootstrap(RendererTest); //nativeScriptBootstrap(Benchmark); //nativeScriptBootstrap(ListTest); -nativeScriptBootstrap(ListTestAsync); +// nativeScriptBootstrap(ListTestAsync); // nativeScriptBootstrap(ImageTest); +nativeScriptBootstrap(NavigationTest, [NS_ROUTER_PROVIDERS]); diff --git a/ng-sample/app/examples/navigation/nav-component.ts b/ng-sample/app/examples/navigation/nav-component.ts new file mode 100644 index 000000000..2ca51246f --- /dev/null +++ b/ng-sample/app/examples/navigation/nav-component.ts @@ -0,0 +1,53 @@ +import {Component} from 'angular2/core'; +import {OnActivate, OnDeactivate, LocationStrategy, RouteParams, ComponentInstruction } from 'angular2/router'; +import {topmost} from "ui/frame"; +import {NS_ROUTER_DIRECTIVES} from "../../nativescript-angular/router/ns-router"; + + +@Component({ + selector: 'example-group', + directives: [NS_ROUTER_DIRECTIVES], + template: ` + + + + + + + + + +` +}) +export class NavComponent implements OnActivate, OnDeactivate { + static counter: number = 0; + + public compId: number; + public depth: number; + + constructor(params: RouteParams, private location: LocationStrategy) { + NavComponent.counter++; + + this.compId = NavComponent.counter; + this.depth = parseInt(params.get("depth")); + + console.log("NavComponent.constructor() componenetID: " + this.compId) + } + + public goBack() { + // this.location.back(); + topmost().goBack(); + } + + routerOnActivate(nextInstruction: ComponentInstruction, prevInstruction: ComponentInstruction): any { + console.log("NavComponent.routerOnActivate() componenetID: " + this.compId) + } + + routerOnDeactivate(nextInstruction: ComponentInstruction, prevInstruction: ComponentInstruction): any { + console.log("NavComponent.routerOnDeactivate() componenetID: " + this.compId) + } +} \ No newline at end of file diff --git a/ng-sample/app/examples/navigation/navigation-test.ts b/ng-sample/app/examples/navigation/navigation-test.ts new file mode 100644 index 000000000..5c90487fe --- /dev/null +++ b/ng-sample/app/examples/navigation/navigation-test.ts @@ -0,0 +1,29 @@ +import {Component} from 'angular2/core'; +import {RouteConfig, ROUTER_PROVIDERS, ROUTER_DIRECTIVES} from 'angular2/router'; + +import {NavComponent} from "./nav-component"; +import {NS_ROUTER_DIRECTIVES} from "../../nativescript-angular/router/ns-router"; + +@Component({ + selector:"start-nav-test", + directives: [NS_ROUTER_DIRECTIVES], + template:`` +}) +class StartComponent { + +} + + +@Component({ + selector: 'navigation-test', + directives: [NS_ROUTER_DIRECTIVES], + template: "" +}) +@RouteConfig([ + { path: '/', component: StartComponent, as: 'Start' }, + { path: '/nav/:depth', component: NavComponent, as: 'Nav' }, +]) +export class NavigationTest { + +} + diff --git a/src/nativescript-angular/router/common.ts b/src/nativescript-angular/router/common.ts new file mode 100644 index 000000000..2ec488458 --- /dev/null +++ b/src/nativescript-angular/router/common.ts @@ -0,0 +1,7 @@ +import trace = require("trace"); + +export const CATEGORY = "ns-router"; + +export function log(message: string) { + trace.write(message, CATEGORY); +} diff --git a/src/nativescript-angular/router/ns-location-strategy.ts b/src/nativescript-angular/router/ns-location-strategy.ts new file mode 100644 index 000000000..03973c765 --- /dev/null +++ b/src/nativescript-angular/router/ns-location-strategy.ts @@ -0,0 +1,90 @@ +import application = require("application"); +import { LocationStrategy } from 'angular2/router'; +import { NgZone, ApplicationRef, Inject, forwardRef } from 'angular2/core'; +import { log } from "./common"; + +interface LocationState +{ + state: any, + title: string, + url: string, + queryParams: string +} + +export class NSLocationStrategy extends LocationStrategy { + private states = new Array(); + private popStateCallbacks = new Array<(_: any) => any>(); + private ngZone: NgZone; + constructor(@Inject(forwardRef(() => NgZone)) zone: NgZone){ + super(); + + this.ngZone = zone; + //if(application.android){ + //application.android.on("activityBackPressed", (args: application.AndroidActivityBackPressedEventData) => { + //this.ngZone.run( () => { + //if(this.states.length > 1){ + //this.back(); + //args.cancel = true; + //} + //}); + //}) + //} + } + + path(): string { + log("NSLocationStrategy.path()"); + if(this.states.length > 0){ + return this.states[this.states.length - 1].url; + } + return "/"; + } + prepareExternalUrl(internal: string): string { + log("NSLocationStrategy.prepareExternalUrl() internal: " + internal); + return internal; + } + pushState(state: any, title: string, url: string, queryParams: string): void { + log(`NSLocationStrategy.pushState state: ${state}, title: ${title}, url: ${url}, queryParams: ${queryParams}`); + + this.states.push({ + state: state, + title: title, + url: url, + queryParams: queryParams }); + + } + replaceState(state: any, title: string, url: string, queryParams: string): void { + log(`NSLocationStrategy.replaceState state: ${state}, title: ${title}, url: ${url}, queryParams: ${queryParams}`); + + this.states.pop() + this.states.push({ + state: state, + title: title, + url: url, + queryParams: queryParams }); + } + forward(): void { + log("NSLocationStrategy.forward"); + throw new Error("Not implemented"); + } + back(): void { + log("NSLocationStrategy.back"); + + var state = this.states.pop(); + this.callPopState(state, true); + } + onPopState(fn: (_: any) => any): void { + log("NSLocationStrategy.onPopState"); + this.popStateCallbacks.push(fn); + } + getBaseHref(): string { + log("NSLocationStrategy.getBaseHref()"); + return ""; + } + + private callPopState(state:LocationState, pop: boolean = true){ + var change = { url: state.url, pop: pop}; + for(var fn of this.popStateCallbacks){ + fn(change); + } + } +} diff --git a/src/nativescript-angular/router/ns-router-link.ts b/src/nativescript-angular/router/ns-router-link.ts new file mode 100644 index 000000000..bb18988a8 --- /dev/null +++ b/src/nativescript-angular/router/ns-router-link.ts @@ -0,0 +1,60 @@ +import {Directive, Input} from 'angular2/core'; +import {isString} from 'angular2/src/facade/lang'; +import {Router, Location, Instruction} from 'angular2/router'; +import { log } from "./common"; + +/** + * The NSRouterLink directive lets you link to specific parts of your app. + * + * Consider the following route configuration: + * ``` + * @RouteConfig([ + * { path: '/user', component: UserCmp, as: 'User' } + * ]); + * class MyComp {} + * ``` + * + * When linking to this `User` route, you can write: + * + * ``` + * link to user component + * ``` + * + * RouterLink expects the value to be an array of route names, followed by the params + * for that level of routing. For instance `['/Team', {teamId: 1}, 'User', {userId: 2}]` + * means that we want to generate a link for the `Team` route with params `{teamId: 1}`, + * and with a child route `User` with params `{userId: 2}`. + * + * The first route name should be prepended with `/`, `./`, or `../`. + * If the route begins with `/`, the router will look up the route from the root of the app. + * If the route begins with `./`, the router will instead look in the current component's + * children for the route. And if the route begins with `../`, the router will look at the + * current component's parent. + */ +@Directive({ + selector: '[nsRouterLink]', + inputs: ['params: nsRouterLink'], + host: { + '(tap)': 'onTap()' + } +}) +export class NSRouterLink { + private _routeParams: any[]; + + // the instruction passed to the router to navigate + private _navigationInstruction: Instruction; + + constructor(private _router: Router, private _location: Location) { } + + // get isRouteActive(): boolean { return this._router.isRouteActive(this._navigationInstruction); } + + set params(changes: any[]) { + this._routeParams = changes; + this._navigationInstruction = this._router.generate(this._routeParams); + } + + onTap(): void { + log("NSRouterLink onTap() instruction: " + JSON.stringify(this._navigationInstruction)) + this._router.navigateByInstruction(this._navigationInstruction); + } +} \ No newline at end of file diff --git a/src/nativescript-angular/router/ns-router.ts b/src/nativescript-angular/router/ns-router.ts new file mode 100644 index 000000000..d6bee22bc --- /dev/null +++ b/src/nativescript-angular/router/ns-router.ts @@ -0,0 +1,19 @@ +import {Type} from 'angular2/src/facade/lang'; +import {NSRouterLink} from './ns-router-link'; +import {PageRouterOutlet} from './page-router-outlet'; +import {NSLocationStrategy} from './ns-location-strategy'; +import {ROUTER_PROVIDERS, LocationStrategy} from 'angular2/router'; +import {provide} from 'angular2/core'; +import { CATEGORY } from "./common"; + +export const NS_ROUTER_PROVIDERS: any[] = [ + ROUTER_PROVIDERS, + provide(LocationStrategy, {useClass: NSLocationStrategy}) +]; + +export const NS_ROUTER_DIRECTIVES: Type[] = [ + NSRouterLink, + PageRouterOutlet +]; + +export const routerTraceCategory = CATEGORY; \ No newline at end of file diff --git a/src/nativescript-angular/router/page-router-outlet.ts b/src/nativescript-angular/router/page-router-outlet.ts new file mode 100644 index 000000000..e4a620c4f --- /dev/null +++ b/src/nativescript-angular/router/page-router-outlet.ts @@ -0,0 +1,316 @@ +import {PromiseWrapper} from 'angular2/src/facade/async'; +import {isBlank, isPresent} from 'angular2/src/facade/lang'; + +import {Directive, Attribute, DynamicComponentLoader, ComponentRef, ElementRef, + Injector, provide, Type, Component, OpaqueToken, Inject} from 'angular2/core'; + +import * as routerHooks from 'angular2/src/router/lifecycle_annotations'; +import { hasLifecycleHook} from 'angular2/src/router/route_lifecycle_reflector'; + +import { ComponentInstruction, RouteParams, RouteData, RouterOutlet, LocationStrategy, Router, + OnActivate, OnDeactivate } from 'angular2/router'; + +import { topmost } from "ui"; +import { Page, NavigatedData } from "ui/page"; +import { log } from "./common"; + +let COMPONENT = new OpaqueToken("COMPONENT"); +let _resolveToTrue = PromiseWrapper.resolve(true); +let _resolveToFalse = PromiseWrapper.resolve(false); + +/** + * Reference Cache + */ +class RefCache { + private cache: Array = new Array(); + + public push(comp: ComponentRef) { + this.cache.push(comp); + } + + public pop(): ComponentRef { + return this.cache.pop(); + } + + public peek(): ComponentRef { + return this.cache[this.cache.length - 1]; + } +} + +var _isGoingBack = false; +function startGoBack() { + log("startGoBack()"); + if (_isGoingBack) { + throw new Error("Calling startGoBack while going back.") + } + _isGoingBack = true; +} + +function endGoBack() { + log("endGoBack()"); + if (!_isGoingBack) { + throw new Error("Calling endGoBack while not going back.") + } + _isGoingBack = false; +} + +function isGoingBack() { + return _isGoingBack; +} + + +/** + * A router outlet that does page navigation in NativeScript + * + * ## Use + * + * ``` + * + * ``` + */ +@Directive({ selector: 'page-router-outlet' }) +export class PageRouterOutlet extends RouterOutlet { + private isInitalPage: boolean = true; + private refCache: RefCache = new RefCache(); + + private componentRef: ComponentRef = null; + private currentComponentType: ComponentRef = null; + private currentInstruction: ComponentInstruction = null; + + constructor(private elementRef: ElementRef, + private loader: DynamicComponentLoader, + private parentRouter: Router, + @Attribute('name') nameAttr: string) { + super(elementRef, loader, parentRouter, nameAttr) + } + + /** + * Called by the Router to instantiate a new component during the commit phase of a navigation. + * This method in turn is responsible for calling the `routerOnActivate` hook of its child. + */ + activate(nextInstruction: ComponentInstruction): Promise { + this.log("activate", nextInstruction); + + let previousInstruction = this.currentInstruction; + let componentType = nextInstruction.componentType; + this.currentInstruction = nextInstruction; + + if (isGoingBack()) { + log("PageRouterOutlet.activate() - Back naviation, so load from cache: " + componentType.name); + + endGoBack(); + + // Get Component form ref and just call the activate hook + this.componentRef = this.refCache.peek(); + this.currentComponentType = componentType; + this.checkComponentRef(this.componentRef, nextInstruction); + + if (hasLifecycleHook(routerHooks.routerOnActivate, componentType)) { + return (this.componentRef.instance) + .routerOnActivate(nextInstruction, previousInstruction); + } + } + else { + let childRouter = this.parentRouter.childRouter(componentType); + let providers = Injector.resolve([ + provide(RouteData, { useValue: nextInstruction.routeData }), + provide(RouteParams, { useValue: new RouteParams(nextInstruction.params) }), + provide(Router, { useValue: childRouter }), + provide(COMPONENT, { useValue: componentType }), + ]); + + // TODO: Is there a better way to check first load? + if (this.isInitalPage) { + log("PageRouterOutlet.activate() inital page - just load component: " + componentType.name); + this.isInitalPage = false; + } + else { + log("PageRouterOutlet.activate() forward navigation - wrap component in page: " + componentType.name); + componentType = PageShim; + } + + return this.loader.loadNextToLocation(componentType, this.elementRef, providers) + .then((componentRef) => { + this.componentRef = componentRef; + this.currentComponentType = componentType; + this.refCache.push(componentRef); + + if (hasLifecycleHook(routerHooks.routerOnActivate, componentType)) { + return (this.componentRef.instance) + .routerOnActivate(nextInstruction, previousInstruction); + } + }); + } + } + + /** + * Called by the {@link Router} when an outlet disposes of a component's contents. + * This method in turn is responsible for calling the `routerOnDeactivate` hook of its child. + */ + deactivate(nextInstruction: ComponentInstruction): Promise { + this.log("deactivate", nextInstruction); + var instruction = this.currentInstruction; + var compType = this.currentComponentType; + + var next = _resolveToTrue; + if (isPresent(this.componentRef) && + isPresent(instruction) && + isPresent(compType) && + hasLifecycleHook(routerHooks.routerOnDeactivate, compType)) { + next = PromiseWrapper.resolve( + (this.componentRef.instance).routerOnDeactivate(nextInstruction, this.currentInstruction)); + } + + if (isGoingBack()) { + log("PageRouterOutlet.deactivate() while going back - should dispose: " + instruction.componentType.name) + return next.then((_) => { + let popedRef = this.refCache.pop(); + + if (this.componentRef !== popedRef) { + throw new Error("Current componentRef is different for cached componentRef"); + } + this.checkComponentRef(popedRef, instruction); + + if (isPresent(this.componentRef)) { + this.componentRef.dispose(); + this.componentRef = null; + } + }); + } + else { + return next; + } + } + + /** + * Called by the {@link Router} during recognition phase of a navigation. + * PageRouterOutlet will aways return true as cancelling navigation + * is currently not supported in NativeScript. + */ + routerCanDeactivate(nextInstruction: ComponentInstruction): Promise { + return _resolveToTrue; + } + + /** + * Called by the {@link Router} during recognition phase of a navigation. + * For PageRouterOutlet it always reurns false, as there is no way to reuse + * the same componenet between two pages. + */ + routerCanReuse(nextInstruction: ComponentInstruction): Promise { + return _resolveToFalse; + } + + /** + * Called by the {@link Router} during the commit phase of a navigation when an outlet + * reuses a component between different routes. + * For PageRouterOutlet this method should never be called, + * because routerCanReuse always returns false. + */ + reuse(nextInstruction: ComponentInstruction): Promise { + throw new Error("reuse() method should never be called for PageRouterOutlet.") + return _resolveToFalse; + } + + private checkComponentRef(popedRef: ComponentRef, instruction: ComponentInstruction) { + if (popedRef.instance instanceof PageShim) { + var shim = popedRef.instance; + if (shim.componentType !== instruction.componentType) { + throw new Error("ComponentRef value is different form expected!"); + } + } + } + + private log(method: string, nextInstruction: ComponentInstruction) { + log("PageRouterOutlet." + method + " isBack: " + isGoingBack() + " nextUrl: " + nextInstruction.urlPath); + } +} + +@Component({ + selector: 'nativescript-page-shim', + template: ` + + + + ` +}) +class PageShim implements OnActivate, OnDeactivate { + private static pageShimCount: number = 0; + private id: number; + private isInitialized: boolean; + private componentRef: ComponentRef; + + constructor( + private element: ElementRef, + private loader: DynamicComponentLoader, + private locationStrategy: LocationStrategy, + @Inject(COMPONENT) public componentType: Type + ) { + this.id = PageShim.pageShimCount++; + this.log("constructor"); + } + + routerOnActivate(nextInstruction: ComponentInstruction, prevInstruction: ComponentInstruction): any { + this.log("routerOnActivate"); + let result = PromiseWrapper.resolve(true); + + // On first activation: + // 1. Load componenet using loadIntoLocation. + // 2. Hijack its native element. + // 3. Put that element into a new page and navigate to it. + if (!this.isInitialized) { + result = new Promise((resolve, reject) => { + this.isInitialized = true; + this.loader.loadIntoLocation(this.componentType, this.element, 'content') + .then((componentRef) => { + this.componentRef = componentRef; + + //Component loaded. Find its root native view. + const viewContainer = this.componentRef.location.nativeElement; + //Remove from original native parent. + //TODO: assuming it's a Layout. + (viewContainer.parent).removeChild(viewContainer); + + topmost().navigate({ + animated: true, + create: () => { + const page = new Page(); + page.on('loaded', () => { + // Finish activation when page is fully loaded. + resolve() + }); + + page.on('navigatingFrom', global.zone.bind((args: NavigatedData) => { + if (args.isBackNavigation) { + startGoBack(); + this.locationStrategy.back(); + } + })); + + // Add to new page. + page.content = viewContainer; + return page; + } + }); + }); + }); + } + + if (hasLifecycleHook(routerHooks.routerOnActivate, this.componentType)) { + result = result.then(() => { + return (this.componentRef.instance).routerOnActivate(nextInstruction, prevInstruction); + }); + } + return result; + } + + routerOnDeactivate(nextInstruction: ComponentInstruction, prevInstruction: ComponentInstruction): any { + this.log("routerOnDeactivate"); + if (hasLifecycleHook(routerHooks.routerOnDeactivate, this.componentType)) { + return (this.componentRef.instance).routerOnDeactivate(nextInstruction, prevInstruction); + } + } + + private log(methodName: string) { + log("PageShim(" + this.id + ")." + methodName) + } +} \ No newline at end of file