refactor(nav): implement nav integration with external router (angular to start)

This commit is contained in:
Dan Bucholtz
2018-02-13 00:02:48 -06:00
committed by GitHub
parent 3fa8623cfe
commit edeb1324c9
91 changed files with 2386 additions and 3893 deletions

File diff suppressed because it is too large Load Diff

View File

@ -14,6 +14,7 @@ import {
FrameworkDelegate,
PickerColumn,
PickerOptions,
RouterDelegate,
} from './index';
import {
AlertButton,
@ -1837,6 +1838,7 @@ declare global {
lazy?: boolean;
mode?: string;
root?: any;
routerDelegate?: RouterDelegate;
swipeBackEnabled?: boolean;
useUrls?: boolean;
}

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,6 @@
import { Component, Element, Listen } from '@stencil/core';
import { NavResult } from '../..';
@Component({
tag: 'ion-nav-pop',
})
@ -8,12 +9,12 @@ export class NavPop {
@Element() element: HTMLElement;
@Listen('child:click')
pop() {
pop(): Promise<NavResult> {
const nav = this.element.closest('ion-nav') as HTMLIonNavElement;
if (nav) {
return nav.pop();
}
return Promise.resolve();
return Promise.resolve(null);
}
render() {

View File

@ -36,6 +36,7 @@ export interface PublicNav {
name?: string;
element?: HTMLElement;
parent?: PublicNav;
initialized?: boolean;
}
export interface NavOptions {
@ -71,9 +72,9 @@ export interface TransitionInstruction {
nav?: Nav;
delegate?: FrameworkDelegate;
animation?: Animation;
escapeHatch?: any;
escapeHatch?: EscapeHatch;
method?: string;
mountingData?: any;
mountingData?: FrameworkMountingData;
}
export interface NavResult {
@ -86,6 +87,18 @@ export interface ComponentDataPair {
data: any;
}
export interface ExternalNavData {
url: string;
method: string;
resolve: Function;
reject: Function;
}
export interface EscapeHatch {
fromExternalRouter?: boolean;
url?: string;
}
export interface Transition extends Animation {
enteringView?: ViewController;
leavingView?: ViewController;

View File

@ -29,22 +29,12 @@ export let VIEW_ID_START = 2000;
let transitionIds = 0;
const activeTransitions = new Map<number, any>();
let portalZindex = 9999;
export function isViewController(object: any): boolean {
return !!(object && object.didLoad && object.willUnload);
}
export function setZIndex(nav: Nav, enteringView: ViewController, leavingView: ViewController, direction: string) {
if (enteringView) {
if (nav.isPortal) {
if (direction === DIRECTION_FORWARD) {
// TODO - fix typing
updateZIndex(enteringView, (nav as any).zIndexOffset + portalZindex);
}
portalZindex++;
return;
}
leavingView = leavingView || nav.getPrevious(enteringView) as ViewController;

View File

@ -5,12 +5,16 @@ import {
AnimationOptions,
ComponentDataPair,
Config,
EscapeHatch,
ExternalNavData,
FrameworkDelegate,
NavOptions,
NavOutlet,
NavResult,
PublicNav,
PublicViewController,
RouterDelegate,
RouterEntries,
Transition,
TransitionInstruction,
} from '../../index';
@ -40,12 +44,14 @@ import {
} from './nav-utils';
import { DomFrameworkDelegate } from '../../utils/dom-framework-delegate';
import { DomRouterDelegate } from '../../utils/dom-router-delegate';
import {
assert,
focusOutActiveElement,
isDef,
isNumber,
normalizeUrl,
} from '../../utils/helpers';
@ -53,8 +59,8 @@ import { buildIOSTransition } from './transitions/transition.ios';
import { buildMdTransition } from './transitions/transition.md';
import { GestureDetail } from '../gesture/gesture';
const queueMap = new Map<number, TransitionInstruction[]>();
const urlMap = new Map<string, TransitionInstruction>();
const transitionQueue = new Map<number, TransitionInstruction[]>();
const allTransitionsCompleteHandlerQueue = new Map<number, Function[]>();
/* it is very important to keep this class in sync with ./nav-interface interface */
@Component({
@ -67,21 +73,23 @@ export class Nav implements PublicNav, NavOutlet {
@Event() navInit: EventEmitter<NavEventDetail>;
@Event() ionNavChanged: EventEmitter<NavEventDetail>;
navId: number;
init = false;
parent: Nav;
useRouter = false;
navId = getNextNavId();
routes: RouterEntries = [];
parent: Nav = null;
views: ViewController[] = [];
transitioning?: boolean;
destroyed?: boolean;
transitionId?: number;
isViewInitialized?: boolean;
isPortal: boolean;
transitioning = false;
destroyed = false;
transitionId = NOT_TRANSITIONING_TRANSITION_ID;
initialized = false;
sbTrns: any; // TODO Transition
childNavs?: Nav[];
urlExternalNavMap = new Map<string, ExternalNavData>();
@Prop() mode: string;
@Prop() root: any;
@Prop() delegate: FrameworkDelegate;
@Prop() routerDelegate: RouterDelegate;
@Prop() useUrls = false;
@Prop({ context: 'config' }) config: Config;
@Prop({ connect: 'ion-animation-controller' }) animationCtrl: AnimationController;
@ -93,20 +101,15 @@ export class Nav implements PublicNav, NavOutlet {
}
componentDidLoad() {
if (this.init) {
return;
}
this.init = true;
if (!this.lazy) {
componentDidLoadImpl(this);
}
return componentDidLoadImpl(this);
}
@Watch('root')
updateRootComponent(): any {
if (this.init) {
updateRootComponent(): Promise<NavResult> {
if (this.initialized) {
return this.setRoot(this.root);
}
return Promise.resolve(null);
}
@Method()
@ -115,52 +118,52 @@ export class Nav implements PublicNav, NavOutlet {
}
@Method()
push(component: any, data?: any, opts?: NavOptions, escapeHatch: any = {}): Promise<any> {
push(component: any, data?: any, opts?: NavOptions, escapeHatch: EscapeHatch = getDefaultEscapeHatch()): Promise<NavResult> {
return pushImpl(this, component, data, opts, escapeHatch);
}
@Method()
pop(opts?: NavOptions, escapeHatch: any = {}): Promise<any> {
pop(opts?: NavOptions, escapeHatch: EscapeHatch = getDefaultEscapeHatch()): Promise<NavResult> {
return popImpl(this, opts, escapeHatch);
}
@Method()
setRoot(component: any, data?: any, opts?: NavOptions, escapeHatch: any = {}): Promise<any> {
setRoot(component: any, data?: any, opts?: NavOptions, escapeHatch: EscapeHatch = getDefaultEscapeHatch()): Promise<NavResult> {
return setRootImpl(this, component, data, opts, escapeHatch);
}
@Method()
insert(insertIndex: number, page: any, params?: any, opts?: NavOptions, escapeHatch: any = {}): Promise<any> {
insert(insertIndex: number, page: any, params?: any, opts?: NavOptions, escapeHatch: EscapeHatch = getDefaultEscapeHatch()): Promise<NavResult> {
return insertImpl(this, insertIndex, page, params, opts, escapeHatch);
}
@Method()
insertPages(insertIndex: number, insertPages: any[], opts?: NavOptions, escapeHatch: any = {}): Promise<any> {
insertPages(insertIndex: number, insertPages: any[], opts?: NavOptions, escapeHatch: EscapeHatch = getDefaultEscapeHatch()): Promise<NavResult> {
return insertPagesImpl(this, insertIndex, insertPages, opts, escapeHatch);
}
@Method()
popToRoot(opts?: NavOptions, escapeHatch: any = {}): Promise<any> {
popToRoot(opts?: NavOptions, escapeHatch: EscapeHatch = getDefaultEscapeHatch()): Promise<NavResult> {
return popToRootImpl(this, opts, escapeHatch);
}
@Method()
popTo(indexOrViewCtrl: any, opts?: NavOptions, escapeHatch: any = {}): Promise<any> {
popTo(indexOrViewCtrl: any, opts?: NavOptions, escapeHatch: EscapeHatch = getDefaultEscapeHatch()): Promise<NavResult> {
return popToImpl(this, indexOrViewCtrl, opts, escapeHatch);
}
@Method()
removeIndex(startIndex: number, removeCount?: number, opts?: NavOptions, escapeHatch: any = {}): Promise<any> {
removeIndex(startIndex: number, removeCount?: number, opts?: NavOptions, escapeHatch: EscapeHatch = getDefaultEscapeHatch()): Promise<NavResult> {
return removeImpl(this, startIndex, removeCount, opts, escapeHatch);
}
@Method()
removeView(viewController: PublicViewController, opts?: NavOptions, escapeHatch: any = {}): Promise<any> {
removeView(viewController: PublicViewController, opts?: NavOptions, escapeHatch: EscapeHatch = getDefaultEscapeHatch()): Promise<NavResult> {
return removeViewImpl(this, viewController, opts, escapeHatch);
}
@Method()
setPages(componentDataPairs: ComponentDataPair[], opts?: NavOptions, escapeHatch: any = {}): Promise<any> {
setPages(componentDataPairs: ComponentDataPair[], opts?: NavOptions, escapeHatch: EscapeHatch = getDefaultEscapeHatch()): Promise<NavResult> {
return setPagesImpl(this, componentDataPairs, opts, escapeHatch);
}
@ -190,10 +193,11 @@ export class Nav implements PublicNav, NavOutlet {
}
@Method()
setRouteId(id: string, _: any = {}): Promise<void> {
setRouteId(id: string, _: any = {}): Promise<any> {
assert(this.useRouter, 'routing is disabled');
const active = this.getActive();
if (active && active.component === id) {
return Promise.resolve();
return Promise.resolve(null);
}
return this.setRoot(id);
}
@ -224,7 +228,7 @@ export class Nav implements PublicNav, NavOutlet {
@Method()
isTransitioning() {
return !!this.transitionId;
return this.transitionId >= 0;
}
@Method()
@ -238,13 +242,8 @@ export class Nav implements PublicNav, NavOutlet {
}
@Method()
getTransitionInfoForUrl(url: string): TransitionInstruction {
return urlMap.get(url);
}
@Method()
clearTransitionInfoForUrl(url: string) {
urlMap.delete(url);
onAllTransitionsComplete() {
return allTransitionsCompleteImpl(this);
}
canSwipeBack(): boolean {
@ -330,48 +329,60 @@ export class Nav implements PublicNav, NavOutlet {
}
export function componentDidLoadImpl(nav: Nav) {
if (nav.initialized) {
return;
}
nav.initialized = true;
nav.navInit.emit();
if (nav.root) {
nav.setRoot(nav.root);
if (!nav.useRouter) {
if (nav.root && !nav.lazy) {
nav.setRoot(nav.root);
}
}
}
export async function pushImpl(nav: Nav, component: any, data: any, opts: NavOptions, escapeHatch: any) {
export async function pushImpl(nav: Nav, component: any, data: any, opts: NavOptions, escapeHatch: EscapeHatch) {
const animation = await hydrateAnimationController(nav.animationCtrl);
return push(nav, nav.delegate, animation, component, data, opts, escapeHatch);
return push(nav, nav.delegate, animation, component, data, opts, escapeHatch).then((navResult) => {
return navResult;
});
}
export async function popImpl(nav: Nav, opts: NavOptions, escapeHatch: any) {
export async function popImpl(nav: Nav, opts: NavOptions, escapeHatch: EscapeHatch) {
const animation = await hydrateAnimationController(nav.animationCtrl);
return pop(nav, nav.delegate, animation, opts, escapeHatch);
return pop(nav, nav.delegate, animation, opts, escapeHatch).then((navResult) => {
return navResult;
});
}
export async function setRootImpl(nav: Nav, component: any, data: any, opts: NavOptions, escapeHatch: any) {
export async function setRootImpl(nav: Nav, component: any, data: any, opts: NavOptions, escapeHatch: EscapeHatch) {
const animation = await hydrateAnimationController(nav.animationCtrl);
return setRoot(nav, nav.delegate, animation, component, data, opts, escapeHatch);
return setRoot(nav, nav.delegate, animation, component, data, opts, escapeHatch).then((navResult) => {
return navResult;
});
}
export async function insertImpl(nav: Nav, insertIndex: number, page: any, params: any, opts: NavOptions, escapeHatch: any) {
export async function insertImpl(nav: Nav, insertIndex: number, page: any, params: any, opts: NavOptions, escapeHatch: EscapeHatch) {
const animation = await hydrateAnimationController(nav.animationCtrl);
return insert(nav, nav.delegate, animation, insertIndex, page, params, opts, escapeHatch);
}
export async function insertPagesImpl(nav: Nav, insertIndex: number, pagesToInsert: any[], opts: NavOptions, escapeHatch: any) {
export async function insertPagesImpl(nav: Nav, insertIndex: number, pagesToInsert: any[], opts: NavOptions, escapeHatch: EscapeHatch) {
const animation = await hydrateAnimationController(nav.animationCtrl);
return insertPages(nav, nav.delegate, animation, insertIndex, pagesToInsert, opts, escapeHatch);
}
export async function popToRootImpl(nav: Nav, opts: NavOptions, escapeHatch: any) {
export async function popToRootImpl(nav: Nav, opts: NavOptions, escapeHatch: EscapeHatch) {
const animation = await hydrateAnimationController(nav.animationCtrl);
return popToRoot(nav, nav.delegate, animation, opts, escapeHatch);
}
export async function popToImpl(nav: Nav, indexOrViewCtrl: any, opts: NavOptions, escapeHatch: any) {
export async function popToImpl(nav: Nav, indexOrViewCtrl: any, opts: NavOptions, escapeHatch: EscapeHatch) {
const animation = await hydrateAnimationController(nav.animationCtrl);
return popTo(nav, nav.delegate, animation, indexOrViewCtrl, opts, escapeHatch);
}
export async function removeImpl(nav: Nav, startIndex: number, removeCount: number, opts: NavOptions, escapeHatch: any) {
export async function removeImpl(nav: Nav, startIndex: number, removeCount: number, opts: NavOptions, escapeHatch: EscapeHatch) {
const animation = await hydrateAnimationController(nav.animationCtrl);
return remove(nav, nav.delegate, animation, startIndex, removeCount, opts, escapeHatch);
}
@ -381,9 +392,9 @@ export async function removeViewImpl(nav: Nav, viewController: PublicViewControl
return removeView(nav, nav.delegate, animation, viewController as ViewController, opts, escapeHatch);
}
export async function setPagesImpl(nav: Nav, componentDataPairs: ComponentDataPair[], opts: NavOptions, escapeHatch: any) {
export async function setPagesImpl(nav: Nav, componentDataPairs: ComponentDataPair[], opts: NavOptions, escapeHatch: EscapeHatch) {
const animation = await hydrateAnimationController(nav.animationCtrl);
return setPages(nav, nav.delegate, animation, componentDataPairs, opts, escapeHatch);
return setPages(nav, nav.delegate, animation, componentDataPairs, opts, escapeHatch, null);
}
export function canGoBackImpl(nav: Nav) {
@ -407,11 +418,10 @@ export function hydrateAnimationController(animationController: AnimationControl
return animationController.create();
}
// public api
export function push(nav: Nav, delegate: FrameworkDelegate, animation: Animation, component: any, data: any, opts: NavOptions, escapeHatch: any): Promise<any> {
return queueTransaction({
export function push(nav: Nav, delegate: FrameworkDelegate, animation: Animation, component: any, data: any, opts: NavOptions, escapeHatch: EscapeHatch): Promise<NavResult> {
return preprocessTransaction({
component: component,
insertStart: -1,
insertViews: [{component, data}],
@ -421,12 +431,12 @@ export function push(nav: Nav, delegate: FrameworkDelegate, animation: Animation
id: nav.navId,
animation,
escapeHatch,
method: 'push'
method: PUSH
});
}
export function insert(nav: Nav, delegate: FrameworkDelegate, animation: Animation, insertIndex: number, component: any, data: any, opts: NavOptions, escapeHatch: any): Promise<any> {
return queueTransaction({
export function insert(nav: Nav, delegate: FrameworkDelegate, animation: Animation, insertIndex: number, component: any, data: any, opts: NavOptions, escapeHatch: EscapeHatch): Promise<NavResult> {
return preprocessTransaction({
component: component,
insertStart: insertIndex,
insertViews: [{ component, data }],
@ -440,8 +450,8 @@ export function insert(nav: Nav, delegate: FrameworkDelegate, animation: Animati
});
}
export function insertPages(nav: Nav, delegate: FrameworkDelegate, animation: Animation, insertIndex: number, insertPages: any[], opts: NavOptions, escapeHatch: any): Promise<any> {
return queueTransaction({
export function insertPages(nav: Nav, delegate: FrameworkDelegate, animation: Animation, insertIndex: number, insertPages: any[], opts: NavOptions, escapeHatch: EscapeHatch): Promise<NavResult> {
return preprocessTransaction({
component: null,
insertStart: insertIndex,
insertViews: insertPages,
@ -455,8 +465,8 @@ export function insertPages(nav: Nav, delegate: FrameworkDelegate, animation: An
});
}
export function pop(nav: Nav, delegate: FrameworkDelegate, animation: Animation, opts: NavOptions, escapeHatch: any): Promise<any> {
return queueTransaction({
export function pop(nav: Nav, delegate: FrameworkDelegate, animation: Animation, opts: NavOptions, escapeHatch: EscapeHatch): Promise<NavResult> {
return preprocessTransaction({
component: null,
removeStart: -1,
removeCount: 1,
@ -466,12 +476,12 @@ export function pop(nav: Nav, delegate: FrameworkDelegate, animation: Animation,
id: nav.navId,
animation,
escapeHatch,
method: 'pop'
method: POP
});
}
export function popToRoot(nav: Nav, delegate: FrameworkDelegate, animation: Animation, opts: NavOptions, escapeHatch: any): Promise<any> {
return queueTransaction({
export function popToRoot(nav: Nav, delegate: FrameworkDelegate, animation: Animation, opts: NavOptions, escapeHatch: EscapeHatch): Promise<NavResult> {
return preprocessTransaction({
component: null,
removeStart: 1,
removeCount: -1,
@ -485,7 +495,7 @@ export function popToRoot(nav: Nav, delegate: FrameworkDelegate, animation: Anim
});
}
export function popTo(nav: Nav, delegate: FrameworkDelegate, animation: Animation, indexOrViewCtrl: any, opts: NavOptions, escapeHatch: any): Promise<any> {
export function popTo(nav: Nav, delegate: FrameworkDelegate, animation: Animation, indexOrViewCtrl: any, opts: NavOptions, escapeHatch: EscapeHatch): Promise<NavResult> {
const config: TransitionInstruction = {
component: null,
removeStart: -1,
@ -504,11 +514,11 @@ export function popTo(nav: Nav, delegate: FrameworkDelegate, animation: Animatio
} else if (isNumber(indexOrViewCtrl)) {
config.removeStart = indexOrViewCtrl + 1;
}
return queueTransaction(config);
return preprocessTransaction(config);
}
export function remove(nav: Nav, delegate: FrameworkDelegate, animation: Animation, startIndex: number, removeCount = 1, opts: NavOptions, escapeHatch: any): Promise<any> {
return queueTransaction({
export function remove(nav: Nav, delegate: FrameworkDelegate, animation: Animation, startIndex: number, removeCount = 1, opts: NavOptions, escapeHatch: EscapeHatch): Promise<NavResult> {
return preprocessTransaction({
component: null,
removeStart: startIndex,
removeCount: removeCount,
@ -522,8 +532,8 @@ export function remove(nav: Nav, delegate: FrameworkDelegate, animation: Animati
});
}
export function removeView(nav: Nav, delegate: FrameworkDelegate, animation: Animation, viewController: ViewController, opts: NavOptions, escapeHatch: any): Promise<any> {
return queueTransaction({
export function removeView(nav: Nav, delegate: FrameworkDelegate, animation: Animation, viewController: ViewController, opts: NavOptions, escapeHatch: EscapeHatch): Promise<NavResult> {
return preprocessTransaction({
component: null,
removeView: viewController,
removeStart: 0,
@ -538,18 +548,18 @@ export function removeView(nav: Nav, delegate: FrameworkDelegate, animation: Ani
});
}
export function setRoot(nav: Nav, delegate: FrameworkDelegate, animation: Animation, component: any, data: any, opts: NavOptions, escapeHatch: any): Promise<any> {
return setPages(nav, delegate, animation, [{ component, data }], opts, escapeHatch);
export function setRoot(nav: Nav, delegate: FrameworkDelegate, animation: Animation, component: any, data: any, opts: NavOptions, escapeHatch: EscapeHatch): Promise<NavResult> {
return setPages(nav, delegate, animation, [{ component, data }], opts, escapeHatch, SET_ROOT);
}
export function setPages(nav: Nav, delegate: FrameworkDelegate, animation: Animation, componentDataPairs: ComponentDataPair[], opts: NavOptions, escapeHatch: any): Promise<any> {
export function setPages(nav: Nav, delegate: FrameworkDelegate, animation: Animation, componentDataPairs: ComponentDataPair[], opts: NavOptions, escapeHatch: EscapeHatch, methodName: string): Promise<NavResult> {
if (!isDef(opts)) {
opts = {};
}
if (opts.animate !== true) {
opts.animate = false;
}
return queueTransaction({
return preprocessTransaction({
component: componentDataPairs.length === 1 ? componentDataPairs[0].component : null,
insertStart: 0,
insertViews: componentDataPairs,
@ -561,22 +571,49 @@ export function setPages(nav: Nav, delegate: FrameworkDelegate, animation: Anima
id: nav.navId,
animation,
escapeHatch,
method: 'setPages'
method: methodName ? methodName : 'setPages'
});
}
// private api, exported for testing
export function preprocessTransaction(ti: TransitionInstruction): Promise<NavResult> {
if (isUrl(ti.component)) {
if (ti.method === PUSH || ti.method === POP || ti.method === SET_ROOT) {
return navigateToUrl(ti.nav, normalizeUrl(ti.component) as string, ti.method);
} else {
return Promise.reject(new Error('only push, pop, and setRoot methods support urls'));
}
}
export function queueOrNavigate(ti: TransitionInstruction): void | Promise<NavResult> {
if (isComponentUrl(ti.component)) {
urlMap.set(ti.component as string, ti);
window.location.href = ti.component;
const response = checkIfPopRedirectRequired(ti);
if (response.required) {
return navigateToUrl(ti.nav, response.url, POP);
}
return queueTransaction(ti);
}
export function isComponentUrl(component: any) {
return component && typeof component === 'string' && component.charAt(0) === '/';
export function isUrl(component: any): boolean {
return typeof component === 'string' && component.charAt(0) === '/';
}
export function navigateToUrl(nav: Nav, url: string, method: string): Promise<NavResult> {
let routingPromise: Promise<any> = null;
return new Promise((resolve, reject) => {
nav.urlExternalNavMap.set(url, {
url,
method,
resolve,
reject
});
if (!nav.routerDelegate) {
nav.routerDelegate = new DomRouterDelegate();
}
routingPromise = nav.routerDelegate.pushUrlState(url);
}).then((navResult: NavResult) => {
return routingPromise.then(() => {
return navResult;
});
});
}
export function queueTransaction(ti: TransitionInstruction): Promise<NavResult> {
@ -617,16 +654,68 @@ export function nextTransaction(nav: Nav): Promise<any> {
const topTransaction = getTopTransaction(nav.navId);
if (!topTransaction) {
// cool, there are no transitions going for this nav
processAllTransitionCompleteQueue(nav.navId);
return Promise.resolve();
}
return doNav(nav, topTransaction);
}
export function checkIfPopRedirectRequired(ti: TransitionInstruction): IsRedirectRequired {
if (ti.method === POP) {
if (ti.escapeHatch.fromExternalRouter) {
// if the pop method is called from a router, that means the redirect already happened
// so just do a normal pop because the url is in a good place. Basically, the router is telling us to
// pop
return {
required: false
};
}
// check if we need to redirect to a url for the pop operation
const popToIndex = ti.nav.views.length - 2;
if (popToIndex >= 0) {
const viewController = ti.nav.views[popToIndex];
return {
required: viewController.fromExternalRouter,
url: viewController.url
};
}
}
return {
required: false,
};
}
export function processAllTransitionCompleteQueue(navId: number) {
const queue = allTransitionsCompleteHandlerQueue.get(navId) || [];
for (const callback of queue) {
callback();
}
allTransitionsCompleteHandlerQueue.set(navId, []);
}
export function allTransitionsCompleteImpl(nav: Nav) {
return new Promise((resolve) => {
const queue = transitionQueue.get(nav.navId) || [];
if (queue.length) {
// there are pending transitions, so queue it up and we'll be notified when it's done
const handlers = allTransitionsCompleteHandlerQueue.get(nav.navId) || [];
handlers.push(resolve);
return allTransitionsCompleteHandlerQueue.set(nav.navId, handlers);
}
// there are no pending transitions, so just resolve right away
return resolve();
});
}
export function doNav(nav: Nav, ti: TransitionInstruction) {
let enteringView: ViewController;
let leavingView: ViewController;
return initializeViewBeforeTransition(nav, ti).then(([_enteringView, _leavingView]) => {
return initializeViewBeforeTransition(ti).then(([_enteringView, _leavingView]) => {
enteringView = _enteringView;
leavingView = _leavingView;
return attachViewToDom(nav, enteringView, ti);
@ -647,8 +736,8 @@ export function successfullyTransitioned(ti: TransitionInstruction) {
return fireError(new Error('Queue is null, the nav must have been destroyed'), ti);
}
ti.nav.isViewInitialized = true;
ti.nav.transitionId = null;
ti.nav.initialized = true;
ti.nav.transitionId = NOT_TRANSITIONING_TRANSITION_ID;
ti.nav.transitioning = false;
// TODO - check if it's a swipe back
@ -739,7 +828,7 @@ export function loadViewAndTransition(nav: Nav, enteringView: ViewController, le
export function executeAsyncTransition(nav: Nav, transition: Transition, enteringView: ViewController, leavingView: ViewController, delegate: FrameworkDelegate, opts: NavOptions, configShouldAnimate: boolean): Promise<void> {
assert(nav.transitioning, 'must be transitioning');
nav.transitionId = null;
nav.transitionId = NOT_TRANSITIONING_TRANSITION_ID;
setZIndex(nav, enteringView, leavingView, opts.direction);
// always ensure the entering view is viewable
@ -751,8 +840,8 @@ export function executeAsyncTransition(nav: Nav, transition: Transition, enterin
// ******** DOM WRITE ****************
leavingView && toggleHidden(leavingView.element, false);
const isFirstPage = !nav.isViewInitialized && nav.views.length === 1;
const shouldNotAnimate = isFirstPage && !nav.isPortal;
const isFirstPage = !nav.initialized && nav.views.length === 1;
const shouldNotAnimate = isFirstPage;
if (configShouldAnimate || shouldNotAnimate) {
opts.animate = false;
}
@ -847,7 +936,7 @@ export function cleanUpView(nav: Nav, delegate: FrameworkDelegate, activeViewCon
// this view comes after the active view
inactiveViewController.willUnload();
promises.push(destroyView(nav, delegate, inactiveViewController));
} else if ( i < activeIndex && !nav.isPortal) {
} else if ( i < activeIndex) {
// this view comes before the active view
// and it is not a portal then ensure it is hidden
toggleHidden(inactiveViewController.element, true);
@ -875,11 +964,11 @@ export function attachViewToDom(nav: Nav, enteringView: ViewController, ti: Tran
return Promise.resolve();
}
export function initializeViewBeforeTransition(nav: Nav, ti: TransitionInstruction): Promise<ViewController[]> {
export function initializeViewBeforeTransition(ti: TransitionInstruction): Promise<ViewController[]> {
let leavingView: ViewController = null;
let enteringView: ViewController = null;
return startTransaction(ti).then(() => {
const viewControllers = convertComponentToViewController(nav, ti);
const viewControllers = convertComponentToViewController(ti);
ti.viewControllers = viewControllers;
leavingView = ti.nav.getActive() as ViewController;
enteringView = getEnteringView(ti, ti.nav, leavingView);
@ -925,7 +1014,7 @@ export function updateNavStacks(enteringView: ViewController, leavingView: ViewC
const finalBalance = ti.nav.views.length + (ti.insertViews ? ti.insertViews.length : 0) - (ti.removeCount ? ti.removeCount : 0);
assert(finalBalance >= 0, 'final balance can not be negative');
if (finalBalance === 0 && !ti.nav.isPortal) {
if (finalBalance === 0) {
console.warn(`You can't remove all the pages in the navigation stack. nav.pop() is probably called too many times.`);
throw new Error('Navigation stack needs at least one root page');
}
@ -1001,7 +1090,7 @@ export function insertViewIntoNav(nav: Nav, view: ViewController, index: number)
assert(view.nav === nav, 'view is not part of the nav');
nav.views.splice(index, 0, nav.views.splice(existingIndex, 1)[0]);
} else {
assert(!view.nav || (nav.isPortal && view.nav === nav), 'nav is used');
assert(!view.nav, 'nav is used');
// this is a new view to add to the stack
// create the new entering view
view.nav = nav;
@ -1117,17 +1206,18 @@ export function getEnteringView(ti: TransitionInstruction, nav: Nav, leavingView
return null;
}
export function convertViewsToViewControllers(pairs: ComponentDataPair[]): ViewController[] {
export function convertViewsToViewControllers(pairs: ComponentDataPair[], escapeHatch: EscapeHatch): ViewController[] {
return pairs.filter(pair => !!pair)
.map(pair => {
return new ViewController(pair.component, pair.data);
const applyEscapeHatch = pair === pairs[pairs.length - 1];
return new ViewController(pair.component, pair.data, applyEscapeHatch ? escapeHatch.fromExternalRouter : false, applyEscapeHatch ? escapeHatch.url : null);
});
}
export function convertComponentToViewController(_: Nav, ti: TransitionInstruction): ViewController[] {
export function convertComponentToViewController(ti: TransitionInstruction): ViewController[] {
if (ti.insertViews) {
assert(ti.insertViews.length > 0, 'length can not be zero');
const viewControllers = convertViewsToViewControllers(ti.insertViews);
const viewControllers = convertViewsToViewControllers(ti.insertViews, ti.escapeHatch);
assert(ti.insertViews.length === viewControllers.length, 'lengths does not match');
if (viewControllers.length === 0) {
throw new Error('No views to insert');
@ -1147,17 +1237,17 @@ export function convertComponentToViewController(_: Nav, ti: TransitionInstructi
}
export function addToQueue(ti: TransitionInstruction) {
const list = queueMap.get(ti.id) || [];
const list = transitionQueue.get(ti.id) || [];
list.push(ti);
queueMap.set(ti.id, list);
transitionQueue.set(ti.id, list);
}
export function getQueue(id: number) {
return queueMap.get(id) || [];
return transitionQueue.get(id) || [];
}
export function resetQueue(id: number) {
queueMap.set(id, []);
transitionQueue.set(id, []);
}
export function getTopTransaction(id: number) {
@ -1167,7 +1257,7 @@ export function getTopTransaction(id: number) {
}
const tmp = queue.concat();
const toReturn = tmp.shift();
queueMap.set(id, tmp);
transitionQueue.set(id, tmp);
return toReturn;
}
@ -1177,6 +1267,7 @@ export function getDefaultTransition(config: Config) {
let viewIds = VIEW_ID_START;
const DISABLE_APP_MINIMUM_DURATION = 64;
const NOT_TRANSITIONING_TRANSITION_ID = -1;
export interface NavEvent extends CustomEvent {
target: HTMLIonNavElement;
@ -1186,3 +1277,18 @@ export interface NavEvent extends CustomEvent {
export interface NavEventDetail {
isPop?: boolean;
}
export function getDefaultEscapeHatch(): EscapeHatch {
return {
fromExternalRouter: false,
};
}
export interface IsRedirectRequired {
required: boolean;
url?: string;
}
export const POP = 'pop';
export const PUSH = 'push';
export const SET_ROOT = 'setRoot';

View File

@ -27,6 +27,11 @@ string
any
#### routerDelegate
#### swipeBackEnabled
boolean
@ -59,6 +64,11 @@ string
any
#### router-delegate
#### swipe-back-enabled
boolean
@ -82,9 +92,6 @@ boolean
#### canGoBack()
#### clearTransitionInfoForUrl()
#### first()
@ -106,9 +113,6 @@ boolean
#### getRouteId()
#### getTransitionInfoForUrl()
#### getViews()
@ -124,6 +128,9 @@ boolean
#### last()
#### onAllTransitionsComplete()
#### pop()

View File

@ -14,10 +14,10 @@ const SHOW_BACK_BTN_CSS = 'show-back-button';
export function buildIOSTransition(rootTransition: Transition, enteringView: ViewController, leavingView: ViewController, opts: AnimationOptions): Promise<Transition> {
const componentReadyPromise: Promise<any>[] = [];
// Let makes sure everything is hydrated and ready to animate
if (enteringView) {
if (enteringView && (enteringView.element as any).componentOnReady) {
componentReadyPromise.push((enteringView.element as any).componentOnReady());
}
if (leavingView) {
if (leavingView && (leavingView.element as any).componentOnReady) {
componentReadyPromise.push((leavingView.element as any).componentOnReady());
}

View File

@ -10,10 +10,10 @@ const SHOW_BACK_BTN_CSS = 'show-back-button';
export function buildMdTransition(rootTransition: Transition, enteringView: ViewController, leavingView: ViewController, opts: AnimationOptions): Promise<Transition> {
const componentReadyPromise: Promise<any>[] = [];
if (enteringView) {
if (enteringView && (enteringView.element as any).componentOnReady) {
componentReadyPromise.push((enteringView.element as any).componentOnReady());
}
if (leavingView) {
if (leavingView && (leavingView.element as any).componentOnReady) {
componentReadyPromise.push((leavingView.element as any).componentOnReady());
}
@ -32,8 +32,7 @@ export function buildMdTransition(rootTransition: Transition, enteringView: View
// animate the component itself
if (backDirection) {
rootTransition.duration(isDef(opts.duration) ? opts.duration : 200).easing('cubic-bezier(0.47,0,0.745,0.715)');
}
else {
} else {
rootTransition.duration(isDef(opts.duration) ? opts.duration : 280).easing('cubic-bezier(0.36,0.66,0.04,1)');
rootTransition

View File

@ -1,7 +1,10 @@
import { FrameworkDelegate, Nav, PublicViewController } from '../../index';
import { STATE_ATTACHED, STATE_DESTROYED, STATE_INITIALIZED, STATE_NEW } from './nav-utils';
import { assert } from '../../utils/helpers';
import {
assert,
normalizeUrl
} from '../../utils/helpers';
export class ViewController implements PublicViewController {
@ -15,13 +18,15 @@ export class ViewController implements PublicViewController {
zIndex: number;
dismissProxy: any;
timestamp: number;
fromExternalRouter: boolean;
url: string;
onDidDismiss: (data: any, role: string) => void;
onWillDismiss: (data: any, role: string) => void;
constructor(public component: any, data?: any) {
initializeNewViewController(this, data);
constructor(public component: any, data: any, fromExternalRouter: boolean, url: string) {
initializeNewViewController(this, data, fromExternalRouter, url);
}
/**
@ -159,8 +164,10 @@ export function didLoadImpl(viewController: ViewController) {
callLifeCycleFunction(viewController.instance, 'ionViewDidLoad');
}
export function initializeNewViewController(viewController: ViewController, data: any) {
export function initializeNewViewController(viewController: ViewController, data: any, fromExternalRouter: boolean, url: string) {
viewController.timestamp = Date.now();
viewController.state = STATE_NEW;
viewController.data = data || {};
viewController.fromExternalRouter = fromExternalRouter;
viewController.url = url && normalizeUrl(url);
}

View File

@ -1,5 +1,5 @@
export interface NavOutlet {
setRouteId(id: any, data?: any): Promise<void>;
setRouteId(id: any, data?: any): Promise<any>;
getRouteId(): string;
getContentElement(): HTMLElement | null;
}

View File

@ -1,5 +1,4 @@
import { Component, Element, Event, EventEmitter, Method, Prop, State, Watch } from '@stencil/core';
import { getNavAsChildIfExists } from '../../utils/helpers';
import { FrameworkDelegate } from '../..';
@ -9,7 +8,7 @@ import { FrameworkDelegate } from '../..';
})
export class Tab {
private loaded = false;
// private loaded = false;
@Element() el: HTMLElement;
@State() init = false;
@ -66,6 +65,7 @@ export class Tab {
@Prop({ mutable: true }) selected = false;
@Watch('selected')
selectedChanged(selected: boolean) {
if (selected) {
@ -81,7 +81,30 @@ export class Tab {
@Method()
setActive(active: boolean): Promise<any> {
this.active = active;
if (this.loaded || !active) {
if (active) {
const nav = getNavAsChildIfExists(this.el);
if (nav) {
// the tab's nav has been initialized externally
if ((window as any).externalNavPromise) {
return (window as any).externalNavPromise.then(() => {
(window as any).externalNavPromise = null;
});
} else {
// the tab's nav has not been initialized externally, so
// check if we need to initiailize it
return (nav as any).componentOnReady().then(() => {
return nav.onAllTransitionsComplete();
}).then(() => {
if (!nav.getViews().length && !nav.isTransitioning() && !nav.initialized) {
return nav.setRoot(nav.root);
}
return Promise.resolve();
});
}
}
}
/*if (this.loaded || !active) {
return Promise.resolve();
}
@ -97,6 +120,8 @@ export class Tab {
promise = Promise.resolve();
}
return promise.then(() => this.fireChildren());
*/
return Promise.resolve();
}
@Method()
@ -110,7 +135,7 @@ export class Tab {
return null;
}
private fireChildren() {
/*private fireChildren() {
const nav = getNavAsChildIfExists(this.el);
if (nav && nav.getViews().length === 0 && nav.root) {
// we need to initialize
@ -119,6 +144,7 @@ export class Tab {
// it's already been initialized if it exists
return Promise.resolve();
}
*/
hostData() {
const visible = this.active && this.selected;
@ -127,7 +153,7 @@ export class Tab {
'aria-labelledby': this.btnId,
'role': 'tabpanel',
class: {
'show-tab': this.active
'show-tab': visible
}
};
}
@ -137,14 +163,15 @@ export class Tab {
}
}
function attachViewToDom(container: HTMLElement, cmp: string): Promise<any> {
/*function attachViewToDom(container: HTMLElement, cmp: string): Promise<any> {
const el = document.createElement(cmp) as HTMLStencilElement;
container.appendChild(el);
if (el.componentOnReady ) {
if (el.componentOnReady) {
return el.componentOnReady();
}
return Promise.resolve();
}
*/
export interface TabEvent extends CustomEvent {
detail: TabEventDetail;

View File

@ -9,6 +9,7 @@ import { Config, NavEventDetail, NavOutlet } from '../../index';
export class Tabs implements NavOutlet {
private ids = -1;
private tabsId: number = (++tabIds);
initialized = false;
@Element() el: HTMLElement;
@ -70,8 +71,14 @@ export class Tabs implements NavOutlet {
this.loadConfig('tabsLayout', 'icon-top');
this.loadConfig('tabsHighlight', true);
this.initTabs();
this.initSelect();
return this.initTabs().then(() => {
if (! (window as any).externalNav) {
return this.initSelect();
}
return null;
}).then(() => {
this.initialized = true;
});
}
componentDidUnload() {
@ -171,16 +178,19 @@ export class Tabs implements NavOutlet {
private initTabs() {
const tabs = this.tabs = Array.from(this.el.querySelectorAll('ion-tab'));
const tabPromises: Promise<any>[] = [];
for (const tab of tabs) {
const id = `t-${this.tabsId}-${++this.ids}`;
tab.btnId = 'tab-' + id;
tab.id = 'tabpanel-' + id;
tabPromises.push((tab as any).componentOnReady());
}
return Promise.all(tabPromises);
}
private initSelect() {
if (document.querySelector('ion-router')) {
return;
return Promise.resolve();
}
// find pre-selected tabs
const selectedTab = this.tabs.find(t => t.selected) ||
@ -192,10 +202,10 @@ export class Tabs implements NavOutlet {
tab.selected = false;
}
}
if (selectedTab) {
selectedTab.setActive(true);
}
this.selectedTab = selectedTab;
const promise = selectedTab ? selectedTab.setActive(true) : Promise.resolve();
return promise.then(() => {
this.selectedTab = selectedTab;
});
}
private loadConfig(attrKey: string, fallback: any) {

View File

@ -146,6 +146,11 @@ export interface FrameworkDelegate {
removeViewFromDom(elementOrContainerToUnmountFrom: any, elementOrComponentToUnmount: any, escapeHatch?: any): Promise<void>;
}
export interface RouterDelegate {
pushUrlState(urlSegment: string, stateObject?: any, title?: string): Promise<any>;
popUrlState(): Promise<any>;
}
export interface FrameworkMountingData {
element: HTMLElement;
component: any;

View File

@ -28,12 +28,4 @@ export class DomFrameworkDelegate implements FrameworkDelegate {
parentElement.removeChild(childElement);
return Promise.resolve();
}
shouldDeferToRouter(_elementOrComponentToMount: any): Promise<boolean> {
return Promise.resolve(false);
}
routeToUrl(_elementOrComponentToMount: any): Promise<any> {
return Promise.resolve('todo');
}
}

View File

@ -0,0 +1,16 @@
import { RouterDelegate } from '../index';
export class DomRouterDelegate implements RouterDelegate {
pushUrlState(urlSegment: string, stateObject: any = null, title = ''): Promise<any> {
history.pushState(stateObject, title, urlSegment);
return Promise.resolve();
}
popUrlState() {
history.back();
return Promise.resolve();
}
}

View File

@ -303,3 +303,16 @@ export function getNavAsChildIfExists(element: HTMLElement): HTMLIonNavElement|n
}
return null;
}
export function normalizeUrl(url: string) {
url = url.trim();
if (url.charAt(0) !== '/') {
// ensure first char is a /
url = '/' + url;
}
if (url.length > 1 && url.charAt(url.length - 1) === '/') {
// ensure last char is not a /
url = url.substr(0, url.length - 1);
}
return url;
}

View File

@ -1,8 +1,8 @@
{
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"allowUnreachableCode": false,
"alwaysStrict": true,
"declaration": true,
"experimentalDecorators": true,
"forceConsistentCasingInFileNames": true,
"jsx": "react",