refactor(nav): add initial support for url in general, add integration w/ ng-router

This commit is contained in:
Dan Bucholtz
2018-01-31 10:52:50 -06:00
committed by GitHub
parent e2b7f64296
commit ab2176b6ce
176 changed files with 17795 additions and 6783 deletions

File diff suppressed because it is too large Load Diff

View File

@ -33,14 +33,15 @@
"dist/"
],
"devDependencies": {
"@angular/common": "next",
"@angular/compiler": "next",
"@angular/compiler-cli": "next",
"@angular/core": "next",
"@angular/forms": "next",
"@angular/http": "next",
"@angular/platform-browser": "next",
"@angular/platform-browser-dynamic": "next",
"@angular/common": "latest",
"@angular/compiler": "latest",
"@angular/compiler-cli": "latest",
"@angular/core": "latest",
"@angular/forms": "latest",
"@angular/http": "latest",
"@angular/platform-browser": "latest",
"@angular/platform-browser-dynamic": "latest",
"@danbucholtz/ng-router": "6.0.0-beta.1-20a6848be",
"@ionic/core": "next",
"glob": "7.1.2",
"ionicons": "~3.0.0",

View File

@ -1,34 +0,0 @@
import {
ComponentFactoryResolver,
Directive,
ElementRef,
Injector,
Type,
} from '@angular/core';
import { FrameworkDelegate } from '@ionic/core';
import { AngularComponentMounter } from '../providers/angular-component-mounter';
import { AngularMountingData } from '../types/interfaces';
@Directive({
selector: 'ion-nav',
})
export class IonNavDelegate implements FrameworkDelegate {
constructor(private elementRef: ElementRef, private angularComponentMounter: AngularComponentMounter, private componentResolveFactory: ComponentFactoryResolver, private injector: Injector) {
this.elementRef.nativeElement.delegate = this;
}
attachViewToDom(elementOrContainerToMountTo: HTMLIonNavElement, elementOrComponentToMount: Type<any>, _propsOrDataObj?: any, classesToAdd?: string[]): Promise<AngularMountingData> {
// wrap whatever the user provides in an ion-page
return this.angularComponentMounter.attachViewToDom(elementOrContainerToMountTo, null, elementOrComponentToMount, this.componentResolveFactory, this.injector, _propsOrDataObj, classesToAdd);
}
removeViewFromDom(parentElement: HTMLElement, childElement: HTMLElement) {
return this.angularComponentMounter.removeViewFromDom(parentElement, childElement);
}
}

View File

@ -7,10 +7,6 @@ export const NavControllerToken = new InjectionToken<any>('NavControllerToken');
export const NavParamsToken = new InjectionToken<any>('NavParamsToken');
export function getProviders(element: HTMLElement, data: any) {
if (element.tagName !== 'ion-nav') {
element.closest('ion-nav');
}
const nearestNavElement = (element.tagName.toLowerCase() === 'ion-nav' ? element : element.closest('ion-nav')) as HTMLIonNavElement;
return [

View File

@ -1,14 +1,19 @@
export { IonicAngularModule } from './module';
/* Components */
export { IonNavDelegate } from './components/ion-nav';
/* Directives */
export { MenuToggle } from './directives/menu-toggle';
/* Nav */
export { IonNav } from './nav/ion-nav';
export { AsyncActivateRoutes } from './nav/router/async-activated-routes';
export { OutletInjector } from './nav/router/outlet-injector';
export { ExtendedRouter } from './nav/router/router-extension';
export { IonicRouterModule } from './nav/nav-module';
/* Providers */
export { ActionSheetController, ActionSheetProxy } from './providers/action-sheet-controller';
export { AlertController, AlertProxy } from './providers/alert-controller';
export { AngularComponentMounter } from './providers/angular-component-mounter';
export { App } from './providers/app';
export { Events } from './providers/events';
export { LoadingController, LoadingProxy } from './providers/loading-controller';
@ -17,4 +22,6 @@ export { ModalController, ModalProxy } from './providers/modal-controller';
export { NavController } from './providers/nav-controller';
export { NavParams } from './providers/nav-params';
export { PopoverController, PopoverProxy } from './providers/popover-controller';
export { ToastController, ToastProxy } from './providers/toast-controller';
export { ToastController, ToastProxy } from './providers/toast-controller';
export * from './types/interfaces';

View File

@ -13,7 +13,7 @@ import { TextValueAccessor } from './control-value-accessors/text-value-accessor
/* Components */
import { IonNavDelegate } from './components/ion-nav';
/* Directives */
import { MenuToggle } from './directives/menu-toggle';
@ -33,7 +33,6 @@ import { ToastController } from './providers/toast-controller';
@NgModule({
declarations: [
BooleanValueAccessor,
IonNavDelegate,
MenuToggle,
NumericValueAccessor,
RadioValueAccessor,
@ -42,7 +41,6 @@ import { ToastController } from './providers/toast-controller';
],
exports: [
BooleanValueAccessor,
IonNavDelegate,
MenuToggle,
NumericValueAccessor,
RadioValueAccessor,

View File

@ -0,0 +1,239 @@
import {
Attribute,
ChangeDetectorRef,
ComponentFactoryResolver,
ComponentRef,
Directive,
ElementRef,
EventEmitter,
Injector,
NgZone,
OnDestroy,
OnInit,
Output,
Type,
ViewContainerRef,
} from '@angular/core';
import {
PRIMARY_OUTLET,
ActivatedRoute,
ChildrenOutletContexts,
Router
} from '@danbucholtz/ng-router';
import { FrameworkDelegate } from '@ionic/core';
import { AngularComponentMounter, AngularEscapeHatch } from '..';
import { OutletInjector } from './router/outlet-injector';
let id = 0;
@Directive({
selector: 'ion-nav',
})
export class IonNav implements FrameworkDelegate, OnDestroy, OnInit {
public name: string;
public activationStatus = NOT_ACTIVATED;
public componentConstructor: Type<any> = null;
public componentInstance: any = null;
public activatedRoute: ActivatedRoute = null;
public activatedRouteData: any = {};
public activeComponentRef: ComponentRef<any> = null;
private id: number = id++;
private parent: HTMLElement;
@Output('activate') activateEvents = new EventEmitter<any>();
@Output('deactivate') deactivateEvents = new EventEmitter<any>();
constructor(
public location: ViewContainerRef,
public changeDetector: ChangeDetectorRef,
public elementRef: ElementRef,
protected angularComponentMounter: AngularComponentMounter,
protected parentContexts: ChildrenOutletContexts,
protected cfr: ComponentFactoryResolver,
protected injector: Injector,
@Attribute('name') name: string) {
this.parent = this.elementRef.nativeElement.parentElement;
this.elementRef.nativeElement.delegate = this;
this.name = name || PRIMARY_OUTLET;
parentContexts.onChildOutletCreated(this.name, this as any);
}
ngOnDestroy(): void {
console.debug(`Nav ${this.id} ngOnDestroy`);
this.parentContexts.onChildOutletDestroyed(this.name);
}
get isActivated(): boolean {
return this.activationStatus === ACTIVATION_IN_PROGRESS
|| this.activationStatus === ACTIVATED;
}
ngOnInit(): void {
if (!this.isActivated) {
// If the outlet was not instantiated at the time the route got activated we need to populate
// the outlet when it is initialized (ie inside a NgIf)
const context = this.parentContexts.getContext(this.name);
if (context && context.route) {
// the component defined in the configuration is created
// otherwise the component defined in the configuration is created
this.activateWith(context.route, context.resolver || null);
}
}
}
get component(): Object {
return this.componentInstance;
}
deactivate(): void {
console.debug(`outlet ${this.id} is being deactivated`);
this.activationStatus = NOT_ACTIVATED;
this.deactivateEvents.emit(this.componentConstructor);
}
activateWith(activatedRoute: ActivatedRoute, cfr: ComponentFactoryResolver): Promise<void> {
if (this.activationStatus !== NOT_ACTIVATED) {
return Promise.resolve();
}
this.activationStatus = ACTIVATION_IN_PROGRESS;
this.activatedRoute = activatedRoute;
const snapshot = (activatedRoute as any)._futureSnapshot;
const component = snapshot.routeConfig ? snapshot.routeConfig.component : null;
cfr = cfr || this.cfr;
const childContexts = this.parentContexts.getOrCreateContext(this.name).children;
const injector = new OutletInjector(activatedRoute, childContexts, this.location.injector);
return activateRoute(this.elementRef.nativeElement, component, cfr, injector).then(() => {
this.changeDetector.markForCheck();
this.activateEvents.emit(null);
this.activationStatus = ACTIVATED;
});
}
attachViewToDom(elementOrContainerToMountTo: HTMLIonNavElement,
elementOrComponentToMount: Type<any>,
data?: any,
classesToAdd?: string[],
escapeHatch: AngularEscapeHatch = {}): Promise<any> {
// wrap whatever the user provides in an ion-page
const cfr = escapeHatch.cfr || this.cfr;
const injector = escapeHatch.injector || this.injector;
return this.angularComponentMounter.attachViewToDom(elementOrContainerToMountTo,
null, elementOrComponentToMount, cfr, injector, data, classesToAdd);
}
removeViewFromDom(parentElement: HTMLElement, childElement: HTMLElement) {
return this.angularComponentMounter.removeViewFromDom(parentElement, childElement);
}
}
export function activateRoute(navElement: HTMLIonNavElement,
component: Type<any>, cfr: ComponentFactoryResolver, injector: Injector): Promise<void> {
return (navElement as any).componentOnReady().then(() => {
// check if the nav has an `<ion-tab>` as a parent
if (isParentTab(navElement)) {
// check if the tab is selected
return updateTab(navElement, component, cfr, injector);
} else {
return updateNav(navElement, component, cfr, injector);
}
});
}
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 as any).componentOnReady());
promises.push((tabElement as any).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) {
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) {
// 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 updateNav(navElement, component, cfr, injector).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);
})
}
function updateNav(navElement: HTMLIonNavElement,
component: Type<any>, cfr: ComponentFactoryResolver, injector: 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.push(component, {}, {}, {
cfr,
injector
});
}
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();
}
// 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();
}
}
// 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);
}
}
// since it's none of those things, we should probably just push that bad boy and call it a day
return navElement.push(component, {}, {}, {
cfr,
injector
});
}
export const NOT_ACTIVATED = 0;
export const ACTIVATION_IN_PROGRESS = 1;
export const ACTIVATED = 2;

