From 3fa532be041242243d50a057b0f1ac84cc949e6b Mon Sep 17 00:00:00 2001 From: vakrilov Date: Wed, 6 Jul 2016 15:31:06 +0300 Subject: [PATCH 1/2] clearHistory & pageTransition implementation --- nativescript-angular/application.ts | 3 +- nativescript-angular/platform-providers.ts | 4 +- .../router-deprecated/page-router-outlet.ts | 12 +- .../router/ns-location-strategy.ts | 109 ++++++++--- .../router/ns-platform-location.ts | 18 +- nativescript-angular/router/ns-router-link.ts | 48 ++++- nativescript-angular/router/ns-router.ts | 3 + .../router/page-router-outlet.ts | 94 +++++---- .../router/router-extensions.ts | 35 ++++ ng-sample/app/app.ts | 10 +- .../app/examples/router/clear-history-test.ts | 107 ++++++++++ .../router/clear-history.component.html | 24 +++ ng-sample/app/examples/router/styles.css | 15 ++ tests/app/tests/ns-location-strategy.ts | 182 ++++++++++++++++++ tests/app/tests/snippets.ts | 1 + 15 files changed, 565 insertions(+), 100 deletions(-) create mode 100644 nativescript-angular/router/router-extensions.ts create mode 100644 ng-sample/app/examples/router/clear-history-test.ts create mode 100644 ng-sample/app/examples/router/clear-history.component.html create mode 100644 tests/app/tests/ns-location-strategy.ts diff --git a/nativescript-angular/application.ts b/nativescript-angular/application.ts index 6f6b0ca0f..9da6d4400 100644 --- a/nativescript-angular/application.ts +++ b/nativescript-angular/application.ts @@ -30,7 +30,7 @@ import {topmost, NavigationEntry} from "ui/frame"; export type ProviderArray = Array; -import {defaultPageProvider, defaultDeviceProvider, defaultAnimationDriverProvider} from "./platform-providers"; +import {defaultPageProvider, defaultFrameProvider, defaultDeviceProvider, defaultAnimationDriverProvider} from "./platform-providers"; import * as nativescriptIntl from "nativescript-intl"; global.Intl = nativescriptIntl; @@ -91,6 +91,7 @@ export function bootstrap(appComponentType: any, }, deps: [] }), + defaultFrameProvider, defaultPageProvider, defaultDeviceProvider, defaultAnimationDriverProvider, diff --git a/nativescript-angular/platform-providers.ts b/nativescript-angular/platform-providers.ts index f5f14123f..9c3191480 100644 --- a/nativescript-angular/platform-providers.ts +++ b/nativescript-angular/platform-providers.ts @@ -1,4 +1,4 @@ -import {topmost} from 'ui/frame'; +import {topmost, Frame} from 'ui/frame'; import {Page} from 'ui/page'; import {provide, Provider, OpaqueToken} from '@angular/core/src/di'; import {device} from "platform"; @@ -19,6 +19,8 @@ export function getDefaultPage(): Page { } } +export const defaultFrameProvider = provide(Frame, { useFactory: topmost }); + export const defaultDeviceProvider = provide(DEVICE, { useValue: device }); export const defaultAnimationDriverProvider = provide(AnimationDriver, { useClass: NativeScriptAnimationDriver }); \ No newline at end of file diff --git a/nativescript-angular/router-deprecated/page-router-outlet.ts b/nativescript-angular/router-deprecated/page-router-outlet.ts index bfc5c804b..b2e4a200e 100644 --- a/nativescript-angular/router-deprecated/page-router-outlet.ts +++ b/nativescript-angular/router-deprecated/page-router-outlet.ts @@ -98,7 +98,7 @@ export class PageRouterOutlet extends RouterOutlet { let previousInstruction = this.currentInstruction; this.currentInstruction = nextInstruction; - if (this.location.isPageNavigatingBack()) { + if (this.location._isPageNavigatingBack()) { return this.activateOnGoBack(nextInstruction, previousInstruction); } else { return this.activateOnGoForward(nextInstruction, previousInstruction); @@ -108,7 +108,7 @@ export class PageRouterOutlet extends RouterOutlet { private activateOnGoBack(nextInstruction: ComponentInstruction, previousInstruction: ComponentInstruction): Promise { routerLog("PageRouterOutlet.activate() - Back naviation, so load from cache: " + nextInstruction.componentType.name); - this.location.finishBackPageNavigation(); + this.location._finishBackPageNavigation(); // Get Component form ref and just call the activate hook let cacheItem = this.refCache.peek(); @@ -177,7 +177,7 @@ export class PageRouterOutlet extends RouterOutlet { //Add it to the new page page.content = componentView; - this.location.navigateToNewPage(); + this.location._beginPageNavigation(); return new Promise((resolve, reject) => { page.on('navigatingTo', () => { // Finish activation when page navigation has started @@ -186,7 +186,7 @@ export class PageRouterOutlet extends RouterOutlet { page.on('navigatedFrom', (global).Zone.current.wrap((args: NavigatedData) => { if (args.isBackNavigation) { - this.location.beginBackPageNavigation(); + this.location._beginBackPageNavigation(); this.location.back(); } })); @@ -214,7 +214,7 @@ export class PageRouterOutlet extends RouterOutlet { (this.componentRef.instance).routerOnDeactivate(nextInstruction, this.currentInstruction)); } - if (this.location.isPageNavigatingBack()) { + if (this.location._isPageNavigatingBack()) { routerLog("PageRouterOutlet.deactivate() while going back - should destroy: " + instruction.componentType.name) return next.then((_) => { const popedItem = this.refCache.pop(); @@ -311,6 +311,6 @@ export class PageRouterOutlet extends RouterOutlet { } private log(method: string, nextInstruction: ComponentInstruction) { - routerLog("PageRouterOutlet." + method + " isBack: " + this.location.isPageNavigatingBack() + " nextUrl: " + nextInstruction.urlPath); + routerLog("PageRouterOutlet." + method + " isBack: " + this.location._isPageNavigatingBack() + " nextUrl: " + nextInstruction.urlPath); } } diff --git a/nativescript-angular/router/ns-location-strategy.ts b/nativescript-angular/router/ns-location-strategy.ts index 0a3826a91..c77623495 100644 --- a/nativescript-angular/router/ns-location-strategy.ts +++ b/nativescript-angular/router/ns-location-strategy.ts @@ -1,10 +1,22 @@ import application = require("application"); +import { Injectable } from '@angular/core'; import { LocationStrategy } from '@angular/common'; -import { NgZone, ApplicationRef, Inject, forwardRef } from '@angular/core'; import { routerLog } from "../trace"; -import { topmost } from "ui/frame"; +import { Frame, NavigationTransition } from "ui/frame"; +import {isPresent} from '@angular/core/src/facade/lang'; -interface LocationState { +export interface NavigationOptions { + clearHistory?: boolean; + animated?: boolean; + transition?: NavigationTransition; +} + +const defaultNavOptions: NavigationOptions = { + clearHistory: false, + animated: true +}; + +export interface LocationState { state: any, title: string, url: string, @@ -12,22 +24,25 @@ interface LocationState { isPageNavigation: boolean } +@Injectable() export class NSLocationStrategy extends LocationStrategy { private states = new Array(); private popStateCallbacks = new Array<(_: any) => any>(); private _isPageNavigationgBack = false; private _isPageNavigatingForward: boolean = false; + private _currentNavigationOptions: NavigationOptions; - constructor() { + constructor(private frame: Frame) { super(); routerLog("NSLocationStrategy.constructor()"); } path(): string { - routerLog("NSLocationStrategy.path()"); let state = this.peekState(); - return state ? state.url : "/"; + const result = state ? state.url : "/"; + routerLog("NSLocationStrategy.path(): " + result); + return result; } prepareExternalUrl(internal: string): string { @@ -42,7 +57,16 @@ export class NSLocationStrategy extends LocationStrategy { } pushStateInternal(state: any, title: string, url: string, queryParams: string): void { - let isNewPage = this._isPageNavigatingForward; + let isNewPage = this._isPageNavigatingForward || this.states.length === 0; + + const navOptions = this._currentNavigationOptions || defaultNavOptions; + + if (navOptions.clearHistory) { + routerLog("NSLocationStrategy.pushStateInternal clearing states history"); + this.states.length = 0; + } + + this._currentNavigationOptions = undefined; this._isPageNavigatingForward = false; this.states.push({ @@ -55,19 +79,22 @@ export class NSLocationStrategy extends LocationStrategy { } replaceState(state: any, title: string, url: string, queryParams: string): void { - routerLog(`NSLocationStrategy.replaceState state: ${state}, title: ${title}, url: ${url}, queryParams: ${queryParams}`); - if (this.states.length > 0) { - let oldState = this.states.pop(); - routerLog(`NSLocationStrategy.replaceState state poped: ${oldState.state}, title: ${oldState.title}, url: ${oldState.url}, queryParams: ${oldState.queryParams}`); + routerLog(`NSLocationStrategy.replaceState changing exisitng state: ${state}, title: ${title}, url: ${url}, queryParams: ${queryParams}`); + const topState = this.peekState(); + topState.state = state; + topState.title = title; + topState.url = url; + topState.queryParams = queryParams; + } + else { + routerLog(`NSLocationStrategy.replaceState pushing new state: ${state}, title: ${title}, url: ${url}, queryParams: ${queryParams}`); + this.pushStateInternal(state, title, url, queryParams); } - - this.pushStateInternal(state, title, url, queryParams); } forward(): void { - routerLog("NSLocationStrategy.forward"); - throw new Error("Not implemented"); + throw new Error("NSLocationStrategy.forward() - not implemented"); } back(): void { @@ -76,21 +103,23 @@ export class NSLocationStrategy extends LocationStrategy { // clear the stack until we get to a page navigation state let state = this.states.pop(); let count = 1; - while (!state.isPageNavigation) { + + while (!(state.isPageNavigation)) { state = this.states.pop(); count++; } - routerLog("NSLocationStrategy.back() while navigating back. States popped: " + count) + + routerLog("NSLocationStrategy.back() while navigating back. States popped: " + count); this.callPopState(state, true); } else { let state = this.peekState(); if (state.isPageNavigation) { // This was a page navigation - so navigate through frame. - routerLog("NSLocationStrategy.back() while not navigating back but top state is page - will call frame.goback()") - topmost().goBack(); + routerLog("NSLocationStrategy.back() while not navigating back but top state is page - will call frame.goback()"); + this.frame.goBack(); } else { // Nested navigation - just pop the state - routerLog("NSLocationStrategy.back() while not navigating back but top state is not page - just pop") + routerLog("NSLocationStrategy.back() while not navigating back but top state is not page - just pop"); this.callPopState(this.states.pop(), true); } } @@ -108,8 +137,8 @@ export class NSLocationStrategy extends LocationStrategy { } private callPopState(state: LocationState, pop: boolean = true) { - var change = { url: state.url, pop: pop }; - for (var fn of this.popStateCallbacks) { + const change = { url: state.url, pop: pop }; + for (let fn of this.popStateCallbacks) { fn(change); } } @@ -121,32 +150,54 @@ export class NSLocationStrategy extends LocationStrategy { return null; } + public toString() { + return this.states + .map((v, i) => `${i}.[${v.isPageNavigation ? "PAGE" : "INTERNAL"}] "${v.url}"`) + .reverse() + .join("\n"); + } + // Methods for syncing with page navigation in PageRouterOutlet - public beginBackPageNavigation() { + public _beginBackPageNavigation() { routerLog("NSLocationStrategy.startGoBack()"); if (this._isPageNavigationgBack) { - throw new Error("Calling startGoBack while going back.") + throw new Error("Calling startGoBack while going back."); } this._isPageNavigationgBack = true; } - public finishBackPageNavigation() { + public _finishBackPageNavigation() { routerLog("NSLocationStrategy.finishBackPageNavigation()"); if (!this._isPageNavigationgBack) { - throw new Error("Calling endGoBack while not going back.") + throw new Error("Calling endGoBack while not going back."); } this._isPageNavigationgBack = false; } - public isPageNavigatingBack() { + public _isPageNavigatingBack() { return this._isPageNavigationgBack; } - public navigateToNewPage() { + public _beginPageNavigation(): NavigationOptions { routerLog("NSLocationStrategy.navigateToNewPage()"); if (this._isPageNavigatingForward) { - throw new Error("Calling navigateToNewPage while already navigating to new page.") + throw new Error("Calling navigateToNewPage while already navigating to new page."); } + + this._isPageNavigatingForward = true; + return this._currentNavigationOptions || defaultNavOptions; + } + + public _setNavigationOptions(options: NavigationOptions) { + this._currentNavigationOptions = { + clearHistory: isPresent(options.clearHistory) ? options.clearHistory : false, + animated: isPresent(options.animated) ? options.animated : true, + transition: options.transition + }; + } + + public _getSatates(): Array { + return this.states.slice(); } } diff --git a/nativescript-angular/router/ns-platform-location.ts b/nativescript-angular/router/ns-platform-location.ts index 900ba4e23..da84d3686 100644 --- a/nativescript-angular/router/ns-platform-location.ts +++ b/nativescript-angular/router/ns-platform-location.ts @@ -17,52 +17,38 @@ export class NativescriptPlatformLocation extends PlatformLocation { } onPopState(fn: UrlChangeListener): void { - routerLog("NativescriptPlatformLocation.onPopState()"); this.locationStartegy.onPopState(fn); } onHashChange(fn: UrlChangeListener): void { - routerLog("NativescriptPlatformLocation.onHashChange()"); } get search(): string { - routerLog("NativescriptPlatformLocation.get search()"); - return ""; } get hash(): string { - routerLog("NativescriptPlatformLocation.get hash()"); - return ""; } get pathname(): string { - routerLog("NativescriptPlatformLocation.get pathname()"); return this.locationStartegy.path(); } set pathname(newPath: string) { - routerLog("NativescriptPlatformLocation.set pathname(): " + newPath); + throw new Error("NativescriptPlatformLocation set pathname - not implemented") } pushState(state: any, title: string, url: string): void { - routerLog("NativescriptPlatformLocation.pushState()"); - this.locationStartegy.pushState(state, title, url, null); } replaceState(state: any, title: string, url: string): void { - routerLog("NativescriptPlatformLocation.replaceState()"); this.locationStartegy.replaceState(state, title, url, null); } forward(): void { - routerLog("NativescriptPlatformLocation.forward()"); - - throw new Error("NativescriptPlatformLocation.forward() not implemend"); + throw new Error("NativescriptPlatformLocation.forward() - not implemented"); } back(): void { - routerLog("NativescriptPlatformLocation.back()"); - this.locationStartegy.back(); } } diff --git a/nativescript-angular/router/ns-router-link.ts b/nativescript-angular/router/ns-router-link.ts index b176829a5..1e06e6346 100644 --- a/nativescript-angular/router/ns-router-link.ts +++ b/nativescript-angular/router/ns-router-link.ts @@ -1,7 +1,12 @@ import {Directive, HostListener, Input, Optional} from '@angular/core'; -import {Router, ActivatedRoute} from '@angular/router'; +import {NavigationExtras} from "@angular/router/src/router"; +import {ActivatedRoute} from '@angular/router'; import {routerLog} from "../trace"; import {PageRoute} from "./page-router-outlet"; +import {RouterExtensions} from "./router-extensions"; +import {NavigationOptions} from "./ns-location-strategy"; +import {NavigationTransition} from "ui/frame"; +import {isString} from "utils/types"; /** * The nsRouterLink directive lets you link to specific parts of your app. @@ -35,12 +40,16 @@ export class NSRouterLink { @Input() queryParams: { [k: string]: any }; @Input() fragment: string; + @Input() clearHistory: boolean; + @Input() pageTransition: boolean | string | NavigationTransition = true; + private usePageRoute: boolean; -constructor( - private router: Router, + constructor( + private navigator: RouterExtensions, private route: ActivatedRoute, @Optional() private pageRoute: PageRoute) { + this.usePageRoute = (this.pageRoute && this.route === this.pageRoute.activatedRoute.getValue()); } @@ -55,11 +64,36 @@ constructor( @HostListener("tap") onTap() { - routerLog("nsRouterLink.tapped: " + this.commands); + routerLog("nsRouterLink.tapped: " + this.commands + " usePageRoute: " + this.usePageRoute + " clearHistory: " + this.clearHistory + " transition: " + JSON.stringify(this.pageTransition)); + const currentRoute = this.usePageRoute ? this.pageRoute.activatedRoute.getValue() : this.route; + const transition = this.getTrasnition(); + let extras: NavigationExtras & NavigationOptions = { + relativeTo: currentRoute, + queryParams: this.queryParams, + fragment: this.fragment, + clearHistory: this.clearHistory, + animated: transition.animated, + transition: transition.transition + }; + + this.navigator.navigate(this.commands, extras); + } - this.router.navigate( - this.commands, - { relativeTo: currentRoute, queryParams: this.queryParams, fragment: this.fragment }); + private getTrasnition(): { animated: boolean, transition?: NavigationTransition } { + if (typeof this.pageTransition === "boolean") { + return { animated: this.pageTransition }; + } else if (isString(this.pageTransition)) { + if (this.pageTransition === "none" || this.pageTransition === "false") { + return { animated: false }; + } else { + return { animated: true, transition: { name: this.pageTransition } }; + } + } else { + return { + animated: true, + transition: this.pageTransition + } + } } } diff --git a/nativescript-angular/router/ns-router.ts b/nativescript-angular/router/ns-router.ts index cacebf84d..38b25b235 100644 --- a/nativescript-angular/router/ns-router.ts +++ b/nativescript-angular/router/ns-router.ts @@ -8,9 +8,11 @@ import {NSRouterLink} from './ns-router-link'; import {PageRouterOutlet} from './page-router-outlet'; import {NSLocationStrategy} from './ns-location-strategy'; import {NativescriptPlatformLocation} from './ns-platform-location'; +import {RouterExtensions} from './router-extensions'; export {routerTraceCategory} from "../trace"; export {PageRoute} from './page-router-outlet'; +export {RouterExtensions} from './router-extensions'; export const NS_ROUTER_PROVIDERS: any[] = [ NSLocationStrategy, @@ -18,6 +20,7 @@ export const NS_ROUTER_PROVIDERS: any[] = [ NativescriptPlatformLocation, provide(PlatformLocation, { useClass: NativescriptPlatformLocation }), + RouterExtensions ]; export const NS_ROUTER_DIRECTIVES: Type[] = [ diff --git a/nativescript-angular/router/page-router-outlet.ts b/nativescript-angular/router/page-router-outlet.ts index b4ef01f6c..280c9ebae 100644 --- a/nativescript-angular/router/page-router-outlet.ts +++ b/nativescript-angular/router/page-router-outlet.ts @@ -13,7 +13,7 @@ import {Device} from "platform"; import {routerLog} from "../trace"; import {DetachedLoader} from "../common/detached-loader"; import {ViewUtil} from "../view-util"; -import {topmost} from "ui/frame"; +import {Frame} from "ui/frame"; import {Page, NavigatedData} from "ui/page"; import {BehaviorSubject} from "rxjs"; @@ -31,7 +31,6 @@ export class PageRoute { } } - /** * Reference Cache */ @@ -53,6 +52,10 @@ class RefCache { public peek(): CacheItem { return this.cache[this.cache.length - 1]; } + + public get length(): number { + return this.cache.length; + } } @Directive({ selector: 'page-router-outlet' }) @@ -62,24 +65,24 @@ export class PageRouterOutlet { private isInitalPage: boolean = true; private detachedLoaderFactory: ComponentFactory; - private currnetActivatedComp: ComponentRef; + private currentActivatedComp: ComponentRef; private currentActivatedRoute: ActivatedRoute; public outletMap: RouterOutletMap; get isActivated(): boolean { - return !!this.currnetActivatedComp; + return !!this.currentActivatedComp; } get component(): Object { - if (!this.currnetActivatedComp) { + if (!this.currentActivatedComp) { throw new Error('Outlet is not activated'); } - return this.currnetActivatedComp.instance; + return this.currentActivatedComp.instance; } get activatedRoute(): ActivatedRoute { - if (!this.currnetActivatedComp) { + if (!this.currentActivatedComp) { throw new Error('Outlet is not activated'); } @@ -93,6 +96,7 @@ export class PageRouterOutlet { private locationStrategy: NSLocationStrategy, private componentFactoryResolver: ComponentFactoryResolver, compiler: ComponentResolver, + private frame: Frame, @Inject(DEVICE) device: Device) { parentOutletMap.registerOutlet(name ? name : PRIMARY_OUTLET, this); @@ -105,28 +109,38 @@ export class PageRouterOutlet { } deactivate(): void { - if (this.locationStrategy.isPageNavigatingBack()) { + if (this.locationStrategy._isPageNavigatingBack()) { log("PageRouterOutlet.deactivate() while going back - should destroy"); const popedItem = this.refCache.pop(); const popedRef = popedItem.componentRef; - if (this.currnetActivatedComp !== popedRef) { + if (this.currentActivatedComp !== popedRef) { throw new Error("Current componentRef is different for cached componentRef"); } - if (isPresent(this.currnetActivatedComp)) { - this.currnetActivatedComp.destroy(); - this.currnetActivatedComp = null; - } + this.destroyCacheItem(popedItem); + this.currentActivatedComp = null; - if (isPresent(popedItem.loaderRef)) { - popedItem.loaderRef.destroy(); - } } else { log("PageRouterOutlet.deactivate() while going foward - do nothing"); } } + private clearRefCache() { + while (this.refCache.length > 0) { + this.destroyCacheItem(this.refCache.pop()); + } + } + private destroyCacheItem(popedItem: CacheItem) { + if (isPresent(popedItem.componentRef)) { + popedItem.componentRef.destroy(); + } + + if (isPresent(popedItem.loaderRef)) { + popedItem.loaderRef.destroy(); + } + } + /** * 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. @@ -139,7 +153,7 @@ export class PageRouterOutlet { this.outletMap = outletMap; this.currentActivatedRoute = activatedRoute; - if (this.locationStrategy.isPageNavigatingBack()) { + if (this.locationStrategy._isPageNavigatingBack()) { this.activateOnGoBack(activatedRoute, providers, outletMap); } else { this.activateOnGoForward(activatedRoute, providers, outletMap); @@ -152,27 +166,27 @@ export class PageRouterOutlet { outletMap: RouterOutletMap): void { const factory = this.getComponentFactory(activatedRoute); - const reusedRoute = new PageRoute(activatedRoute); - providers = [...providers, ...ReflectiveInjector.resolve([{ provide: PageRoute, useValue: reusedRoute }])]; + const pageRoute = new PageRoute(activatedRoute); + providers = [...providers, ...ReflectiveInjector.resolve([{ provide: PageRoute, useValue: pageRoute }])]; if (this.isInitalPage) { - log("PageRouterOutlet.activate() inital page - just load component: " + activatedRoute.component); + log("PageRouterOutlet.activate() inital page - just load component"); this.isInitalPage = false; const inj = ReflectiveInjector.fromResolvedProviders(providers, this.containerRef.parentInjector); - this.currnetActivatedComp = this.containerRef.createComponent(factory, this.containerRef.length, inj, []); - this.refCache.push(this.currnetActivatedComp, reusedRoute, outletMap, null); + this.currentActivatedComp = this.containerRef.createComponent(factory, this.containerRef.length, inj, []); + this.refCache.push(this.currentActivatedComp, pageRoute, outletMap, null); } else { - log("PageRouterOutlet.activate() forward navigation - create detached loader in the loader container: " + activatedRoute.component); + log("PageRouterOutlet.activate() forward navigation - create detached loader in the loader container"); const page = new Page(); const pageResolvedProvider = ReflectiveInjector.resolve([provide(Page, { useValue: page })]); const childInjector = ReflectiveInjector.fromResolvedProviders([...providers, ...pageResolvedProvider], this.containerRef.parentInjector); const loaderRef = this.containerRef.createComponent(this.detachedLoaderFactory, this.containerRef.length, childInjector, []); - this.currnetActivatedComp = loaderRef.instance.loadWithFactory(factory); - this.loadComponentInPage(page, this.currnetActivatedComp); - this.refCache.push(this.currnetActivatedComp, reusedRoute, outletMap, loaderRef); + this.currentActivatedComp = loaderRef.instance.loadWithFactory(factory); + this.loadComponentInPage(page, this.currentActivatedComp); + this.refCache.push(this.currentActivatedComp, pageRoute, outletMap, loaderRef); } } @@ -180,9 +194,9 @@ export class PageRouterOutlet { activatedRoute: ActivatedRoute, providers: ResolvedReflectiveProvider[], outletMap: RouterOutletMap): void { - log("PageRouterOutlet.activate() - Back naviation, so load from cache: " + activatedRoute.component); + log("PageRouterOutlet.activate() - Back naviation, so load from cache"); - this.locationStrategy.finishBackPageNavigation(); + this.locationStrategy._finishBackPageNavigation(); let cacheItem = this.refCache.peek(); cacheItem.reusedRoute.activatedRoute.next(activatedRoute); @@ -194,7 +208,7 @@ export class PageRouterOutlet { // its child router-outlets to the newly created outlet map. Object.assign(outletMap, cacheItem.outletMap); - this.currnetActivatedComp = cacheItem.componentRef; + this.currentActivatedComp = cacheItem.componentRef; } private loadComponentInPage(page: Page, componentRef: ComponentRef): void { @@ -205,25 +219,33 @@ export class PageRouterOutlet { //Add it to the new page page.content = componentView; - this.locationStrategy.navigateToNewPage(); - page.on('navigatedFrom', (global).Zone.current.wrap((args: NavigatedData) => { + // console.log("page.navigatedFrom: " + page + " args.isBackNavigation:" + args.isBackNavigation); + if (args.isBackNavigation) { - this.locationStrategy.beginBackPageNavigation(); + this.locationStrategy._beginBackPageNavigation(); this.locationStrategy.back(); } })); - topmost().navigate({ - animated: true, - create: () => { return page; } + const navOptions = this.locationStrategy._beginPageNavigation(); + this.frame.navigate({ + create: () => { return page; }, + clearHistory: navOptions.clearHistory, + animated: navOptions.animated, + transition: navOptions.transition }); + + // Clear refCache if navigation with clearHistory + if (navOptions.clearHistory) { + this.clearRefCache(); + } } // NOTE: Using private APIs - potential break point! private getComponentFactory(activatedRoute: any): ComponentFactory { const snapshot = activatedRoute._futureSnapshot; - const component: any = snapshot._routeConfig.component; + const component = snapshot._routeConfig.component; let factory: ComponentFactory; try { factory = typeof component === 'string' ? diff --git a/nativescript-angular/router/router-extensions.ts b/nativescript-angular/router/router-extensions.ts new file mode 100644 index 000000000..a97a1a2ff --- /dev/null +++ b/nativescript-angular/router/router-extensions.ts @@ -0,0 +1,35 @@ +import {Injectable} from "@angular/core"; +import {Router, UrlTree} from "@angular/router"; +import {NavigationExtras} from "@angular/router/src/router"; +import {NSLocationStrategy, NavigationOptions} from "./ns-location-strategy"; +import {Frame} from "ui/frame"; + +export type ExtendedNavigationExtras = NavigationExtras & NavigationOptions; + +@Injectable() +export class RouterExtensions { + + constructor(public router: Router, public locationStrategy: NSLocationStrategy, public frame: Frame) { } + + public navigate(commands: any[], extras?: NavigationExtras, options?: ExtendedNavigationExtras): Promise { + this.locationStrategy._setNavigationOptions(extras); + return this.router.navigate(commands, extras); + } + + public navigateByUrl(url: string | UrlTree, options?: NavigationOptions): Promise { + this.locationStrategy._setNavigationOptions(options); + return this.router.navigateByUrl(url); + } + + public back() { + this.locationStrategy.back(); + } + + public backToPreviousPage() { + this.frame.goBack(); + } + + public canGoBackToPreviousPage() { + this.frame.canGoBack(); + } +} \ No newline at end of file diff --git a/ng-sample/app/app.ts b/ng-sample/app/app.ts index 23dc01a8d..bb0aa4fa5 100644 --- a/ng-sample/app/app.ts +++ b/ng-sample/app/app.ts @@ -37,9 +37,10 @@ import {RouterOutletTest} from "./examples/router-deprecated/router-outlet-test" import {LoginTest} from "./examples/router-deprecated/login-test"; // new router -import { RouterOutletAppComponent, RouterOutletRouterProviders} from "./examples/router/router-outlet-test" -import { PageRouterOutletAppComponent, PageRouterOutletRouterProviders } from "./examples/router/page-router-outlet-test" -import { PageRouterOutletNestedAppComponent, PageRouterOutletNestedRouterProviders } from "./examples/router/page-router-outlet-nested-test" +import { RouterOutletAppComponent, RouterOutletRouterProviders} from "./examples/router/router-outlet-test"; +import { PageRouterOutletAppComponent, PageRouterOutletRouterProviders } from "./examples/router/page-router-outlet-test"; +import { PageRouterOutletNestedAppComponent, PageRouterOutletNestedRouterProviders } from "./examples/router/page-router-outlet-nested-test"; +import { ClearHistoryAppComponent, ClearHistoryRouterProviders } from "./examples/router/clear-history-test"; // animations import { AnimationEnterLeaveTest } from "./examples/animation/animation-enter-leave-test"; @@ -62,7 +63,8 @@ import { AnimationStatesTest } from "./examples/animation/animation-states-test" // new router // nativeScriptBootstrap(RouterOutletAppComponent, [RouterOutletRouterProviders]); // nativeScriptBootstrap(PageRouterOutletAppComponent, [PageRouterOutletRouterProviders]); -nativeScriptBootstrap(PageRouterOutletNestedAppComponent, [PageRouterOutletNestedRouterProviders]); +// nativeScriptBootstrap(PageRouterOutletNestedAppComponent, [PageRouterOutletNestedRouterProviders]); +nativeScriptBootstrap(ClearHistoryAppComponent, [ClearHistoryRouterProviders]); // router-deprecated // nativeScriptBootstrap(NavigationTest, [NS_ROUTER_PROVIDERS_DEPRECATED]); diff --git a/ng-sample/app/examples/router/clear-history-test.ts b/ng-sample/app/examples/router/clear-history-test.ts new file mode 100644 index 000000000..18c7bdc60 --- /dev/null +++ b/ng-sample/app/examples/router/clear-history-test.ts @@ -0,0 +1,107 @@ +import { Component, OnInit, OnDestroy, Injectable } from "@angular/core"; +import { RouterConfig, ActivatedRoute, Router, ROUTER_DIRECTIVES, CanDeactivate, ActivatedRouteSnapshot, RouterStateSnapshot} from '@angular/router'; +import { Observable } from "rxjs"; +import { NS_ROUTER_DIRECTIVES, nsProvideRouter, RouterExtensions, PageRoute} from "nativescript-angular/router"; +import { NSLocationStrategy } from "nativescript-angular/router/ns-location-strategy"; +import { BehaviorSubject} from "rxjs"; + +@Injectable() +class LocationLogService { + public locationStack$ = new BehaviorSubject>([]); + public routerEvents$ = new BehaviorSubject>([]); + public showStack: boolean = true; + + constructor(router: Router, private strategy: NSLocationStrategy) { + router.events.subscribe((e) => { + this.routerEvents$.next([...this.routerEvents$.getValue(), e.toString()]); + + let states = this.strategy._getSatates() + .map((v, i) => { + return (i + "." + (v.isPageNavigation ? "[PAGE]" : "") + " \"" + v.url + "\""); + }) + .reverse(); + + this.locationStack$.next(states); + }); + } +} + +@Component({ + selector: 'location-log', + styleUrls: ["examples/router/styles.css"], + template: ` + + + + + + + + ` +}) +export class LocationLog { + constructor(private service: LocationLogService) { } +} + +@Component({ + selector: "first", + directives: [ROUTER_DIRECTIVES, NS_ROUTER_DIRECTIVES, LocationLog], + templateUrl: "examples/router/clear-history.component.html", + styleUrls: ["examples/router/styles.css"] +}) +class FirstComponent implements OnInit, OnDestroy { + name = "First"; + constructor(private nav: RouterExtensions) { } + ngOnInit() { console.log("FirstComponent - ngOnInit()"); } + ngOnDestroy() { console.log("FirstComponent - ngOnDestroy()"); } +} + +@Component({ + selector: "second", + directives: [ROUTER_DIRECTIVES, NS_ROUTER_DIRECTIVES, LocationLog], + templateUrl: "examples/router/clear-history.component.html", + styleUrls: ["examples/router/styles.css"] +}) +class SecondComponent implements OnInit, OnDestroy { + name = "Second"; + constructor(private nav: RouterExtensions) { } + ngOnInit() { console.log("SecondComponent - ngOnInit()"); } + ngOnDestroy() { console.log("SecondComponent - ngOnDestroy()"); } +} + +@Component({ + selector: "third", + directives: [ROUTER_DIRECTIVES, NS_ROUTER_DIRECTIVES, LocationLog], + templateUrl: "examples/router/clear-history.component.html", + styleUrls: ["examples/router/styles.css"] +}) +class ThirdComponent implements OnInit, OnDestroy { + name = "Third"; + constructor(private nav: RouterExtensions) { } + ngOnInit() { console.log("ThirdComponent - ngOnInit()"); } + ngOnDestroy() { console.log("ThirdComponent - ngOnDestroy()"); } +} + +@Component({ + selector: 'navigation-test', + directives: [ROUTER_DIRECTIVES, NS_ROUTER_DIRECTIVES], + providers: [LocationLogService], + template: `` +}) +export class ClearHistoryAppComponent {} + +const routes: RouterConfig = [ + { path: "", redirectTo: "/first", terminal: true }, + { path: "first", component: FirstComponent}, + { path: "second", component: SecondComponent}, + { path: "third", component: ThirdComponent}, +]; + +export const ClearHistoryRouterProviders = [ + nsProvideRouter(routes, { enableTracing: false }) +]; \ No newline at end of file diff --git a/ng-sample/app/examples/router/clear-history.component.html b/ng-sample/app/examples/router/clear-history.component.html new file mode 100644 index 000000000..028782159 --- /dev/null +++ b/ng-sample/app/examples/router/clear-history.component.html @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ng-sample/app/examples/router/styles.css b/ng-sample/app/examples/router/styles.css index 3df96a326..e711edfbb 100644 --- a/ng-sample/app/examples/router/styles.css +++ b/ng-sample/app/examples/router/styles.css @@ -47,4 +47,19 @@ button { .detail button { horizontal-align: center; +} + +.odd { + background-color: white; + margin: 2; +} + +.even { + background-color: whitesmoke; + margin: 2; +} + +.stretch { + horizontal-align: stretch; + margin: 0 10; } \ No newline at end of file diff --git a/tests/app/tests/ns-location-strategy.ts b/tests/app/tests/ns-location-strategy.ts new file mode 100644 index 000000000..86f5cb353 --- /dev/null +++ b/tests/app/tests/ns-location-strategy.ts @@ -0,0 +1,182 @@ +//make sure you import mocha-config before @angular/core +import {assert} from "./test-config"; +import {NSLocationStrategy, LocationState} from "nativescript-angular/router/ns-location-strategy"; +import {Frame, BackstackEntry, NavigationEntry} from "ui/frame"; +import {Page} from "ui/page"; +import {View} from "ui/core/view"; + +class FakeFrame extends View implements Frame { + backStack: Array; + currentPage: Page; + currentEntry: NavigationEntry; + animated: boolean; + transition: any; + + canGoBack(): boolean { return true; } + goBack(to?: BackstackEntry) { + if (this.backCB) { + this.backCB(); + } + } + + navigate(entry: NavigationEntry) { } + + constructor(private backCB?: () => void) { + super(); + } +} + +function initStrategy(back?: () => void): NSLocationStrategy { + const strategy = new NSLocationStrategy(new FakeFrame(back)); + strategy.pushState(null, null, "/", null); // load initial state + return strategy; +} + +function assertStatesEqual(actual: Array, expeced: Array) { + assert.isArray(actual); + assert.isArray(expeced); + assert.equal(actual.length, expeced.length); + + for (let i = 0; i < actual.length; i++) { + assert.deepEqual( + actual[i], expeced[i], + `State[${i}] does not match!\n actual: ${JSON.stringify(actual[i])}\nexpected: ${JSON.stringify(expeced[i])}`); + } +} + +function createState(url: string, isPageNav: boolean = false) { + return { + state: null, + title: null, + url: url, + queryParams: null, + isPageNavigation: isPageNav + }; +} + +function simulatePageNavigation(strategy: NSLocationStrategy, url: string) { + strategy._beginPageNavigation(); + strategy.pushState(null, null, url, null); +} + +function simulatePageBack(strategy: NSLocationStrategy) { + strategy._beginBackPageNavigation(); + strategy.back(); + strategy._finishBackPageNavigation(); +} + +describe('NSLocationStrategy', () => { + + it("initial path() value", () => { + const strategy = new NSLocationStrategy(new FakeFrame()); + assert.equal(strategy.path(), "/"); + }); + + + it("pushState changes path", () => { + const strategy = initStrategy(); + + strategy.pushState(null, "test", "/test", null); + assert.equal(strategy.path(), "/test"); + }); + + it("back() calls onPopState", () => { + const strategy = initStrategy(); + let popCount = 0; + strategy.onPopState(() => { popCount++; }); + + strategy.pushState(null, "test", "/test", null); + assert.equal(strategy.path(), "/test"); + assert.equal(popCount, 0); + + strategy.back(); + assert.equal(strategy.path(), "/"); + assert.equal(popCount, 1); + }); + + it("replaceState() replaces state - dosn't call onPopState", () => { + const strategy = initStrategy(); + let popCount = 0; + strategy.onPopState(() => { popCount++; }); + + strategy.pushState(null, "test", "/test", null); + assert.equal(strategy.path(), "/test"); + + strategy.replaceState(null, "test2", "/test2", null); + assert.equal(strategy.path(), "/test2"); + + assert.equal(popCount, 0); // no onPopState when replacing + }); + + it("pushState() with page navigation", () => { + const strategy = initStrategy(); + const expextedStates: Array = [createState("/", true)]; + + simulatePageNavigation(strategy, "/page"); + expextedStates.push(createState("/page", true)); + + strategy.pushState(null, null, "/internal", null); + expextedStates.push(createState("/internal")); + + assertStatesEqual(strategy._getSatates(), expextedStates); + }); + + + it("back() when on page-state calls frame.goBack() if no page navigation in progress", () => { + let frameBackCount = 0; + const strategy = initStrategy(() => { frameBackCount++; }); + let popCount = 0; + strategy.onPopState(() => { popCount++; }); + + simulatePageNavigation(strategy, "/page"); + + assert.equal(frameBackCount, 0); + assert.equal(popCount, 0); + assert.equal(strategy._getSatates().length, 2); + + // Act + strategy.back(); + + // Assert + assert.equal(frameBackCount, 1); + assert.equal(popCount, 0); + assert.equal(strategy._getSatates().length, 2); + }); + + + it("back() when on page-state navigates back if page navigation is in progress", () => { + let frameBackCount = 0; + const strategy = initStrategy(() => { frameBackCount++; }); + let popCount = 0; + strategy.onPopState(() => { popCount++; }); + + simulatePageNavigation(strategy, "/page"); + + assert.equal(frameBackCount, 0); + assert.equal(popCount, 0); + assert.equal(strategy._getSatates().length, 2); + + // Act + simulatePageBack(strategy); + + // Assert + assert.equal(frameBackCount, 0); + assert.equal(popCount, 1); + assert.equal(strategy._getSatates().length, 1); + }); + + + it("pushState() with clearHistory clears history", () => { + const strategy = initStrategy(); + + // Act + strategy._setNavigationOptions({ clearHistory: true }); + simulatePageNavigation(strategy, "/cleared"); + + // Assert + assertStatesEqual(strategy._getSatates(), [createState("/cleared", true)]); + }); +}); + + + diff --git a/tests/app/tests/snippets.ts b/tests/app/tests/snippets.ts index feb49cfa7..22f96fb48 100644 --- a/tests/app/tests/snippets.ts +++ b/tests/app/tests/snippets.ts @@ -78,6 +78,7 @@ describe('Snippets Navigation', () => { let navStarted = false; subscription = app.router.events.subscribe((e) => { + console.log("------>>>>>> " + e.toString()); if (e instanceof NavigationStart) { assert.equal("/", e.url); navStarted = true; From 125d9df85696458ead44c27143e3e987826e957a Mon Sep 17 00:00:00 2001 From: vakrilov Date: Fri, 22 Jul 2016 23:39:54 +0300 Subject: [PATCH 2/2] Login example --- .../router/ns-location-strategy.ts | 1 - nativescript-angular/router/ns-router-link.ts | 6 +- .../router/router-extensions.ts | 10 +- ng-sample/app/app.ts | 4 +- ng-sample/app/examples/router/login-test.ts | 133 ++++++++++++++++++ 5 files changed, 146 insertions(+), 8 deletions(-) create mode 100644 ng-sample/app/examples/router/login-test.ts diff --git a/nativescript-angular/router/ns-location-strategy.ts b/nativescript-angular/router/ns-location-strategy.ts index c77623495..53909d666 100644 --- a/nativescript-angular/router/ns-location-strategy.ts +++ b/nativescript-angular/router/ns-location-strategy.ts @@ -184,7 +184,6 @@ export class NSLocationStrategy extends LocationStrategy { throw new Error("Calling navigateToNewPage while already navigating to new page."); } - this._isPageNavigatingForward = true; return this._currentNavigationOptions || defaultNavOptions; } diff --git a/nativescript-angular/router/ns-router-link.ts b/nativescript-angular/router/ns-router-link.ts index 1e06e6346..c53337dcf 100644 --- a/nativescript-angular/router/ns-router-link.ts +++ b/nativescript-angular/router/ns-router-link.ts @@ -65,9 +65,9 @@ export class NSRouterLink { @HostListener("tap") onTap() { routerLog("nsRouterLink.tapped: " + this.commands + " usePageRoute: " + this.usePageRoute + " clearHistory: " + this.clearHistory + " transition: " + JSON.stringify(this.pageTransition)); - + const currentRoute = this.usePageRoute ? this.pageRoute.activatedRoute.getValue() : this.route; - const transition = this.getTrasnition(); + const transition = this.getTransition(); let extras: NavigationExtras & NavigationOptions = { relativeTo: currentRoute, queryParams: this.queryParams, @@ -80,7 +80,7 @@ export class NSRouterLink { this.navigator.navigate(this.commands, extras); } - private getTrasnition(): { animated: boolean, transition?: NavigationTransition } { + private getTransition(): { animated: boolean, transition?: NavigationTransition } { if (typeof this.pageTransition === "boolean") { return { animated: this.pageTransition }; } else if (isString(this.pageTransition)) { diff --git a/nativescript-angular/router/router-extensions.ts b/nativescript-angular/router/router-extensions.ts index a97a1a2ff..5a5244e17 100644 --- a/nativescript-angular/router/router-extensions.ts +++ b/nativescript-angular/router/router-extensions.ts @@ -11,13 +11,17 @@ export class RouterExtensions { constructor(public router: Router, public locationStrategy: NSLocationStrategy, public frame: Frame) { } - public navigate(commands: any[], extras?: NavigationExtras, options?: ExtendedNavigationExtras): Promise { - this.locationStrategy._setNavigationOptions(extras); + public navigate(commands: any[], extras?: ExtendedNavigationExtras): Promise { + if (extras) { + this.locationStrategy._setNavigationOptions(extras); + } return this.router.navigate(commands, extras); } public navigateByUrl(url: string | UrlTree, options?: NavigationOptions): Promise { - this.locationStrategy._setNavigationOptions(options); + if (options) { + this.locationStrategy._setNavigationOptions(options); + } return this.router.navigateByUrl(url); } diff --git a/ng-sample/app/app.ts b/ng-sample/app/app.ts index bb0aa4fa5..43069ddca 100644 --- a/ng-sample/app/app.ts +++ b/ng-sample/app/app.ts @@ -41,6 +41,7 @@ import { RouterOutletAppComponent, RouterOutletRouterProviders} from "./examples import { PageRouterOutletAppComponent, PageRouterOutletRouterProviders } from "./examples/router/page-router-outlet-test"; import { PageRouterOutletNestedAppComponent, PageRouterOutletNestedRouterProviders } from "./examples/router/page-router-outlet-nested-test"; import { ClearHistoryAppComponent, ClearHistoryRouterProviders } from "./examples/router/clear-history-test"; +import { LoginAppComponent, LoginExampleProviders } from "./examples/router/login-test"; // animations import { AnimationEnterLeaveTest } from "./examples/animation/animation-enter-leave-test"; @@ -64,7 +65,8 @@ import { AnimationStatesTest } from "./examples/animation/animation-states-test" // nativeScriptBootstrap(RouterOutletAppComponent, [RouterOutletRouterProviders]); // nativeScriptBootstrap(PageRouterOutletAppComponent, [PageRouterOutletRouterProviders]); // nativeScriptBootstrap(PageRouterOutletNestedAppComponent, [PageRouterOutletNestedRouterProviders]); -nativeScriptBootstrap(ClearHistoryAppComponent, [ClearHistoryRouterProviders]); +// nativeScriptBootstrap(ClearHistoryAppComponent, [ClearHistoryRouterProviders]); +nativeScriptBootstrap(LoginAppComponent, [LoginExampleProviders]); // router-deprecated // nativeScriptBootstrap(NavigationTest, [NS_ROUTER_PROVIDERS_DEPRECATED]); diff --git a/ng-sample/app/examples/router/login-test.ts b/ng-sample/app/examples/router/login-test.ts new file mode 100644 index 000000000..dee583555 --- /dev/null +++ b/ng-sample/app/examples/router/login-test.ts @@ -0,0 +1,133 @@ +import { Component, OnInit, OnDestroy, Injectable } from "@angular/core"; +import { RouterConfig, Router, CanActivate} from '@angular/router'; +import { Observable } from "rxjs"; +import { NS_ROUTER_DIRECTIVES, nsProvideRouter, RouterExtensions, PageRoute} from "nativescript-angular/router"; +import { NSLocationStrategy } from "nativescript-angular/router/ns-location-strategy"; +import { BehaviorSubject} from "rxjs"; +import * as appSettings from "application-settings" + +const USER_KEY = "user"; + +@Injectable() +class LoginService { + username: string; + get isLogged(): boolean { return !!this.username; } + + constructor() { + this.username = appSettings.getString(USER_KEY); + console.log("LoginService.constructor() username: " + this.username); + } + + login(user: string, password: string): Promise { + console.log("LoginService.login() username: " + this.username); + if (user) { + this.username = user; + appSettings.setString(USER_KEY, this.username); + return Promise.resolve(true); + } else { + return Promise.resolve(false); + } + } + + logout(): Promise { + console.log("LoginService.logout()"); + + this.username = undefined; + appSettings.remove(USER_KEY); + return Promise.resolve(true); + } +} + + +@Component({ + selector: 'login', + directives: [NS_ROUTER_DIRECTIVES], + styleUrls: ["examples/router/styles.css"], + template: ` + + + + + + + + ` +}) +class LoginComponent { + public user: string; + public pass: string; + constructor(private nav: RouterExtensions, private loginService: LoginService) { + } + + login() { + this.loginService.login(this.user, this.pass).then((result) => { + if (result) { + this.nav.navigate(["/"], { clearHistory: true }); + } + }); + } +} + +@Component({ + selector: 'main', + directives: [NS_ROUTER_DIRECTIVES], + styleUrls: ["examples/router/styles.css"], + template: ` + + + + + + ` +}) +class MainComponent { + constructor(private nav: RouterExtensions, private loginService: LoginService) { + } + + logout() { + this.loginService.logout().then((result) => { + if (result) { + this.nav.navigate(["/login"], { clearHistory: true }); + } + }); + } +} + +@Injectable() +class AuthGuard implements CanActivate { + constructor( + private loginService: LoginService, + private nav: RouterExtensions) { + } + + canActivate() { + if (this.loginService.isLogged) { + console.log("GUARD: authenticated"); + return true; + } + else { + console.log("GUARD: redirecting to login"); + this.nav.navigate(["/login"]); + return false; + } + } +} + +@Component({ + selector: 'navigation-test', + directives: [NS_ROUTER_DIRECTIVES], + template: `` +}) +export class LoginAppComponent { } + +const routes: RouterConfig = [ + { path: "", redirectTo: "/main", terminal: true }, + { path: "main", component: MainComponent, canActivate: [AuthGuard] }, + { path: "login", component: LoginComponent }, +]; + +export const LoginExampleProviders = [ + LoginService, + AuthGuard, + nsProvideRouter(routes, { enableTracing: false }) +]; \ No newline at end of file