feature(routing): create external router controller for reconciling state from router in re-useable fashion

* external router controller

* external router controller

* gif it's working
This commit is contained in:
Dan Bucholtz
2018-02-13 01:28:39 -06:00
committed by GitHub
parent 529148e163
commit 4271a0fc1e
11 changed files with 464 additions and 419 deletions

File diff suppressed because it is too large Load Diff

View File

@ -47,7 +47,7 @@
"glob": "7.1.2", "glob": "7.1.2",
"ionicons": "~3.0.0", "ionicons": "~3.0.0",
"rxjs": "5.5.2", "rxjs": "5.5.2",
"typescript": "~2.5.2", "typescript": "latest",
"zone.js": "0.8.18" "zone.js": "0.8.18"
}, },
"dependencies": { "dependencies": {

View File

@ -28,6 +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';
let id = 0; let id = 0;
@ -108,7 +109,7 @@ export class RouterOutlet implements OnDestroy, OnInit, RouterDelegate {
} }
activateWith(activatedRoute: ActivatedRoute, cfr: ComponentFactoryResolver): Promise<void> { activateWith(activatedRoute: ActivatedRoute, cfr: ComponentFactoryResolver): Promise<void> {
this.routeEventHandler.externalNavStart();
if (this.activationStatus !== NOT_ACTIVATED) { if (this.activationStatus !== NOT_ACTIVATED) {
return Promise.resolve(); return Promise.resolve();
} }
@ -122,10 +123,13 @@ export class RouterOutlet implements OnDestroy, OnInit, RouterDelegate {
const injector = new OutletInjector(activatedRoute, childContexts, this.location.injector); const injector = new OutletInjector(activatedRoute, childContexts, this.location.injector);
const isTopLevel = !hasChildComponent(activatedRoute); const isTopLevel = !hasChildComponent(activatedRoute);
return activateRoute(this.elementRef.nativeElement, component, cfr, injector, isTopLevel).then(() => {
this.changeDetector.markForCheck(); return this.routeEventHandler.externalNavStart().then(() => {
this.activateEvents.emit(null); return activateRoute(this.elementRef.nativeElement, component, cfr, injector, isTopLevel).then(() => {
this.activationStatus = ACTIVATED; this.changeDetector.markForCheck();
this.activateEvents.emit(null);
this.activationStatus = ACTIVATED;
});
}); });
} }
} }
@ -133,103 +137,12 @@ export class RouterOutlet implements OnDestroy, OnInit, RouterDelegate {
export function activateRoute(navElement: HTMLIonNavElement, export function activateRoute(navElement: HTMLIonNavElement,
component: Type<any>, cfr: ComponentFactoryResolver, injector: Injector, isTopLevel: boolean): Promise<void> { component: Type<any>, cfr: ComponentFactoryResolver, injector: Injector, isTopLevel: boolean): Promise<void> {
return navElement.componentOnReady().then(() => { return ensureExternalRounterController().then((externalRouterController) => {
const escapeHatch = getEscapeHatch(cfr, injector);
// check if the nav has an `<ion-tab>` as a parent return externalRouterController.reconcileNav(navElement, component, escapeHatch, isTopLevel);
if (isParentTab(navElement)) {
// check if the tab is selected
return updateTab(navElement, component, cfr, injector, isTopLevel);
} else {
return updateNav(navElement, component, cfr, injector, isTopLevel);
}
}); });
} }
function isParentTab(navElement: HTMLIonNavElement) {
return navElement.parentElement.tagName.toLowerCase() === 'ion-tab';
}
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 getSelected(tabsElement: HTMLIonTabsElement) {
tabsElement.getSelected();
}
function updateTab(navElement: HTMLIonNavElement,
component: Type<any>, cfr: ComponentFactoryResolver, injector: Injector, isTopLevel: boolean) {
const tab = navElement.parentElement as HTMLIonTabElement;
// tab.externalNav = true;
// (tab.parentElement as HTMLIonTabsElement).externalInitialize = true;
// 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;
// tabs.externalInitialize = true;
return isTabSelected(tabs, tab).then((isSelected: boolean) => {
if (!isSelected) {
const promise = updateNav(navElement, component, cfr, injector, isTopLevel);
(window as any).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, cfr, injector, isTopLevel);
})
}
function updateNav(navElement: HTMLIonNavElement,
component: Type<any>, cfr: ComponentFactoryResolver, injector: Injector, isTopLevel: boolean): Promise<NavResult> {
const escapeHatch = getEscapeHatch(cfr, injector);
// 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, {}, {}, 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, {}, { animate: isTopLevel }, escapeHatch);
}
export const NOT_ACTIVATED = 0; export const NOT_ACTIVATED = 0;
export const ACTIVATION_IN_PROGRESS = 1; export const ACTIVATION_IN_PROGRESS = 1;
export const ACTIVATED = 2; export const ACTIVATED = 2;

View File

@ -2,26 +2,29 @@ import { Injectable } from '@angular/core';
import { import {
Event, Event,
NavigationEnd, NavigationEnd,
NavigationStart,
Router Router
} from '@angular/router'; } from '@angular/router';
let initialized = false; import { ensureExternalRounterController } from '../util/util';
@Injectable() @Injectable()
export class RouteEventHandler { export class RouteEventHandler {
constructor(private router: Router) { constructor(private router: Router) {
(window as any).externalNav = false;
router.events.subscribe((event: Event) => { router.events.subscribe((event: Event) => {
if (event instanceof NavigationEnd) { if (event instanceof NavigationEnd) {
(window as any).externalNav = false; ensureExternalRounterController().then((element) => {
element.updateExternalNavOccuring(false);
});
} }
}); });
} }
externalNavStart() { externalNavStart() {
(window as any).externalNav = true; return ensureExternalRounterController().then((element) => {
element.updateExternalNavOccuring(true);
});
} }
} }

View File

@ -25,3 +25,13 @@ export function removeAllNodeChildren(element: HTMLElement) {
export function isString(something: any) { export function isString(something: any) {
return typeof something === 'string' ? true : false; return typeof something === 'string' ? true : false;
} }
export function ensureExternalRounterController(): Promise<HTMLIonExternalRouterControllerElement> {
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();
}

View File

@ -874,6 +874,36 @@ 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

@ -0,0 +1,119 @@
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, 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, escapeHatch, isTopLevel);
} else {
return updateNav(nav, component, escapeHatch, isTopLevel);
}
});
}
}
function isParentTab(navElement: HTMLIonNavElement) {
return navElement.parentElement.tagName.toLowerCase() === 'ion-tab';
}
function updateTab(externalRouterController: ExternalRouterController, navElement: HTMLIonNavElement, component: 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, 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, 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, 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, {}, {}, 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, {}, { animate: isTopLevel }, escapeHatch);
}