View File

@ -0,0 +1,110 @@
import {
Location
} from '@angular/common';
import {
ApplicationRef,
Compiler,
Injector,
ModuleWithProviders,
NgModule,
NgModuleFactoryLoader,
Optional,
} from '@angular/core';
import {ɵgetDOM as getDOM} from '@angular/platform-browser';
import {
ROUTES,
ROUTER_CONFIGURATION,
ChildrenOutletContexts,
ExtraOptions,
Route,
Router,
RouteReuseStrategy,
UrlHandlingStrategy,
UrlSerializer
} from '@danbucholtz/ng-router';
import { IonicAngularModule } from '../module';
import { PushPopOutletContexts } from './router/push-pop-outlet-contexts';
import { ExtendedRouter } from './router/router-extension';
import { IonNav } from './ion-nav';
import { flatten } from '../util/util';
@NgModule({
declarations: [
IonNav
],
imports: [
IonicAngularModule
],
exports: [
IonNav
]
})
export class IonicRouterModule {
static forRoot(): ModuleWithProviders {
return {
ngModule: IonicRouterModule,
providers: [
{
provide: ChildrenOutletContexts,
useClass: PushPopOutletContexts
},
{
provide: Router,
useFactory: setupRouter,
deps: [
ApplicationRef, UrlSerializer, ChildrenOutletContexts, Location, Injector,
NgModuleFactoryLoader, Compiler, ROUTES, ROUTER_CONFIGURATION,
[UrlHandlingStrategy, new Optional()], [RouteReuseStrategy, new Optional()]
]
},
]
};
}
}
export function setupRouter(
ref: ApplicationRef, urlSerializer: UrlSerializer, contexts: ChildrenOutletContexts,
location: Location, injector: Injector, loader: NgModuleFactoryLoader, compiler: Compiler,
config: Route[][], opts: ExtraOptions = {}, urlHandlingStrategy?: UrlHandlingStrategy,
routeReuseStrategy?: RouteReuseStrategy) {
const router = new ExtendedRouter(
null, urlSerializer, contexts, location, injector, loader, compiler, flatten(config));
if (urlHandlingStrategy) {
router.urlHandlingStrategy = urlHandlingStrategy;
}
if (routeReuseStrategy) {
router.routeReuseStrategy = routeReuseStrategy;
}
if (opts.errorHandler) {
router.errorHandler = opts.errorHandler;
}
if (opts.enableTracing) {
const dom = getDOM();
router.events.subscribe(e => {
dom.logGroup(`Router Event: ${(<any>e.constructor).name}`);
dom.log(e.toString());
dom.log(e);
dom.logGroupEnd();
});
}
if (opts.onSameUrlNavigation) {
router.onSameUrlNavigation = opts.onSameUrlNavigation;
}
if (opts.paramsInheritanceStrategy) {
router.paramsInheritanceStrategy = opts.paramsInheritanceStrategy;
}
return router;
}

