fix(angular): save internal data

fixes #14888
fixes #14885
fixes #15054
fixes #15050
This commit is contained in:
Manu Mtz.-Almeida
2018-08-16 15:47:40 +02:00
parent fef751d718
commit f84bb76481
5 changed files with 68 additions and 55 deletions

View File

@ -1,6 +1,6 @@
import { Attribute, ChangeDetectorRef, ComponentFactoryResolver, ComponentRef, Directive, ElementRef, EventEmitter, Injector, Input, OnDestroy, OnInit, Optional, Output, ViewContainerRef } from '@angular/core'; import { Attribute, ChangeDetectorRef, ComponentFactoryResolver, ComponentRef, Directive, ElementRef, EventEmitter, Injector, Input, OnDestroy, OnInit, Optional, Output, ViewContainerRef } from '@angular/core';
import { ActivatedRoute, ChildrenOutletContexts, PRIMARY_OUTLET, Router } from '@angular/router'; import { ActivatedRoute, ChildrenOutletContexts, OutletContext, PRIMARY_OUTLET, Router } from '@angular/router';
import { StackController } from './router-controller'; import { StackController, RouteView } from './router-controller';
import { NavController } from '../../providers/nav-controller'; import { NavController } from '../../providers/nav-controller';
import { bindLifecycleEvents } from '../../providers/angular-delegate'; import { bindLifecycleEvents } from '../../providers/angular-delegate';
@ -11,6 +11,7 @@ import { bindLifecycleEvents } from '../../providers/angular-delegate';
export class IonRouterOutlet implements OnDestroy, OnInit { export class IonRouterOutlet implements OnDestroy, OnInit {
private activated: ComponentRef<any> | null = null; private activated: ComponentRef<any> | null = null;
private activatedView: RouteView | null = null;
private _activatedRoute: ActivatedRoute | null = null; private _activatedRoute: ActivatedRoute | null = null;
private name: string; private name: string;
@ -42,25 +43,24 @@ export class IonRouterOutlet implements OnDestroy, OnInit {
} }
ngOnDestroy(): void { ngOnDestroy(): void {
console.log('router-outlet destroyed');
this.parentContexts.onChildOutletDestroyed(this.name); this.parentContexts.onChildOutletDestroyed(this.name);
} }
getContext(): OutletContext | null {
return this.parentContexts.getContext(this.name);
}
ngOnInit(): void { ngOnInit(): void {
if (!this.activated) { if (!this.activated) {
// If the outlet was not instantiated at the time the route got activated we need to populate // 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) // the outlet when it is initialized (ie inside a NgIf)
const context = this.parentContexts.getContext(this.name); const context = this.getContext();
if (context && context.route) { if (context && context.route) {
if (context.attachRef) {
// `attachRef` is populated when there is an existing component to mount
this.attach(context.attachRef, context.route);
} else {
// otherwise the component defined in the configuration is created
this.activateWith(context.route, context.resolver || null); this.activateWith(context.route, context.resolver || null);
} }
} }
} }
}
get isActivated(): boolean { return !!this.activated; } get isActivated(): boolean { return !!this.activated; }
@ -89,43 +89,45 @@ export class IonRouterOutlet implements OnDestroy, OnInit {
* Called when the `RouteReuseStrategy` instructs to detach the subtree * Called when the `RouteReuseStrategy` instructs to detach the subtree
*/ */
detach(): ComponentRef<any> { detach(): ComponentRef<any> {
if (!this.activated) { throw new Error('incompatible reuse strategy');
throw new Error('Outlet is not activated');
}
this.location.detach();
const cmp = this.activated;
this.activated = null;
this._activatedRoute = null;
return cmp;
} }
/** /**
* Called when the `RouteReuseStrategy` instructs to re-attach a previously detached subtree * Called when the `RouteReuseStrategy` instructs to re-attach a previously detached subtree
*/ */
attach(ref: ComponentRef<any>, activatedRoute: ActivatedRoute) { attach(_ref: ComponentRef<any>, _activatedRoute: ActivatedRoute) {
this.activated = ref; throw new Error('incompatible reuse strategy');
this._activatedRoute = activatedRoute;
this.location.insert(ref.hostView);
} }
deactivate(): void { deactivate(): void {
if (this.activated) { if (this.activated) {
if (this.activatedView) {
this.activatedView.savedData = new Map(this.getContext()!.children['contexts']);
}
const c = this.component; const c = this.component;
this.activatedView = null;
this.activated = null; this.activated = null;
this._activatedRoute = null; this._activatedRoute = null;
this.deactivateEvents.emit(c); this.deactivateEvents.emit(c);
} }
} }
async activateWith(activatedRoute: ActivatedRoute, resolver: ComponentFactoryResolver | null) { activateWith(activatedRoute: ActivatedRoute, resolver: ComponentFactoryResolver | null) {
if (this.isActivated) { if (this.isActivated) {
throw new Error('Cannot activate an already activated outlet'); throw new Error('Cannot activate an already activated outlet');
} }
this._activatedRoute = activatedRoute; this._activatedRoute = activatedRoute;
let cmpRef: any;
let enteringView = this.stackCtrl.getExistingView(activatedRoute); let enteringView = this.stackCtrl.getExistingView(activatedRoute);
if (enteringView) { if (enteringView) {
this.activated = enteringView.ref; cmpRef = this.activated = enteringView.ref;
const saved = enteringView.savedData;
if (saved) {
// self-restore
const context = this.getContext()!;
context.children['contexts'] = saved;
}
} else { } else {
const snapshot = (activatedRoute as any)._futureSnapshot; const snapshot = (activatedRoute as any)._futureSnapshot;
const component = snapshot.routeConfig!.component as any; const component = snapshot.routeConfig!.component as any;
@ -135,9 +137,9 @@ export class IonRouterOutlet implements OnDestroy, OnInit {
const childContexts = this.parentContexts.getOrCreateContext(this.name).children; const childContexts = this.parentContexts.getOrCreateContext(this.name).children;
const injector = new OutletInjector(activatedRoute, childContexts, this.location.injector); const injector = new OutletInjector(activatedRoute, childContexts, this.location.injector);
const cmp = this.activated = this.location.createComponent(factory, this.location.length, injector); cmpRef = this.activated = this.location.createComponent(factory, this.location.length, injector);
bindLifecycleEvents(cmp.instance, cmp.location.nativeElement); bindLifecycleEvents(cmpRef.instance, cmpRef.location.nativeElement);
// Calling `markForCheck` to make sure we will run the change detection when the // Calling `markForCheck` to make sure we will run the change detection when the
// `RouterOutlet` is inside a `ChangeDetectionStrategy.OnPush` component. // `RouterOutlet` is inside a `ChangeDetectionStrategy.OnPush` component.
@ -146,10 +148,12 @@ export class IonRouterOutlet implements OnDestroy, OnInit {
} }
const { direction, animated } = this.navCtrl.consumeTransition(); const { direction, animated } = this.navCtrl.consumeTransition();
await this.stackCtrl.setActive(enteringView, direction, animated); this.activatedView = enteringView;
this.activateEvents.emit(this.activated.instance); this.stackCtrl.setActive(enteringView, direction, animated).then(() => {
this.activateEvents.emit(cmpRef.instance);
emitEvent(this.elementRef.nativeElement); emitEvent(this.elementRef.nativeElement);
});
} }
canGoBack(deep = 1) { canGoBack(deep = 1) {

View File

@ -80,9 +80,11 @@ export class StackController {
.forEach(view => destroyView(view)); .forEach(view => destroyView(view));
for (let i = 0; i < views.length - 1; i++) { for (let i = 0; i < views.length - 1; i++) {
const element = views[i].element; const view = views[i];
const element = view.element;
element.setAttribute('aria-hidden', 'true'); element.setAttribute('aria-hidden', 'true');
element.classList.add('ion-page-hidden'); element.classList.add('ion-page-hidden');
// view.ref.changeDetectorRef.detach();
} }
this.viewsSnapshot = views.slice(); this.viewsSnapshot = views.slice();
@ -149,4 +151,5 @@ export interface RouteView {
element: HTMLElement; element: HTMLElement;
ref: ComponentRef<any>; ref: ComponentRef<any>;
deactivatedId: number; deactivatedId: number;
savedData?: any;
} }

View File

@ -1,18 +1,19 @@
import { ActivatedRouteSnapshot, DetachedRouteHandle, RouteReuseStrategy } from '@angular/router'; import { ActivatedRouteSnapshot, DetachedRouteHandle, RouteReuseStrategy } from '@angular/router';
import { deepEqual, objectValues } from './util';
export class IonicRouteStrategy implements RouteReuseStrategy { export class IonicRouteStrategy implements RouteReuseStrategy {
shouldDetach(_route: ActivatedRouteSnapshot): boolean { shouldDetach(_route: ActivatedRouteSnapshot): boolean {
return false; return false;
} }
// tslint:disable-next-line
store(_route: ActivatedRouteSnapshot, _detachedTree: DetachedRouteHandle): void {}
shouldAttach(_route: ActivatedRouteSnapshot): boolean { shouldAttach(_route: ActivatedRouteSnapshot): boolean {
return false; return false;
} }
store(_route: ActivatedRouteSnapshot, _detachedTree: DetachedRouteHandle): void {
return;
}
retrieve(_route: ActivatedRouteSnapshot): DetachedRouteHandle | null { retrieve(_route: ActivatedRouteSnapshot): DetachedRouteHandle | null {
return null; return null;
} }
@ -21,23 +22,29 @@ export class IonicRouteStrategy implements RouteReuseStrategy {
future: ActivatedRouteSnapshot, future: ActivatedRouteSnapshot,
curr: ActivatedRouteSnapshot curr: ActivatedRouteSnapshot
): boolean { ): boolean {
// checking router params if (future.routeConfig !== curr.routeConfig) {
const futureParams = objectValues(future.params); return false;
const currParams = objectValues(curr.params); }
if (future.component !== curr.component) {
return false;
}
if ( // checking router params
futureParams && const futureParams = future.params;
!!futureParams.length && const currentParams = curr.params;
currParams && const keysA = Object.keys(futureParams);
currParams.length > 0 const keysB = Object.keys(currentParams);
) {
// If the router params do not match, render the new component if (keysA.length !== keysB.length) {
return ( return false;
deepEqual(future.params, curr.params) && }
future.routeConfig === curr.routeConfig
); // Test for A's keys different from B.
} else { for (const key of keysA) {
return future.routeConfig === curr.routeConfig; if (currentParams[key] !== futureParams[key]) {
return false;
} }
} }
return true;
}
} }

View File

@ -30,10 +30,6 @@ export function ensureElementInBody(elementName: string) {
return element as HTMLStencilElement; return element as HTMLStencilElement;
} }
export function objectValues(obj: any): any[] {
return Object.keys(obj).map(key => obj[key]);
}
export function deepEqual(x: any, y: any) { export function deepEqual(x: any, y: any) {
if (x === y) { if (x === y) {
return true; return true;

View File

@ -62,6 +62,9 @@ export function matches(view: ViewController | undefined, id: string, params: Co
const currentParams = view.params; const currentParams = view.params;
const null1 = (currentParams == null); const null1 = (currentParams == null);
const null2 = (params == null); const null2 = (params == null);
if (currentParams === params) {
return true;
}
if (null1 !== null2) { if (null1 !== null2) {
return false; return false;
} }