mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-18 11:17:19 +08:00
fix(angular): stack based navigation
This commit is contained in:
@ -15,6 +15,7 @@ export { ToastController } from './providers/toast-controller';
|
||||
|
||||
// navigation
|
||||
export { GoBack } from './navigation/go-back';
|
||||
export { IonBackButton } from './navigation/ion-back-button';
|
||||
export { NavController } from './navigation/ion-nav-controller';
|
||||
export { Nav } from './navigation/ion-nav';
|
||||
export { IonRouterOutlet } from './navigation/ion-router-outlet';
|
||||
|
@ -10,6 +10,7 @@ import { TextValueAccessor } from './control-value-accessors/text-value-accessor
|
||||
|
||||
// navigation
|
||||
import { GoBack } from './navigation/go-back';
|
||||
import { IonBackButton } from './navigation/ion-back-button';
|
||||
import { NavController } from './navigation/ion-nav-controller';
|
||||
import { Nav } from './navigation/ion-nav';
|
||||
import { Tab } from './navigation/ion-tab';
|
||||
@ -222,6 +223,7 @@ import {
|
||||
Tab,
|
||||
Tabs,
|
||||
GoBack,
|
||||
IonBackButton,
|
||||
|
||||
// router
|
||||
IonRouterOutlet,
|
||||
@ -331,6 +333,7 @@ import {
|
||||
Nav,
|
||||
IonRouterOutlet,
|
||||
GoBack,
|
||||
IonBackButton,
|
||||
Tab,
|
||||
Tabs,
|
||||
NumericValueAccessor,
|
||||
|
27
angular/src/navigation/ion-back-button.ts
Normal file
27
angular/src/navigation/ion-back-button.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { Directive } from '@angular/core';
|
||||
|
||||
@Directive({
|
||||
selector: 'ion-back-button'
|
||||
})
|
||||
export class IonBackButton {
|
||||
|
||||
// constructor(
|
||||
// private navCtrl: NavController,
|
||||
// private router
|
||||
// @Optional() private routerOutlet: IonRouterOutlet,
|
||||
// ) {
|
||||
// routerOutlet.
|
||||
// }
|
||||
|
||||
|
||||
|
||||
// @HostListener('click')
|
||||
// onClick() {
|
||||
// if(routerOutlet.canGoBack())
|
||||
// this.navCtrl.setGoback();
|
||||
// if (!this.routerLink) {
|
||||
// window.history.back();
|
||||
// }
|
||||
// }
|
||||
|
||||
}
|
@ -3,26 +3,26 @@ import { Injectable } from '@angular/core';
|
||||
@Injectable()
|
||||
export class NavController {
|
||||
|
||||
private direction = 0;
|
||||
private stack: string[] = [];
|
||||
private direction = 1;
|
||||
// private stack: string[] = [];
|
||||
|
||||
setGoback() {
|
||||
this.direction = -1;
|
||||
}
|
||||
|
||||
consumeDirection() {
|
||||
if (this.direction === 0) {
|
||||
const index = this.stack.indexOf(document.location.href);
|
||||
if (index === -1) {
|
||||
this.stack.push(document.location.href);
|
||||
this.direction = 1;
|
||||
} else {
|
||||
this.stack = this.stack.slice(0, index + 1);
|
||||
this.direction = -1;
|
||||
}
|
||||
}
|
||||
// if (this.direction === 0) {
|
||||
// const index = this.stack.indexOf(document.location.href);
|
||||
// if (index === -1) {
|
||||
// this.stack.push(document.location.href);
|
||||
// this.direction = 1;
|
||||
// } else {
|
||||
// this.stack = this.stack.slice(0, index + 1);
|
||||
// this.direction = -1;
|
||||
// }
|
||||
// }
|
||||
const direction = this.direction;
|
||||
this.direction = 0;
|
||||
this.direction = 1;
|
||||
return direction;
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { Attribute, ChangeDetectorRef, ComponentFactoryResolver, ComponentRef, Directive, ElementRef, EventEmitter, Injector, OnDestroy, OnInit, Output, ViewContainerRef } from '@angular/core';
|
||||
import { ActivatedRoute, ChildrenOutletContexts, PRIMARY_OUTLET } from '@angular/router';
|
||||
import { NavController } from './ion-nav-controller';
|
||||
import { NavDirection } from '@ionic/core/dist/types/components/nav/nav-util';
|
||||
import { Attribute, ChangeDetectorRef, ComponentFactoryResolver, ComponentRef, Directive, ElementRef, EventEmitter, Injector, OnDestroy, OnInit, Optional, Output, ViewContainerRef } from '@angular/core';
|
||||
import { ActivatedRoute, ChildrenOutletContexts, PRIMARY_OUTLET, Router } from '@angular/router';
|
||||
import { StackController } from './router-controller';
|
||||
|
||||
@Directive({
|
||||
selector: 'ion-router-outlet',
|
||||
@ -10,10 +9,10 @@ import { NavDirection } from '@ionic/core/dist/types/components/nav/nav-util';
|
||||
export class IonRouterOutlet implements OnDestroy, OnInit {
|
||||
|
||||
private activated: ComponentRef<any>|null = null;
|
||||
private deactivated: ComponentRef<any>|null = null;
|
||||
|
||||
private _activatedRoute: ActivatedRoute|null = null;
|
||||
private name: string;
|
||||
private stackCtrl: StackController;
|
||||
|
||||
@Output('activate') activateEvents = new EventEmitter<any>();
|
||||
@Output('deactivate') deactivateEvents = new EventEmitter<any>();
|
||||
@ -22,13 +21,16 @@ export class IonRouterOutlet implements OnDestroy, OnInit {
|
||||
private parentContexts: ChildrenOutletContexts,
|
||||
private location: ViewContainerRef,
|
||||
private resolver: ComponentFactoryResolver,
|
||||
private elementRef: ElementRef,
|
||||
elementRef: ElementRef,
|
||||
@Attribute('name') name: string,
|
||||
@Optional() @Attribute('stack') stack: any,
|
||||
private changeDetector: ChangeDetectorRef,
|
||||
private navCtrl: NavController
|
||||
// private navCtrl: NavController,
|
||||
router: Router
|
||||
) {
|
||||
this.name = name || PRIMARY_OUTLET;
|
||||
parentContexts.onChildOutletCreated(this.name, this as any);
|
||||
this.stackCtrl = new StackController(stack != null, elementRef.nativeElement, router);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
@ -55,12 +57,16 @@ export class IonRouterOutlet implements OnDestroy, OnInit {
|
||||
get isActivated(): boolean { return !!this.activated; }
|
||||
|
||||
get component(): Object {
|
||||
if (!this.activated) throw new Error('Outlet is not activated');
|
||||
if (!this.activated) {
|
||||
throw new Error('Outlet is not activated');
|
||||
}
|
||||
return this.activated.instance;
|
||||
}
|
||||
|
||||
get activatedRoute(): ActivatedRoute {
|
||||
if (!this.activated) throw new Error('Outlet is not activated');
|
||||
if (!this.activated) {
|
||||
throw new Error('Outlet is not activated');
|
||||
}
|
||||
return this._activatedRoute as ActivatedRoute;
|
||||
}
|
||||
|
||||
@ -75,7 +81,9 @@ export class IonRouterOutlet implements OnDestroy, OnInit {
|
||||
* Called when the `RouteReuseStrategy` instructs to detach the subtree
|
||||
*/
|
||||
detach(): ComponentRef<any> {
|
||||
if (!this.activated) throw new Error('Outlet is not activated');
|
||||
if (!this.activated) {
|
||||
throw new Error('Outlet is not activated');
|
||||
}
|
||||
this.location.detach();
|
||||
const cmp = this.activated;
|
||||
this.activated = null;
|
||||
@ -95,7 +103,6 @@ export class IonRouterOutlet implements OnDestroy, OnInit {
|
||||
deactivate(): void {
|
||||
if (this.activated) {
|
||||
const c = this.component;
|
||||
this.deactivated = this.activated;
|
||||
this.activated = null;
|
||||
this._activatedRoute = null;
|
||||
this.deactivateEvents.emit(c);
|
||||
@ -107,46 +114,38 @@ export class IonRouterOutlet implements OnDestroy, OnInit {
|
||||
throw new Error('Cannot activate an already activated outlet');
|
||||
}
|
||||
this._activatedRoute = activatedRoute;
|
||||
const snapshot = (activatedRoute as any)._futureSnapshot;
|
||||
|
||||
const component = <any>snapshot.routeConfig !.component;
|
||||
resolver = resolver || this.resolver;
|
||||
let enteringView = this.stackCtrl.getExistingView(activatedRoute);
|
||||
if (enteringView) {
|
||||
this.activated = enteringView.ref;
|
||||
} else {
|
||||
const snapshot = (activatedRoute as any)._futureSnapshot;
|
||||
|
||||
const factory = resolver.resolveComponentFactory(component);
|
||||
const childContexts = this.parentContexts.getOrCreateContext(this.name).children;
|
||||
const component = <any>snapshot.routeConfig !.component;
|
||||
resolver = resolver || this.resolver;
|
||||
|
||||
const injector = new OutletInjector(activatedRoute, childContexts, this.location.injector);
|
||||
this.activated = this.location.createComponent(factory, this.location.length, injector);
|
||||
const factory = resolver.resolveComponentFactory(component);
|
||||
const childContexts = this.parentContexts.getOrCreateContext(this.name).children;
|
||||
|
||||
// Calling `markForCheck` to make sure we will run the change detection when the
|
||||
// `RouterOutlet` is inside a `ChangeDetectionStrategy.OnPush` component.
|
||||
this.changeDetector.markForCheck();
|
||||
await this.transition();
|
||||
const injector = new OutletInjector(activatedRoute, childContexts, this.location.injector);
|
||||
this.activated = this.location.createComponent(factory, this.location.length, injector);
|
||||
|
||||
// Calling `markForCheck` to make sure we will run the change detection when the
|
||||
// `RouterOutlet` is inside a `ChangeDetectionStrategy.OnPush` component.
|
||||
this.changeDetector.markForCheck();
|
||||
enteringView = this.stackCtrl.createView(this.activated, activatedRoute);
|
||||
}
|
||||
|
||||
await this.stackCtrl.setActive(enteringView, undefined);
|
||||
this.activateEvents.emit(this.activated.instance);
|
||||
}
|
||||
|
||||
async transition() {
|
||||
const enteringRef = this.activated;
|
||||
const enteringEl = (enteringRef && enteringRef.location && enteringRef.location.nativeElement) as HTMLElement;
|
||||
if (enteringEl) {
|
||||
enteringEl.classList.add('ion-page', 'hide-page');
|
||||
canGoBack(deep = 1) {
|
||||
return this.stackCtrl.canGoBack(deep);
|
||||
}
|
||||
|
||||
const navEl = this.elementRef.nativeElement as HTMLIonRouterOutletElement;
|
||||
navEl.appendChild(enteringEl);
|
||||
|
||||
const direction = this.navCtrl.consumeDirection();
|
||||
|
||||
await navEl.componentOnReady();
|
||||
await navEl.commit(enteringEl, {
|
||||
duration: direction === 0 ? 0 : undefined,
|
||||
direction: direction === -1 ? NavDirection.back : NavDirection.forward
|
||||
});
|
||||
|
||||
if (this.deactivated) {
|
||||
this.deactivated.destroy();
|
||||
this.deactivated = null;
|
||||
}
|
||||
}
|
||||
pop(deep = 1) {
|
||||
return this.stackCtrl.pop(deep);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,38 +1,112 @@
|
||||
import { ComponentRef, ViewContainerRef } from '@angular/core';
|
||||
import { ActivatedRoute, UrlSegment } from '@angular/router';
|
||||
import { ComponentRef } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { NavDirection } from '@ionic/core/dist/types/components/nav/nav-util';
|
||||
|
||||
|
||||
export function attachView(views: RouteView[], location: ViewContainerRef, ref: ComponentRef<any>, activatedRoute: ActivatedRoute) {
|
||||
initRouteViewElm(views, ref, activatedRoute);
|
||||
location.insert(ref.hostView);
|
||||
export class StackController {
|
||||
|
||||
viewsSnapshot: RouteView[] = [];
|
||||
views: RouteView[] = [];
|
||||
|
||||
constructor(
|
||||
private stack: boolean,
|
||||
private containerEl: HTMLIonRouterOutletElement,
|
||||
private router: Router
|
||||
) {}
|
||||
|
||||
createView(enteringRef: ComponentRef<any>, route: ActivatedRoute): RouteView {
|
||||
return {
|
||||
ref: enteringRef,
|
||||
element: (enteringRef && enteringRef.location && enteringRef.location.nativeElement) as HTMLElement,
|
||||
url: this.getUrl(route),
|
||||
deactivatedId: -1
|
||||
};
|
||||
}
|
||||
|
||||
getExistingView(activatedRoute: ActivatedRoute): RouteView|null {
|
||||
const activatedUrlKey = this.getUrl(activatedRoute);
|
||||
return this.views.find(vw => vw.url === activatedUrlKey);
|
||||
}
|
||||
|
||||
canGoBack(deep: number): boolean {
|
||||
return this.views.length > deep;
|
||||
}
|
||||
|
||||
async setActive(enteringView: RouteView, defaultDir: number|undefined) {
|
||||
const leavingView = this.getActive();
|
||||
const reused = this.insertView(enteringView);
|
||||
const direction = defaultDir != null ? defaultDir : (reused ? -1 : 1);
|
||||
await this.transition(enteringView, leavingView, direction);
|
||||
|
||||
this.cleanup();
|
||||
}
|
||||
|
||||
pop(deep: number) {
|
||||
const view = this.views[this.views.length - deep - 1];
|
||||
this.router.navigateByUrl(view.url);
|
||||
}
|
||||
|
||||
private insertView(enteringView: RouteView): boolean {
|
||||
if (this.stack) {
|
||||
const index = this.views.indexOf(enteringView);
|
||||
if (index >= 0) {
|
||||
this.views = this.views.slice(0, index + 1);
|
||||
return true;
|
||||
} else {
|
||||
this.views.push(enteringView);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
this.views = [enteringView];
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private cleanup() {
|
||||
this.viewsSnapshot
|
||||
.filter(view => !this.views.includes(view))
|
||||
.forEach(view => destroyView(view));
|
||||
|
||||
for (let i = 0; i < this.views.length - 1; i++) {
|
||||
this.views[i].element.hidden = true;
|
||||
}
|
||||
this.viewsSnapshot = this.views.slice();
|
||||
}
|
||||
|
||||
getActive(): RouteView | null {
|
||||
return this.views.length > 0 ? this.views[this.views.length - 1] : null;
|
||||
}
|
||||
|
||||
private async transition(enteringView: RouteView, leavingView: RouteView, direction: number) {
|
||||
const enteringEl = enteringView ? enteringView.element : undefined;
|
||||
const leavingEl = leavingView ? leavingView.element : undefined;
|
||||
const containerEl = this.containerEl;
|
||||
if (enteringEl) {
|
||||
enteringEl.classList.add('ion-page', 'hide-page');
|
||||
containerEl.appendChild(enteringEl);
|
||||
|
||||
await containerEl.componentOnReady();
|
||||
await containerEl.commit(enteringEl, leavingEl, {
|
||||
duration: direction === 0 ? 0 : undefined,
|
||||
direction: direction === -1 ? NavDirection.Back : NavDirection.Forward
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private getUrl(activatedRoute: ActivatedRoute) {
|
||||
const urlTree = this.router.createUrlTree(['.'], { relativeTo: activatedRoute });
|
||||
return this.router.serializeUrl(urlTree);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
export function initRouteViewElm(views: RouteView[], ref: ComponentRef<any>, activatedRoute: ActivatedRoute) {
|
||||
views.push({
|
||||
ref: ref,
|
||||
urlKey: getUrlKey(activatedRoute),
|
||||
deactivatedId: -1
|
||||
});
|
||||
|
||||
(ref.location.nativeElement as HTMLElement).classList.add('ion-page');
|
||||
export function destroyView(view: RouteView) {
|
||||
if (view) {
|
||||
// TODO lifecycle event
|
||||
view.ref.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function getExistingView(views: RouteView[], activatedRoute: ActivatedRoute) {
|
||||
return views.find(vw => {
|
||||
return isMatchingActivatedRoute(vw.urlKey, activatedRoute);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function isMatchingActivatedRoute(existingUrlKey: string, activatedRoute: ActivatedRoute) {
|
||||
const activatedUrlKey = getUrlKey(activatedRoute);
|
||||
|
||||
return activatedUrlKey === existingUrlKey;
|
||||
}
|
||||
|
||||
|
||||
export function getLastDeactivatedRef(views: RouteView[]) {
|
||||
if (views.length < 2) {
|
||||
return null;
|
||||
@ -45,37 +119,9 @@ export function getLastDeactivatedRef(views: RouteView[]) {
|
||||
})[0].ref;
|
||||
}
|
||||
|
||||
|
||||
function getUrlKey(activatedRoute: ActivatedRoute) {
|
||||
const url: UrlSegment[] = (activatedRoute.url as any).value;
|
||||
|
||||
return url.map(u => {
|
||||
return u.path + '$$' + JSON.stringify(u.parameters);
|
||||
}).join('/');
|
||||
}
|
||||
|
||||
|
||||
export function deactivateView(views: RouteView[], ref: ComponentRef<any>) {
|
||||
const view = views.find(vw => vw.ref === ref);
|
||||
if (view) {
|
||||
view.deactivatedId = deactivatedIds++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function destoryViews(views: RouteView[]) {
|
||||
views.forEach(vw => {
|
||||
vw.ref.destroy();
|
||||
});
|
||||
views.length = 0;
|
||||
}
|
||||
|
||||
|
||||
export interface RouteView {
|
||||
urlKey: string;
|
||||
url: string;
|
||||
element: HTMLElement;
|
||||
ref: ComponentRef<any>;
|
||||
deactivatedId: number;
|
||||
}
|
||||
|
||||
|
||||
let deactivatedIds = 0;
|
||||
|
Reference in New Issue
Block a user