View File

@ -0,0 +1,273 @@
import {
ComponentRef
} from '@angular/core';
import {
ActivatedRoute,
ActivatedRouteSnapshot,
ActivationEnd,
ChildActivationEnd,
ChildrenOutletContexts,
Event,
RouteReuseStrategy,
RouterState,
TreeNode,
advanceActivatedRoute,
forEach,
nodeChildrenAsMap,
} from '@danbucholtz/ng-router';
export class AsyncActivateRoutes {
constructor(
protected routeReuseStrategy: RouteReuseStrategy, protected futureState: RouterState,
protected currState: RouterState, protected forwardEvent: (evt: Event) => void) {}
activate(parentContexts: ChildrenOutletContexts): void | Promise<void> {
const futureRoot = (this.futureState as any)._root;
const currRoot = this.currState ? (this.currState as any)._root : null;
const result = this.deactivateChildRoutes(futureRoot, currRoot, parentContexts);
return Promise.resolve(result)
.then(
() => {
advanceActivatedRoute(this.futureState.root);
return this.activateChildRoutes(futureRoot, currRoot, parentContexts);
}
);
}
// De-activate the child route that are not re-used for the future state
protected deactivateChildRoutes(
futureNode: TreeNode<ActivatedRoute>, currNode: TreeNode<ActivatedRoute>|null,
contexts: ChildrenOutletContexts): Promise<any> {
const children: {[outletName: string]: TreeNode<ActivatedRoute>} = nodeChildrenAsMap(currNode);
const promises = futureNode.children.map(futureChild => {
const childOutletName = futureChild.value.outlet;
const promise = this.deactivateRoutes(futureChild, children[childOutletName], contexts);
promise
.then(
() => {
delete children[childOutletName];
}
);
return promise;
});
return Promise.all(promises)
.then(
() => {
const promises: Promise<void>[] = [];
// De-activate the routes that will not be re-used
forEach(children, (v: TreeNode<ActivatedRoute>, childName: string) => {
promises.push(this.deactivateRouteAndItsChildren(v, contexts));
});
return Promise.all(promises);
}
);
}
protected deactivateRoutes(
futureNode: TreeNode<ActivatedRoute>, currNode: TreeNode<ActivatedRoute>,
parentContext: ChildrenOutletContexts): Promise<void> {
const future = futureNode.value;
const curr = currNode ? currNode.value : null;
if (future === curr) {
// Reusing the node, check to see if the children need to be de-activated
if (future.component) {
// If we have a normal route, we need to go through an outlet.
const context = parentContext.getContext(future.outlet);
if (context) {
return this.deactivateChildRoutes(futureNode, currNode, context.children);
}
return Promise.resolve();
} else {
// if we have a componentless route, we recurse but keep the same outlet map.
return this.deactivateChildRoutes(futureNode, currNode, parentContext);
}
} else {
if (curr) {
// Deactivate the current route which will not be re-used
return this.deactivateRouteAndItsChildren(currNode, parentContext);
}
return Promise.resolve();
}
}
protected deactivateRouteAndItsChildren(
route: TreeNode<ActivatedRoute>, parentContexts: ChildrenOutletContexts): Promise<void> {
if (this.routeReuseStrategy.shouldDetach(route.value.snapshot)) {
return this.detachAndStoreRouteSubtree(route, parentContexts);
} else {
return this.deactivateRouteAndOutlet(route, parentContexts);
}
}
protected detachAndStoreRouteSubtree(
route: TreeNode<ActivatedRoute>, parentContexts: ChildrenOutletContexts): Promise<void> {
const context = parentContexts.getContext(route.value.outlet);
if (context && context.outlet) {
const componentRefOrPromise = context.outlet.detach();
return Promise.resolve(componentRefOrPromise)
.then(
(componentRef: ComponentRef<any>) => {
const contexts = context.children.onOutletDeactivated();
this.routeReuseStrategy.store(route.value.snapshot, {componentRef, route, contexts});
}
);
}
return Promise.resolve();
}
protected deactivateRouteAndOutlet(
route: TreeNode<ActivatedRoute>, parentContexts: ChildrenOutletContexts): Promise<void> {
const context = parentContexts.getContext(route.value.outlet);
if (context) {
const children: {[outletName: string]: any} = nodeChildrenAsMap(route);
const contexts = route.value.component ? context.children : parentContexts;
const promises: Promise<void>[] = [];
forEach(children, (v: any, k: string) => {
promises.push(this.deactivateRouteAndItsChildren(v, contexts));
});
return Promise.all(promises)
.then(
() => {
if (context.outlet) {
// Destroy the component
const result = context.outlet.deactivate();
return Promise.resolve(result);
}
return Promise.resolve();
}
)
.then(
() => {
context.children.onOutletDeactivated();
}
);
}
return Promise.resolve();
}
protected activateChildRoutes(
futureNode: TreeNode<ActivatedRoute>, currNode: TreeNode<ActivatedRoute>|null,
contexts: ChildrenOutletContexts): Promise<void> {
const children: {[outlet: string]: any} = nodeChildrenAsMap(currNode);
const promises = futureNode.children.map(c => {
const promise = this.activateRoutes(c, children[c.value.outlet], contexts);
promise
.then(
() => {
this.forwardEvent(new ActivationEnd(c.value.snapshot));
}
);
return promise;
});
return Promise.all(promises)
.then(
() => {
if (futureNode.children.length) {
this.forwardEvent(new ChildActivationEnd(futureNode.value.snapshot));
}
}
);
}
protected activateRoutes(
futureNode: TreeNode<ActivatedRoute>, currNode: TreeNode<ActivatedRoute>,
parentContexts: ChildrenOutletContexts): Promise<void> {
const future = futureNode.value;
const curr = currNode ? currNode.value : null;
advanceActivatedRoute(future);
// reusing the node
if (future === curr) {
if (future.component) {
// If we have a normal route, we need to go through an outlet.
const context = parentContexts.getOrCreateContext(future.outlet);
return this.activateChildRoutes(futureNode, currNode, context.children);
} else {
// if we have a componentless route, we recurse but keep the same outlet map.
return this.activateChildRoutes(futureNode, currNode, parentContexts);
}
} else {
if (future.component) {
// if we have a normal route, we need to place the component into the outlet and recurse.
const context = parentContexts.getOrCreateContext(future.outlet);
if (this.routeReuseStrategy.shouldAttach(future.snapshot)) {
const stored =
(<any>this.routeReuseStrategy.retrieve(future.snapshot));
this.routeReuseStrategy.store(future.snapshot, null);
context.children.onOutletReAttached(stored.contexts);
context.attachRef = stored.componentRef;
context.route = stored.route.value;
if (context.outlet) {
// Attach right away when the outlet has already been instantiated
// Otherwise attach from `RouterOutlet.ngOnInit` when it is instantiated
const result = context.outlet.attach(stored.componentRef, stored.route.value);
return Promise.resolve(result)
.then(
() => {
return this.advanceActivatedRouteNodeAndItsChildren(stored.route);
}
);
}
return Promise.resolve(this.advanceActivatedRouteNodeAndItsChildren(stored.route));
} else {
const config = this.parentLoadedConfig(future.snapshot);
const cmpFactoryResolver = config ? config.module.componentFactoryResolver : null;
context.route = future;
context.resolver = cmpFactoryResolver;
if (context.outlet) {
// Activate the outlet when it has already been instantiated
// Otherwise it will get activated from its `ngOnInit` when instantiated
const result = context.outlet.activateWith(future, cmpFactoryResolver);
return Promise.resolve(result)
.then(
() => {
return this.activateChildRoutes(futureNode, null, context.children);
}
);
}
return this.activateChildRoutes(futureNode, null, context.children);
}
} else {
// if we have a componentless route, we recurse but keep the same outlet map.
return this.activateChildRoutes(futureNode, null, parentContexts);
}
}
}
advanceActivatedRouteNodeAndItsChildren(node: TreeNode<ActivatedRoute>): void {
advanceActivatedRoute(node.value);
node.children.forEach(this.advanceActivatedRouteNodeAndItsChildren);
}
parentLoadedConfig(snapshot: ActivatedRouteSnapshot): any|null {
for (let s = snapshot.parent; s; s = s.parent) {
const route = s.routeConfig as any;
if (route && route._loadedConfig) return route._loadedConfig;
if (route && route.component) return null;
}
return null;
}
}

