refactor(external-router): remove the external router controller, move the reconciliation methods to the nav itself, and move the external activation status information to ion-app

This commit is contained in:
Dan Bucholtz
2018-02-15 23:14:09 -06:00
parent 902c4c7f72
commit 3f6e1ddbcd
13 changed files with 213 additions and 217 deletions

View File

@ -28,7 +28,7 @@ import { OutletInjector } from './outlet-injector';
import { RouteEventHandler } from './route-event-handler'; import { RouteEventHandler } from './route-event-handler';
import { AngularComponentMounter, AngularEscapeHatch } from '..'; import { AngularComponentMounter, AngularEscapeHatch } from '..';
import { ensureExternalRounterController } from '../util/util'; import { getIonApp } from '../util/util';
let id = 0; let id = 0;
@ -139,9 +139,14 @@ export class RouterOutlet implements OnDestroy, OnInit, RouterDelegate {
export function activateRoute(navElement: HTMLIonNavElement, export function activateRoute(navElement: HTMLIonNavElement,
component: Type<any>, data: any = {}, cfr: ComponentFactoryResolver, injector: Injector, isTopLevel: boolean): Promise<void> { component: Type<any>, data: any = {}, cfr: ComponentFactoryResolver, injector: Injector, isTopLevel: boolean): Promise<void> {
return ensureExternalRounterController().then((externalRouterController) => { return getIonApp().then((ionApp) => {
if (!ionApp) {
return Promise.reject(new Error(`<ion-app> element is required for angular router integration`));
}
const escapeHatch = getEscapeHatch(cfr, injector); const escapeHatch = getEscapeHatch(cfr, injector);
return externalRouterController.reconcileNav(navElement, component, data, escapeHatch, isTopLevel); return navElement.componentOnReady().then(() => {
return navElement.reconcileFromExternalRouter(component, data, escapeHatch, isTopLevel);
});
}); });
} }

View File

@ -5,7 +5,7 @@ import {
Router Router
} from '@angular/router'; } from '@angular/router';
import { ensureExternalRounterController } from '../util/util'; import { getIonApp } from '../util/util';
@Injectable() @Injectable()
export class RouteEventHandler { export class RouteEventHandler {
@ -14,16 +14,16 @@ export class RouteEventHandler {
router.events.subscribe((event: Event) => { router.events.subscribe((event: Event) => {
if (event instanceof NavigationEnd) { if (event instanceof NavigationEnd) {
ensureExternalRounterController().then((element) => { getIonApp().then((appElement) => {
element.updateExternalNavOccuring(false); appElement.updateExternalNavOccuring(false);
}); });
} }
}); });
} }
externalNavStart() { externalNavStart() {
return ensureExternalRounterController().then((element) => { return getIonApp().then((appElement) => {
element.updateExternalNavOccuring(true); appElement.updateExternalNavOccuring(true);
}); });
} }
} }

View File

@ -26,12 +26,7 @@ export function isString(something: any) {
return typeof something === 'string' ? true : false; return typeof something === 'string' ? true : false;
} }
export function ensureExternalRounterController(): Promise<HTMLIonExternalRouterControllerElement> { export function getIonApp(): Promise<HTMLIonAppElement> {
const element = document.querySelector('ion-external-router-controller'); const element = ensureElementInBody('ion-app') as HTMLIonAppElement;
if (element) { return element.componentOnReady();
return (element as any).componentOnReady();
}
const toCreate = document.createElement('ion-external-router-controller');
document.body.appendChild(toCreate);
return (toCreate as any).componentOnReady();
} }

View File

@ -875,36 +875,6 @@ declare global {
} }
import {
ExternalRouterController as IonExternalRouterController
} from './components/external-router-controller/external-router-controller';
declare global {
interface HTMLIonExternalRouterControllerElement extends IonExternalRouterController, HTMLStencilElement {
}
var HTMLIonExternalRouterControllerElement: {
prototype: HTMLIonExternalRouterControllerElement;
new (): HTMLIonExternalRouterControllerElement;
};
interface HTMLElementTagNameMap {
"ion-external-router-controller": HTMLIonExternalRouterControllerElement;
}
interface ElementTagNameMap {
"ion-external-router-controller": HTMLIonExternalRouterControllerElement;
}
namespace JSX {
interface IntrinsicElements {
"ion-external-router-controller": JSXElements.IonExternalRouterControllerAttributes;
}
}
namespace JSXElements {
export interface IonExternalRouterControllerAttributes extends HTMLAttributes {
}
}
}
import { import {
FabButton as IonFabButton FabButton as IonFabButton
} from './components/fab-button/fab-button'; } from './components/fab-button/fab-button';