View File

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

View File

@ -1,5 +1,5 @@
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 { getNavAsChildIfExists } from '../../utils/helpers'; import { ensureExternalRounterController, getNavAsChildIfExists } from '../../utils/helpers';
import { FrameworkDelegate } from '../..'; import { FrameworkDelegate } from '../..';
@Component({ @Component({
@ -85,11 +85,14 @@ export class Tab {
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
if ((window as any).externalNavPromise) {
return (window as any).externalNavPromise.then(() => { return ensureExternalRounterController().then((externalRouterController) => {
(window as any).externalNavPromise = null; if (externalRouterController.getExternalNavPromise()) {
}); return (externalRouterController.getExternalNavPromise() as Promise<any>).then(() => {
} else { externalRouterController.clearExternalNavPromise();
});
}
// the tab's nav has not been initialized externally, so // the tab's nav has not been initialized externally, so
// check if we need to initiailize it // check if we need to initiailize it
return (nav as any).componentOnReady().then(() => { return (nav as any).componentOnReady().then(() => {
@ -100,7 +103,7 @@ export class Tab {
} }
return Promise.resolve(); return Promise.resolve();
}); });
} });
} }
} }

View File

@ -1,6 +1,8 @@
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 { ensureExternalRounterController } from '../../utils/helpers';
@Component({ @Component({
tag: 'ion-tabs', tag: 'ion-tabs',
@ -71,8 +73,13 @@ export class Tabs implements NavOutlet {
this.loadConfig('tabsLayout', 'icon-top'); this.loadConfig('tabsLayout', 'icon-top');
this.loadConfig('tabsHighlight', true); this.loadConfig('tabsHighlight', true);
return this.initTabs().then(() => { const promises: Promise<any>[] = [];
if (! (window as any).externalNav) { promises.push(this.initTabs());
promises.push(ensureExternalRounterController());
return Promise.all(promises).then(([_, externalRouterController]) => {
return (externalRouterController as HTMLIonExternalRouterControllerElement).getExternalNavOccuring();
}).then((externalNavOccuring) => {
if (!externalNavOccuring) {
return this.initSelect(); return this.initSelect();
} }
return null; return null;

View File

@ -316,3 +316,13 @@ export function normalizeUrl(url: string) {
} }
return url; return url;
} }
export function ensureExternalRounterController(): Promise<HTMLIonExternalRouterControllerElement> {
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();
}