View File

@ -0,0 +1,23 @@
import { Injector } from '@angular/core';
import {
ActivatedRoute,
ChildrenOutletContexts
} from '@danbucholtz/ng-router';
export class OutletInjector implements Injector {
constructor(private route: ActivatedRoute, private childContexts: ChildrenOutletContexts, private parent: Injector) {
}
get(token: any, notFoundValue?: any): any {
if (token === ActivatedRoute) {
return this.route;
}
if (token === ChildrenOutletContexts) {
return this.childContexts;
}
return this.parent.get(token, notFoundValue);
}
}

View File

@ -0,0 +1,41 @@
import {
ComponentFactoryResolver,
ComponentRef
} from '@angular/core';
import {
ActivatedRoute,
ChildrenOutletContexts,
OutletContext,
RouterOutlet
} from '@danbucholtz/ng-router';
export class PushPopOutletContexts extends ChildrenOutletContexts {
// this method is a public api, but the members in the ChildrenOutletContexts are private
// so we're gonna cheat, 'cause we play to win
onOutletDeactivated(): Map<string, OutletContext> {
return (this as any).contexts;
}
getOrCreateContext(childName: string): OutletContext {
let context = this.getContext(childName) as any;
if (!context) {
context = {
children: new PushPopOutletContexts()
};
(this as any).contexts.set(childName, context);
}
return context;
}
}
export interface PushPopOutletContext {
outlet?: RouterOutlet;
route?: ActivatedRoute;
resolver?: ComponentFactoryResolver;
children?: PushPopOutletContexts;
attachRef: ComponentRef<any>;
}