View File

@ -30,6 +30,49 @@ export class App {
@Prop({ context: 'config' }) config: Config; @Prop({ context: 'config' }) config: Config;
externalNavPromise: void | Promise<any> = null;
externalNavOccuring = false;
/**
* Returns the promise set by an external navigation system
* This API is not meant for public usage and could
* change at any time
*/
@Method()
getExternalNavPromise(): void | Promise<any> {
return this.externalNavPromise;
}
/**
* Updates the Promise set by an external navigation system
* This API is not meant for public usage and could
* change at any time
*/
@Method()
setExternalNavPromise(value: null | Promise<any>): void {
this.externalNavPromise = value;
}
/**
* Returns whether an external navigation event is occuring
* This API is not meant for public usage and could
* change at any time
*/
@Method()
getExternalNavOccuring(): boolean {
return this.externalNavOccuring;
}
/**
* Updates whether an external navigation event is occuring
* This API is not meant for public usage and could
* change at any time
*/
@Method()
updateExternalNavOccuring(status: boolean) {
this.externalNavOccuring = status;
}
componentWillLoad() { componentWillLoad() {
this.modeCode = this.config.get('mode'); this.modeCode = this.config.get('mode');
this.useRouter = this.config.getBoolean('useRouter', false); this.useRouter = this.config.getBoolean('useRouter', false);

View File

@ -12,6 +12,20 @@
## Methods ## Methods
#### getExternalNavOccuring()
Returns whether an external navigation event is occuring
This API is not meant for public usage and could
change at any time
#### getExternalNavPromise()
Returns the promise set by an external navigation system
This API is not meant for public usage and could
change at any time
#### getNavByIdOrName() #### getNavByIdOrName()
@ -33,9 +47,23 @@ Returns whether the application is enabled or not
Boolean if the app is actively scrolling or not. Boolean if the app is actively scrolling or not.
#### setExternalNavPromise()
Updates the Promise set by an external navigation system
This API is not meant for public usage and could
change at any time
#### setScrolling() #### setScrolling()
#### updateExternalNavOccuring()
Updates whether an external navigation event is occuring
This API is not meant for public usage and could
change at any time
---------------------------------------------- ----------------------------------------------

View File

@ -1,119 +0,0 @@
import { Component, Method } from '@stencil/core';
import { EscapeHatch, NavResult } from '../../index';
@Component({
tag: 'ion-external-router-controller'
})
export class ExternalRouterController {
externalNavPromise: void | Promise<any> = null;
externalNavOccuring = false;
@Method()
getExternalNavPromise(): void | Promise<any> {
return this.externalNavPromise;
}
@Method()
clearExternalNavPromise(): void {
this.externalNavPromise = null;
}
@Method()
getExternalNavOccuring(): boolean {
return this.externalNavOccuring;
}
@Method()
updateExternalNavOccuring(status: boolean) {
this.externalNavOccuring = status;
}
@Method()
reconcileNav(nav: HTMLIonNavElement, component: any, data: any = {}, escapeHatch: EscapeHatch, isTopLevel: boolean) {
return nav.componentOnReady().then(() => {
// check if the nav has an `<ion-tab>` as a parent
if (isParentTab(nav)) {
// check if the tab is selected
return updateTab(this, nav, component, data, escapeHatch, isTopLevel);
} else {
return updateNav(nav, component, data, escapeHatch, isTopLevel);
}
});
}
}
function isParentTab(navElement: HTMLIonNavElement) {
return navElement.parentElement.tagName.toLowerCase() === 'ion-tab';
}
function updateTab(externalRouterController: ExternalRouterController, navElement: HTMLIonNavElement, component: any, data: any, escapeHatch: EscapeHatch, isTopLevel: boolean) {
const tab = navElement.parentElement as HTMLIonTabElement;
// yeah yeah, I know this is kind of ugly but oh well, I know the internal structure of <ion-tabs>
const tabs = tab.parentElement.parentElement as HTMLIonTabsElement;
return isTabSelected(tabs, tab).then((isSelected: boolean) => {
if (!isSelected) {
const promise = updateNav(navElement, component, data, escapeHatch, isTopLevel);
externalRouterController.externalNavPromise = promise;
// okay, the tab is not selected, so we need to do a "switch" transition
// basically, we should update the nav, and then swap the tabs
return promise.then(() => {
return tabs.select(tab);
});
}
// okay cool, the tab is already selected, so we want to see a transition
return updateNav(navElement, component, data, escapeHatch, isTopLevel);
});
}
function isTabSelected(tabsElement: HTMLIonTabsElement, tabElement: HTMLIonTabElement ): Promise<boolean> {
const promises: Promise<any>[] = [];
promises.push(tabsElement.componentOnReady());
promises.push(tabElement.componentOnReady());
return Promise.all(promises).then(() => {
return tabsElement.getSelected() === tabElement;
});
}
function updateNav(navElement: HTMLIonNavElement,
component: any, data: any, escapeHatch: EscapeHatch, isTopLevel: boolean): Promise<NavResult> {
// check if the component is the top view
const activeViews = navElement.getViews();
if (activeViews.length === 0) {
// there isn't a view in the stack, so push one
return navElement.setRoot(component, data, {}, escapeHatch);
}
const currentView = activeViews[activeViews.length - 1];
if (currentView.component === component) {
// the top view is already the component being activated, so there is no change needed
return Promise.resolve(null);
}
// check if the component is the previous view, if so, pop back to it
if (activeViews.length > 1) {
// there's at least two views in the stack
const previousView = activeViews[activeViews.length - 2];
if (previousView.component === component) {
// cool, we match the previous view, so pop it
return navElement.pop(null, escapeHatch);
}
}
// check if the component is already in the stack of views, in which case we pop back to it
for (const view of activeViews) {
if (view.component === component) {
// cool, we found the match, pop back to that bad boy
return navElement.popTo(view, null, escapeHatch);
}
}
// it's the top level nav, and it's not one of those other behaviors, so do a push so the user gets a chill animation
return navElement.push(component, data, { animate: isTopLevel }, escapeHatch);
}

View File

@ -1,28 +0,0 @@
# ion-external-router-controller
<!-- Auto Generated Below -->
## Methods
#### clearExternalNavPromise()
#### getExternalNavOccuring()
#### getExternalNavPromise()
#### reconcileNav()
#### updateExternalNavOccuring()
----------------------------------------------
*Built with [StencilJS](https://stenciljs.com/)*

View File

@ -51,6 +51,7 @@ import {
focusOutActiveElement, focusOutActiveElement,
isDef, isDef,
isNumber, isNumber,
isParentTab,
normalizeUrl, normalizeUrl,
} from '../../utils/helpers'; } from '../../utils/helpers';
@ -258,6 +259,11 @@ export class Nav implements PublicNav, NavOutlet {
return allTransitionsCompleteImpl(this); return allTransitionsCompleteImpl(this);
} }
@Method()
reconcileFromExternalRouter(component: any, data: any = {}, escapeHatch: EscapeHatch, isTopLevel: boolean) {
return reconcileFromExternalRouterImpl(this, component, data, escapeHatch, isTopLevel);
}
canSwipeBack(): boolean { canSwipeBack(): boolean {
return (this.swipeBackEnabled && return (this.swipeBackEnabled &&
// this.childNavs.length === 0 && // this.childNavs.length === 0 &&
@ -1300,6 +1306,92 @@ export function getDefaultEscapeHatch(): EscapeHatch {
}; };
} }
export function reconcileFromExternalRouterImpl(nav: Nav, component: any, data: any = {}, escapeHatch: EscapeHatch, isTopLevel: boolean) {
// check if the nav has an `<ion-tab>` as a parent
if (isParentTab(nav.element as any)) {
// check if the tab is selected
return updateTab(nav, component, data, escapeHatch, isTopLevel);
} else {
return updateNav(nav, component, data, escapeHatch, isTopLevel);
}
}
export function updateTab(nav: Nav, component: any, data: any, escapeHatch: EscapeHatch, isTopLevel: boolean) {
const tab = nav.element.parentElement as HTMLIonTabElement;
// yeah yeah, I know this is kind of ugly but oh well, I know the internal structure of <ion-tabs>
const tabs = tab.parentElement.parentElement as HTMLIonTabsElement;
return isTabSelected(tabs, tab).then((isSelected: boolean) => {
if (!isSelected) {
const promise = updateNav(nav, component, data, escapeHatch, isTopLevel);
const app = document.querySelector('ion-app');
return app.componentOnReady().then(() => {
app.setExternalNavPromise(promise);
}).then(() => {
// okay, the tab is not selected, so we need to do a "switch" transition
// basically, we should update the nav, and then swap the tabs
return promise.then(() => {
return tabs.select(tab);
});
});
}
// okay cool, the tab is already selected, so we want to see a transition
return updateNav(nav, component, data, escapeHatch, isTopLevel);
});
}
export function isTabSelected(tabsElement: HTMLIonTabsElement, tabElement: HTMLIonTabElement ): Promise<boolean> {
const promises: Promise<any>[] = [];
promises.push(tabsElement.componentOnReady());
promises.push(tabElement.componentOnReady());
return Promise.all(promises).then(() => {
return tabsElement.getSelected() === tabElement;
});
}
export function updateNav(nav: Nav,
component: any, data: any, escapeHatch: EscapeHatch, isTopLevel: boolean): Promise<NavResult> {
// check if the component is the top view
const activeViews = nav.getViews();
if (activeViews.length === 0) {
// there isn't a view in the stack, so push one
return nav.setRoot(component, data, {}, escapeHatch);
}
const currentView = activeViews[activeViews.length - 1];
if (currentView.component === component) {
// the top view is already the component being activated, so there is no change needed
return Promise.resolve(null);
}
// check if the component is the previous view, if so, pop back to it
if (activeViews.length > 1) {
// there's at least two views in the stack
const previousView = activeViews[activeViews.length - 2];
if (previousView.component === component) {
// cool, we match the previous view, so pop it
return nav.pop(null, escapeHatch);
}
}
// check if the component is already in the stack of views, in which case we pop back to it
for (const view of activeViews) {
if (view.component === component) {
// cool, we found the match, pop back to that bad boy
return nav.popTo(view, null, escapeHatch);
}
}
// it's the top level nav, and it's not one of those other behaviors, so do a push so the user gets a chill animation
return nav.push(component, data, { animate: isTopLevel }, escapeHatch);
}
export interface IsRedirectRequired { export interface IsRedirectRequired {
required: boolean; required: boolean;
url?: string; url?: string;

View File

@ -143,6 +143,9 @@ boolean
#### push() #### push()
#### reconcileFromExternalRouter()
#### removeIndex() #### removeIndex()

View File

@ -1,6 +1,8 @@
import { Component, Element, Event, EventEmitter, Method, Prop, State, Watch } from '@stencil/core'; import { Component, Element, Event, EventEmitter, Method, Prop, State, Watch } from '@stencil/core';
import { ensureExternalRounterController, getNavAsChildIfExists } from '../../utils/helpers';
import { FrameworkDelegate } from '../..'; import { FrameworkDelegate } from '../..';
import { getIonApp, getNavAsChildIfExists } from '../../utils/helpers';
@Component({ @Component({
tag: 'ion-tab', tag: 'ion-tab',
@ -81,7 +83,7 @@ export class Tab {
@Method() @Method()
prepareActive(): Promise<any> { prepareActive(): Promise<any> {
if (this.loaded) { if (this.loaded) {
return this.configChildgNav(); return this.configChildNav();
} }
this.loaded = true; this.loaded = true;
@ -94,7 +96,7 @@ export class Tab {
} else { } else {
promise = Promise.resolve(); promise = Promise.resolve();
} }
return promise.then(() => this.configChildgNav()); return promise.then(() => this.configChildNav());
} }
@Method() @Method()
@ -108,15 +110,15 @@ export class Tab {
return null; return null;
} }
private configChildgNav(): Promise<any|void> { private configChildNav(): Promise<any|void> {
const nav = getNavAsChildIfExists(this.el); const nav = getNavAsChildIfExists(this.el);
if (nav) { if (nav) {
// the tab's nav has been initialized externally // the tab's nav has been initialized externally
return ensureExternalRounterController().then<void|any>((externalRouterController) => { return getIonApp().then((ionApp) => {
const externalNavPromise = externalRouterController.getExternalNavPromise(); const externalNavPromise = ionApp ? ionApp.getExternalNavPromise() : null;
if (externalNavPromise) { if (externalNavPromise) {
return externalNavPromise.then(() => { return (externalNavPromise as any).then(() => {
externalRouterController.clearExternalNavPromise(); ionApp.setExternalNavPromise(null);
}); });
} }

View File

@ -1,7 +1,7 @@
import { Component, Element, Event, EventEmitter, Listen, Method, Prop, State } from '@stencil/core'; import { Component, Element, Event, EventEmitter, Listen, Method, Prop, State } from '@stencil/core';
import { Config, NavEventDetail, NavOutlet } from '../../index'; import { Config, NavEventDetail, NavOutlet } from '../../index';
import { asyncRaf, ensureExternalRounterController } from '../../utils/helpers'; import { asyncRaf, getIonApp } from '../../utils/helpers';
@Component({ @Component({
@ -75,10 +75,13 @@ export class Tabs implements NavOutlet {
const promises: Promise<any>[] = []; const promises: Promise<any>[] = [];
promises.push(this.initTabs()); promises.push(this.initTabs());
promises.push(ensureExternalRounterController()); promises.push(getIonApp());
return Promise.all(promises).then(([_, externalRouterController]) => { return Promise.all(promises).then(([_, ionApp]) => {
return (externalRouterController as HTMLIonExternalRouterControllerElement).getExternalNavOccuring(); if (ionApp) {
}).then((externalNavOccuring) => { return (ionApp as HTMLIonAppElement).getExternalNavOccuring();
}
return false;
}).then((externalNavOccuring: boolean) => {
if (!externalNavOccuring) { if (!externalNavOccuring) {
return this.initSelect(); return this.initSelect();
} }
@ -116,11 +119,8 @@ export class Tabs implements NavOutlet {
tab.selected = false; tab.selected = false;
} }
} }
selectedTab.selected = true;
const leavingTab = this.selectedTab; const leavingTab = this.selectedTab;
this.selectedTab = selectedTab;
return selectedTab.prepareActive() return selectedTab.prepareActive()
.then(() => selectedTab.active = true) .then(() => selectedTab.active = true)
@ -129,6 +129,8 @@ export class Tabs implements NavOutlet {
if (leavingTab) { if (leavingTab) {
leavingTab.active = false; leavingTab.active = false;
} }
selectedTab.selected = true;
this.selectedTab = selectedTab;
this.ionChange.emit(selectedTab); this.ionChange.emit(selectedTab);
this.ionNavChanged.emit({isPop: false}); this.ionNavChanged.emit({isPop: false});
}); });
@ -216,6 +218,7 @@ export class Tabs implements NavOutlet {
this.selectedTab = selectedTab; this.selectedTab = selectedTab;
if (selectedTab) { if (selectedTab) {
selectedTab.selected = true; selectedTab.selected = true;
selectedTab.active = true;
} }
}); });
} }

View File

@ -321,12 +321,14 @@ export function normalizeUrl(url: string) {
return url; return url;
} }
export function ensureExternalRounterController(): Promise<HTMLIonExternalRouterControllerElement> { export function isParentTab(element: HTMLElement) {
const element = document.querySelector('ion-external-router-controller'); return element.parentElement.tagName.toLowerCase() === 'ion-tab';
if (element) {
return (element as any).componentOnReady();
} }
const toCreate = document.createElement('ion-external-router-controller');
document.body.appendChild(toCreate); export function getIonApp(): Promise<HTMLIonAppElement> {
return (toCreate as any).componentOnReady(); const appElement = document.querySelector('ion-app');
if (!appElement) {
return Promise.resolve(null);
}
return appElement.componentOnReady();
} }