From 3f6e1ddbcd6832bad8bd490c235aea4ea5c93196 Mon Sep 17 00:00:00 2001 From: Dan Bucholtz Date: Thu, 15 Feb 2018 23:14:09 -0600 Subject: [PATCH] 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 --- packages/angular/src/router/outlet.ts | 11 +- .../angular/src/router/route-event-handler.ts | 10 +- packages/angular/src/util/util.ts | 11 +- packages/core/src/components.d.ts | 30 ----- packages/core/src/components/app/app.tsx | 43 +++++++ packages/core/src/components/app/readme.md | 28 +++++ .../external-router-controller.ts | 119 ------------------ .../external-router-controller/readme.md | 28 ----- packages/core/src/components/nav/nav.tsx | 92 ++++++++++++++ packages/core/src/components/nav/readme.md | 3 + packages/core/src/components/tab/tab.tsx | 18 +-- packages/core/src/components/tabs/tabs.tsx | 19 +-- packages/core/src/utils/helpers.ts | 18 +-- 13 files changed, 213 insertions(+), 217 deletions(-) delete mode 100644 packages/core/src/components/external-router-controller/external-router-controller.ts delete mode 100644 packages/core/src/components/external-router-controller/readme.md diff --git a/packages/angular/src/router/outlet.ts b/packages/angular/src/router/outlet.ts index 519ab4b839..d4c73ba43b 100644 --- a/packages/angular/src/router/outlet.ts +++ b/packages/angular/src/router/outlet.ts @@ -28,7 +28,7 @@ import { OutletInjector } from './outlet-injector'; import { RouteEventHandler } from './route-event-handler'; import { AngularComponentMounter, AngularEscapeHatch } from '..'; -import { ensureExternalRounterController } from '../util/util'; +import { getIonApp } from '../util/util'; let id = 0; @@ -139,9 +139,14 @@ export class RouterOutlet implements OnDestroy, OnInit, RouterDelegate { export function activateRoute(navElement: HTMLIonNavElement, component: Type, data: any = {}, cfr: ComponentFactoryResolver, injector: Injector, isTopLevel: boolean): Promise { - return ensureExternalRounterController().then((externalRouterController) => { + return getIonApp().then((ionApp) => { + if (!ionApp) { + return Promise.reject(new Error(` element is required for angular router integration`)); + } const escapeHatch = getEscapeHatch(cfr, injector); - return externalRouterController.reconcileNav(navElement, component, data, escapeHatch, isTopLevel); + return navElement.componentOnReady().then(() => { + return navElement.reconcileFromExternalRouter(component, data, escapeHatch, isTopLevel); + }); }); } diff --git a/packages/angular/src/router/route-event-handler.ts b/packages/angular/src/router/route-event-handler.ts index af1beeeac9..693a110235 100644 --- a/packages/angular/src/router/route-event-handler.ts +++ b/packages/angular/src/router/route-event-handler.ts @@ -5,7 +5,7 @@ import { Router } from '@angular/router'; -import { ensureExternalRounterController } from '../util/util'; +import { getIonApp } from '../util/util'; @Injectable() export class RouteEventHandler { @@ -14,16 +14,16 @@ export class RouteEventHandler { router.events.subscribe((event: Event) => { if (event instanceof NavigationEnd) { - ensureExternalRounterController().then((element) => { - element.updateExternalNavOccuring(false); + getIonApp().then((appElement) => { + appElement.updateExternalNavOccuring(false); }); } }); } externalNavStart() { - return ensureExternalRounterController().then((element) => { - element.updateExternalNavOccuring(true); + return getIonApp().then((appElement) => { + appElement.updateExternalNavOccuring(true); }); } } diff --git a/packages/angular/src/util/util.ts b/packages/angular/src/util/util.ts index 01917fdca5..8593a56c6f 100644 --- a/packages/angular/src/util/util.ts +++ b/packages/angular/src/util/util.ts @@ -26,12 +26,7 @@ export function isString(something: any) { return typeof something === 'string' ? true : false; } -export function ensureExternalRounterController(): Promise { - const element = document.querySelector('ion-external-router-controller'); - if (element) { - return (element as any).componentOnReady(); - } - const toCreate = document.createElement('ion-external-router-controller'); - document.body.appendChild(toCreate); - return (toCreate as any).componentOnReady(); +export function getIonApp(): Promise { + const element = ensureElementInBody('ion-app') as HTMLIonAppElement; + return element.componentOnReady(); } \ No newline at end of file diff --git a/packages/core/src/components.d.ts b/packages/core/src/components.d.ts index 6d35bf4382..9a96a6072d 100644 --- a/packages/core/src/components.d.ts +++ b/packages/core/src/components.d.ts @@ -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 { FabButton as IonFabButton } from './components/fab-button/fab-button'; diff --git a/packages/core/src/components/app/app.tsx b/packages/core/src/components/app/app.tsx index 7ad1f60c50..6396c1c288 100644 --- a/packages/core/src/components/app/app.tsx +++ b/packages/core/src/components/app/app.tsx @@ -30,6 +30,49 @@ export class App { @Prop({ context: 'config' }) config: Config; + externalNavPromise: void | Promise = 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 { + 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): 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() { this.modeCode = this.config.get('mode'); this.useRouter = this.config.getBoolean('useRouter', false); diff --git a/packages/core/src/components/app/readme.md b/packages/core/src/components/app/readme.md index f94d434fba..e74516f2d3 100644 --- a/packages/core/src/components/app/readme.md +++ b/packages/core/src/components/app/readme.md @@ -12,6 +12,20 @@ ## 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() @@ -33,9 +47,23 @@ Returns whether the application is enabled 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() +#### updateExternalNavOccuring() + +Updates whether an external navigation event is occuring +This API is not meant for public usage and could +change at any time + + ---------------------------------------------- diff --git a/packages/core/src/components/external-router-controller/external-router-controller.ts b/packages/core/src/components/external-router-controller/external-router-controller.ts deleted file mode 100644 index 8a267cd701..0000000000 --- a/packages/core/src/components/external-router-controller/external-router-controller.ts +++ /dev/null @@ -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 = null; - externalNavOccuring = false; - - @Method() - getExternalNavPromise(): void | Promise { - 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 `` 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 - 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 { - const promises: Promise[] = []; - 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 { - - - // 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); -} diff --git a/packages/core/src/components/external-router-controller/readme.md b/packages/core/src/components/external-router-controller/readme.md deleted file mode 100644 index 7395772c2a..0000000000 --- a/packages/core/src/components/external-router-controller/readme.md +++ /dev/null @@ -1,28 +0,0 @@ -# ion-external-router-controller - - - - - - -## Methods - -#### clearExternalNavPromise() - - -#### getExternalNavOccuring() - - -#### getExternalNavPromise() - - -#### reconcileNav() - - -#### updateExternalNavOccuring() - - - ----------------------------------------------- - -*Built with [StencilJS](https://stenciljs.com/)* diff --git a/packages/core/src/components/nav/nav.tsx b/packages/core/src/components/nav/nav.tsx index 1734b89185..748f76630d 100644 --- a/packages/core/src/components/nav/nav.tsx +++ b/packages/core/src/components/nav/nav.tsx @@ -51,6 +51,7 @@ import { focusOutActiveElement, isDef, isNumber, + isParentTab, normalizeUrl, } from '../../utils/helpers'; @@ -258,6 +259,11 @@ export class Nav implements PublicNav, NavOutlet { return allTransitionsCompleteImpl(this); } + @Method() + reconcileFromExternalRouter(component: any, data: any = {}, escapeHatch: EscapeHatch, isTopLevel: boolean) { + return reconcileFromExternalRouterImpl(this, component, data, escapeHatch, isTopLevel); + } + canSwipeBack(): boolean { return (this.swipeBackEnabled && // 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 `` 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 + 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 { + const promises: Promise[] = []; + 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 { + + + // 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 { required: boolean; url?: string; diff --git a/packages/core/src/components/nav/readme.md b/packages/core/src/components/nav/readme.md index 624110cd1e..fbe58fc7fe 100644 --- a/packages/core/src/components/nav/readme.md +++ b/packages/core/src/components/nav/readme.md @@ -143,6 +143,9 @@ boolean #### push() +#### reconcileFromExternalRouter() + + #### removeIndex() diff --git a/packages/core/src/components/tab/tab.tsx b/packages/core/src/components/tab/tab.tsx index 458682305c..60e6a0e61b 100644 --- a/packages/core/src/components/tab/tab.tsx +++ b/packages/core/src/components/tab/tab.tsx @@ -1,6 +1,8 @@ import { Component, Element, Event, EventEmitter, Method, Prop, State, Watch } from '@stencil/core'; -import { ensureExternalRounterController, getNavAsChildIfExists } from '../../utils/helpers'; + import { FrameworkDelegate } from '../..'; +import { getIonApp, getNavAsChildIfExists } from '../../utils/helpers'; + @Component({ tag: 'ion-tab', @@ -81,7 +83,7 @@ export class Tab { @Method() prepareActive(): Promise { if (this.loaded) { - return this.configChildgNav(); + return this.configChildNav(); } this.loaded = true; @@ -94,7 +96,7 @@ export class Tab { } else { promise = Promise.resolve(); } - return promise.then(() => this.configChildgNav()); + return promise.then(() => this.configChildNav()); } @Method() @@ -108,15 +110,15 @@ export class Tab { return null; } - private configChildgNav(): Promise { + private configChildNav(): Promise { const nav = getNavAsChildIfExists(this.el); if (nav) { // the tab's nav has been initialized externally - return ensureExternalRounterController().then((externalRouterController) => { - const externalNavPromise = externalRouterController.getExternalNavPromise(); + return getIonApp().then((ionApp) => { + const externalNavPromise = ionApp ? ionApp.getExternalNavPromise() : null; if (externalNavPromise) { - return externalNavPromise.then(() => { - externalRouterController.clearExternalNavPromise(); + return (externalNavPromise as any).then(() => { + ionApp.setExternalNavPromise(null); }); } diff --git a/packages/core/src/components/tabs/tabs.tsx b/packages/core/src/components/tabs/tabs.tsx index 6bb1196880..4bfeeaa45d 100644 --- a/packages/core/src/components/tabs/tabs.tsx +++ b/packages/core/src/components/tabs/tabs.tsx @@ -1,7 +1,7 @@ import { Component, Element, Event, EventEmitter, Listen, Method, Prop, State } from '@stencil/core'; import { Config, NavEventDetail, NavOutlet } from '../../index'; -import { asyncRaf, ensureExternalRounterController } from '../../utils/helpers'; +import { asyncRaf, getIonApp } from '../../utils/helpers'; @Component({ @@ -75,10 +75,13 @@ export class Tabs implements NavOutlet { const promises: Promise[] = []; promises.push(this.initTabs()); - promises.push(ensureExternalRounterController()); - return Promise.all(promises).then(([_, externalRouterController]) => { - return (externalRouterController as HTMLIonExternalRouterControllerElement).getExternalNavOccuring(); - }).then((externalNavOccuring) => { + promises.push(getIonApp()); + return Promise.all(promises).then(([_, ionApp]) => { + if (ionApp) { + return (ionApp as HTMLIonAppElement).getExternalNavOccuring(); + } + return false; + }).then((externalNavOccuring: boolean) => { if (!externalNavOccuring) { return this.initSelect(); } @@ -116,11 +119,8 @@ export class Tabs implements NavOutlet { tab.selected = false; } } - selectedTab.selected = true; const leavingTab = this.selectedTab; - this.selectedTab = selectedTab; - return selectedTab.prepareActive() .then(() => selectedTab.active = true) @@ -129,6 +129,8 @@ export class Tabs implements NavOutlet { if (leavingTab) { leavingTab.active = false; } + selectedTab.selected = true; + this.selectedTab = selectedTab; this.ionChange.emit(selectedTab); this.ionNavChanged.emit({isPop: false}); }); @@ -216,6 +218,7 @@ export class Tabs implements NavOutlet { this.selectedTab = selectedTab; if (selectedTab) { selectedTab.selected = true; + selectedTab.active = true; } }); } diff --git a/packages/core/src/utils/helpers.ts b/packages/core/src/utils/helpers.ts index a8c8b63fc7..7b4a729bb1 100644 --- a/packages/core/src/utils/helpers.ts +++ b/packages/core/src/utils/helpers.ts @@ -321,12 +321,14 @@ export function normalizeUrl(url: string) { return url; } -export function ensureExternalRounterController(): Promise { - const element = document.querySelector('ion-external-router-controller'); - if (element) { - return (element as any).componentOnReady(); - } - const toCreate = document.createElement('ion-external-router-controller'); - document.body.appendChild(toCreate); - return (toCreate as any).componentOnReady(); +export function isParentTab(element: HTMLElement) { + return element.parentElement.tagName.toLowerCase() === 'ion-tab'; +} + +export function getIonApp(): Promise { + const appElement = document.querySelector('ion-app'); + if (!appElement) { + return Promise.resolve(null); + } + return appElement.componentOnReady(); }