View File

@ -0,0 +1,226 @@
import {
Event,
GuardsCheckEnd,
GuardsCheckStart,
NavigationCancel,
NavigationEnd,
NavigationError,
PreActivation,
ResolveEnd,
ResolveStart,
Router,
RouterState,
RouterStateSnapshot,
RoutesRecognized,
UrlTree,
applyRedirects,
createRouterState,
isNavigationCancelingError,
recognize
} from '@danbucholtz/ng-router';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
import { Subscription } from 'rxjs/Subscription';
import { of } from 'rxjs/observable/of';
import { concatMap } from 'rxjs/operator/concatMap';
import { map } from 'rxjs/operator/map';
import { mergeMap } from 'rxjs/operator/mergeMap';
import { AsyncActivateRoutes } from './async-activated-routes';
export class ExtendedRouter extends Router {
protected runNavigate(
url: UrlTree, rawUrl: UrlTree, skipLocationChange: boolean, replaceUrl: boolean, id: number,
precreatedState: RouterStateSnapshot|null): Promise<boolean> {
if (id !== this.navigationId) {
(this.events as Subject<Event>)
.next(new NavigationCancel(
id, this.serializeUrl(url),
`Navigation ID ${id} is not equal to the current navigation id ${this.navigationId}`));
return Promise.resolve(false);
}
return new Promise((resolvePromise, rejectPromise) => {
// create an observable of the url and route state snapshot
// this operation do not result in any side effects
let urlAndSnapshot$: Observable<{appliedUrl: UrlTree, snapshot: RouterStateSnapshot}>;
if (!precreatedState) {
const moduleInjector = this.ngModule.injector;
const redirectsApplied$ =
applyRedirects(moduleInjector, this.configLoader, this.urlSerializer, url, this.config);
urlAndSnapshot$ = mergeMap.call(redirectsApplied$, (appliedUrl: UrlTree) => {
return map.call(
recognize(
this.rootComponentType, this.config, appliedUrl, this.serializeUrl(appliedUrl),
this.paramsInheritanceStrategy),
(snapshot: any) => {
(this.events as Subject<Event>)
.next(new RoutesRecognized(
id, this.serializeUrl(url), this.serializeUrl(appliedUrl), snapshot));
return {appliedUrl, snapshot};
});
});
} else {
urlAndSnapshot$ = of ({appliedUrl: url, snapshot: precreatedState});
}
const beforePreactivationDone$ = mergeMap.call(
urlAndSnapshot$, (p: {appliedUrl: string, snapshot: RouterStateSnapshot}) => {
return map.call(this.hooks.beforePreactivation(p.snapshot), () => p);
});
// run preactivation: guards and data resolvers
let preActivation: PreActivation;
const preactivationSetup$ = map.call(
beforePreactivationDone$,
({appliedUrl, snapshot}: {appliedUrl: string, snapshot: RouterStateSnapshot}) => {
const moduleInjector = this.ngModule.injector;
preActivation = new PreActivation(
snapshot, this.routerState.snapshot, moduleInjector,
(evt: Event) => this.triggerEvent(evt));
preActivation.initialize(this.rootContexts);
return {appliedUrl, snapshot};
});
const preactivationCheckGuards$ = mergeMap.call(
preactivationSetup$,
({appliedUrl, snapshot}: {appliedUrl: string, snapshot: RouterStateSnapshot}) => {
if (this.navigationId !== id) return of (false);
this.triggerEvent(
new GuardsCheckStart(id, this.serializeUrl(url), appliedUrl, snapshot));
return map.call(preActivation.checkGuards(), (shouldActivate: boolean) => {
this.triggerEvent(new GuardsCheckEnd(
id, this.serializeUrl(url), appliedUrl, snapshot, shouldActivate));
return {appliedUrl: appliedUrl, snapshot: snapshot, shouldActivate: shouldActivate};
});
});
const preactivationResolveData$ = mergeMap.call(
preactivationCheckGuards$,
(p: {appliedUrl: string, snapshot: RouterStateSnapshot, shouldActivate: boolean}) => {
if (this.navigationId !== id) return of (false);
if (p.shouldActivate && preActivation.isActivating()) {
this.triggerEvent(
new ResolveStart(id, this.serializeUrl(url), p.appliedUrl, p.snapshot));
return map.call(preActivation.resolveData(this.paramsInheritanceStrategy), () => {
this.triggerEvent(
new ResolveEnd(id, this.serializeUrl(url), p.appliedUrl, p.snapshot));
return p;
});
} else {
return of (p);
}
});
const preactivationDone$ = mergeMap.call(preactivationResolveData$, (p: any) => {
return map.call(this.hooks.afterPreactivation(p.snapshot), () => p);
});
// create router state
// this operation has side effects => route state is being affected
const routerState$ =
map.call(preactivationDone$, ({appliedUrl, snapshot, shouldActivate}: any) => {
if (shouldActivate) {
const state = createRouterState(this.routeReuseStrategy, snapshot, this.routerState);
return {appliedUrl, state, shouldActivate};
} else {
return {appliedUrl, state: null, shouldActivate};
}
});
// applied the new router state
// this operation has side effects
let navigationIsSuccessful = false;
const storedState = this.routerState;
const storedUrl = this.currentUrlTree;
const activatedRoutes: AsyncActivateRoutes[] = [];
routerState$
.forEach(({appliedUrl, state, shouldActivate}: any) => {
if (!shouldActivate || id !== this.navigationId) {
navigationIsSuccessful = false;
return;
}
this.currentUrlTree = appliedUrl;
this.rawUrlTree = this.urlHandlingStrategy.merge(this.currentUrlTree, rawUrl);
(this as{routerState: RouterState}).routerState = state;
if (!skipLocationChange) {
const path = this.urlSerializer.serialize(this.rawUrlTree);
if (this.location.isCurrentPathEqualTo(path) || replaceUrl) {
// this.location.replaceState(path, '', {navigationId: id});
this.location.replaceState(path, '');
} else {
// this.location.go(path, '', {navigationId: id});
this.location.go(path, '');
}
}
activatedRoutes.push(new AsyncActivateRoutes(this.routeReuseStrategy, state, storedState, (evt: Event) => this.triggerEvent(evt)))
})
.then(() => {
const promises = activatedRoutes.map(activatedRoute => activatedRoute.activate(this.rootContexts));
return Promise.all(promises)
.then(
() => {
navigationIsSuccessful = true;
}
);
})
.then(
() => {
if (navigationIsSuccessful) {
this.navigated = true;
this.lastSuccessfulId = id;
(this.events as Subject<Event>)
.next(new NavigationEnd(
id, this.serializeUrl(url), this.serializeUrl(this.currentUrlTree)));
resolvePromise(true);
} else {
this.resetUrlToCurrentUrlTree();
(this.events as Subject<Event>)
.next(new NavigationCancel(id, this.serializeUrl(url), ''));
resolvePromise(false);
}
},
(e: any) => {
if (isNavigationCancelingError(e)) {
this.navigated = true;
this.resetStateAndUrl(storedState, storedUrl, rawUrl);
(this.events as Subject<Event>)
.next(new NavigationCancel(id, this.serializeUrl(url), e.message));
resolvePromise(false);
} else {
this.resetStateAndUrl(storedState, storedUrl, rawUrl);
(this.events as Subject<Event>)
.next(new NavigationError(id, this.serializeUrl(url), e));
try {
resolvePromise(this.errorHandler(e));
} catch (ee) {
rejectPromise(ee);
}
}
});
});
}
triggerEvent(e: Event): void { ((this.events as any)as Subject<Event>).next(e); }
}

View File

@ -26,7 +26,7 @@ export class AngularComponentMounter {
const crf = componentResolveFactory ? componentResolveFactory : this.defaultCfr;
const mountingData = attachViewToDom(crf, parentElement, hostElement, componentToMount, injector, this.appRef, data, classesToAdd);
const mountingData = this.attachViewToDomImpl(crf, parentElement, hostElement, componentToMount, injector, this.appRef, data, classesToAdd);
resolve(mountingData);
});
});
@ -41,6 +41,40 @@ export class AngularComponentMounter {
});
}
attachViewToDomImpl(crf: ComponentFactoryResolver, parentElement: HTMLElement, hostElement: HTMLElement, componentToMount: Type<any>, injector: Injector, appRef: ApplicationRef, data: any, classesToAdd: string[]): AngularMountingData {
const componentProviders = ReflectiveInjector.resolve(getProviders(parentElement, data));
const componentFactory = crf.resolveComponentFactory(componentToMount);
if (!hostElement) {
hostElement = document.createElement(componentFactory.selector);
}
const childInjector = ReflectiveInjector.fromResolvedProviders(componentProviders, injector);
const componentRef = componentFactory.create(childInjector, [], hostElement);
for (const clazz of classesToAdd) {
hostElement.classList.add(clazz);
}
parentElement.appendChild(hostElement);
appRef.attachView(componentRef.hostView);
const mountingData = {
component: componentToMount,
componentFactory,
childInjector,
componentRef,
instance: componentRef.instance,
angularHostElement: componentRef.location.nativeElement,
element: hostElement,
data
};
elementToComponentRefMap.set(hostElement, mountingData);
return mountingData;
}
}
export function removeViewFromDom(_parentElement: HTMLElement, childElement: HTMLElement) {
@ -49,35 +83,3 @@ export function removeViewFromDom(_parentElement: HTMLElement, childElement: HTM
mountingData.componentRef.destroy();
}
}
export function attachViewToDom(crf: ComponentFactoryResolver, parentElement: HTMLElement, hostElement: HTMLElement, componentToMount: Type<any>, injector: Injector, appRef: ApplicationRef, data: any, classesToAdd: string[]): AngularMountingData {
const componentProviders = ReflectiveInjector.resolve(getProviders(parentElement, data));
const componentFactory = crf.resolveComponentFactory(componentToMount);
if (!hostElement) {
hostElement = document.createElement(componentFactory.selector);
}
const childInjector = ReflectiveInjector.fromResolvedProviders(componentProviders, injector);
const componentRef = componentFactory.create(childInjector, [], hostElement);
for (const clazz of classesToAdd) {
hostElement.classList.add(clazz);
}
parentElement.appendChild(hostElement);
appRef.attachView(componentRef.hostView);
const mountingData = {
componentFactory,
childInjector,
componentRef,
instance: componentRef.instance,
angularHostElement: componentRef.location.nativeElement,
element: hostElement,
};
elementToComponentRefMap.set(hostElement, mountingData);
return mountingData;
}

View File

@ -36,9 +36,9 @@ export class ModalController implements FrameworkDelegate {
});
}
attachViewToDom(elementOrContainerToMountTo: HTMLElement, elementOrComponentToMount: Type<any>, _propsOrDataObj?: any, classesToAdd?: string[]): Promise<AngularMountingData> {
attachViewToDom(elementOrContainerToMountTo: HTMLElement, elementOrComponentToMount: Type<any>, data?: any, classesToAdd?: string[]): Promise<AngularMountingData> {
return this.angularComponentMounter.attachViewToDom(elementOrContainerToMountTo, null, elementOrComponentToMount, this.componentResolveFactory, this.injector, _propsOrDataObj, classesToAdd);
return this.angularComponentMounter.attachViewToDom(elementOrContainerToMountTo, null, elementOrComponentToMount, this.componentResolveFactory, this.injector, data, classesToAdd);
}
removeViewFromDom(parentElement: HTMLElement, childElement: HTMLElement) {

View File

@ -29,9 +29,16 @@ export class PopoverController implements FrameworkDelegate {
return getPopoverProxy(opts);
}
attachViewToDom(elementOrContainerToMountTo: HTMLElement, elementOrComponentToMount: Type<any>, _propsOrDataObj?: any, classesToAdd?: string[]): Promise<AngularMountingData> {
dismiss(data?: any, role?: string, id?: number) {
const popoverController = document.querySelector('ion-popover-controller');
return (popoverController as any).componentOnReady().then(() => {
return popoverController.dismiss(data, role, id);
});
}
return this.angularComponentMounter.attachViewToDom(elementOrContainerToMountTo, null, elementOrComponentToMount, this.componentResolveFactory, this.injector, _propsOrDataObj, classesToAdd);
attachViewToDom(elementOrContainerToMountTo: HTMLElement, elementOrComponentToMount: Type<any>, data?: any, classesToAdd?: string[]): Promise<AngularMountingData> {
return this.angularComponentMounter.attachViewToDom(elementOrContainerToMountTo, null, elementOrComponentToMount, this.componentResolveFactory, this.injector, data, classesToAdd);
}
removeViewFromDom(parentElement: HTMLElement, childElement: HTMLElement) {

View File

@ -1,8 +1,10 @@
import {
ComponentFactory,
ComponentFactoryResolver,
ComponentRef,
Injector
} from '@angular/core';
import { ActivatedRoute } from '@danbucholtz/ng-router';
import { FrameworkMountingData } from '@ionic/core';
@ -13,3 +15,9 @@ export interface AngularMountingData extends FrameworkMountingData {
instance?: any;
angularHostElement?: HTMLElement;
}
export interface AngularEscapeHatch {
activatedRoute?: ActivatedRoute;
cfr?: ComponentFactoryResolver;
injector?: Injector;
}

View File

@ -15,3 +15,20 @@ export function ensureElementInBody(elementName: string) {
}
return element;
}
export function removeAllNodeChildren(element: HTMLElement) {
while (element.firstChild) {
element.removeChild(element.firstChild);
}
}
export function isString(something: any) {
return typeof something === 'string' ? true : false;
}
/**
* Flattens single-level nested arrays.
*/
export function flatten<T>(arr: T[][]): T[] {
return Array.prototype.concat.apply([], arr);
}

View File

@ -10,8 +10,8 @@
"moduleResolution": "node",
"noImplicitAny": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"outDir": "dist",
"removeComments": false,
"sourceMap": true,