mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
@@ -1,4 +1,4 @@
|
||||
import { defineCustomElements } from '@ionic/core/loader';
|
||||
import * as c from '@ionic/core/loader';
|
||||
import { Config } from './providers/config';
|
||||
import { IonicWindow } from './types/interfaces';
|
||||
|
||||
@@ -32,8 +32,104 @@ export function appInitialize(config: Config) {
|
||||
}
|
||||
};
|
||||
|
||||
// define all of Ionic's custom elements
|
||||
defineCustomElements(win);
|
||||
c.defineCustomElement(win, [
|
||||
c.IonActionSheet,
|
||||
c.IonActionSheetController,
|
||||
c.IonAlert,
|
||||
c.IonAlertController,
|
||||
c.IonAnchor,
|
||||
c.IonAnimationController,
|
||||
c.IonApp,
|
||||
c.IonAvatar,
|
||||
c.IonBackButton,
|
||||
c.IonBackdrop,
|
||||
c.IonBadge,
|
||||
c.IonButton,
|
||||
c.IonButtons,
|
||||
c.IonCard,
|
||||
c.IonCardContent,
|
||||
c.IonCardHeader,
|
||||
c.IonCardSubtitle,
|
||||
c.IonCardTitle,
|
||||
c.IonCheckbox,
|
||||
c.IonChip,
|
||||
c.IonCol,
|
||||
c.IonContent,
|
||||
c.IonDatetime,
|
||||
c.IonFab,
|
||||
c.IonFabButton,
|
||||
c.IonFabList,
|
||||
c.IonFooter,
|
||||
c.IonGrid,
|
||||
c.IonHeader,
|
||||
c.IonIcon,
|
||||
c.IonImg,
|
||||
c.IonInfiniteScroll,
|
||||
c.IonInfiniteScrollContent,
|
||||
c.IonInput,
|
||||
c.IonItem,
|
||||
c.IonItemDivider,
|
||||
c.IonItemGroup,
|
||||
c.IonItemOption,
|
||||
c.IonItemOptions,
|
||||
c.IonItemSliding,
|
||||
c.IonLabel,
|
||||
c.IonList,
|
||||
c.IonListHeader,
|
||||
c.IonLoading,
|
||||
c.IonLoadingController,
|
||||
c.IonMenu,
|
||||
c.IonMenuButton,
|
||||
c.IonMenuController,
|
||||
c.IonMenuToggle,
|
||||
c.IonModal,
|
||||
c.IonModalController,
|
||||
c.IonNav,
|
||||
c.IonNavPop,
|
||||
c.IonNavPush,
|
||||
c.IonNavSetRoot,
|
||||
c.IonNote,
|
||||
c.IonPicker,
|
||||
c.IonPickerColumn,
|
||||
c.IonPickerController,
|
||||
c.IonPopover,
|
||||
c.IonPopoverController,
|
||||
c.IonRadio,
|
||||
c.IonRadioGroup,
|
||||
c.IonRange,
|
||||
c.IonRefresher,
|
||||
c.IonRefresherContent,
|
||||
c.IonReorder,
|
||||
c.IonReorderGroup,
|
||||
c.IonRippleEffect,
|
||||
c.IonRoute,
|
||||
c.IonRouteRedirect,
|
||||
c.IonRouter,
|
||||
c.IonRouterOutlet,
|
||||
c.IonRow,
|
||||
c.IonSearchbar,
|
||||
c.IonSegment,
|
||||
c.IonSegmentButton,
|
||||
c.IonSelect,
|
||||
c.IonSelectOption,
|
||||
c.IonSelectPopover,
|
||||
c.IonSkeletonText,
|
||||
c.IonSlide,
|
||||
c.IonSlides,
|
||||
c.IonSpinner,
|
||||
c.IonSplitPane,
|
||||
c.IonTabBar,
|
||||
c.IonTabButton,
|
||||
c.IonText,
|
||||
c.IonTextarea,
|
||||
c.IonThumbnail,
|
||||
c.IonTitle,
|
||||
c.IonToast,
|
||||
c.IonToastController,
|
||||
c.IonToggle,
|
||||
c.IonToolbar,
|
||||
c.IonVirtualScroll
|
||||
]);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,10 +5,9 @@ export { RadioValueAccessor } from './control-value-accessors/radio-value-access
|
||||
export { SelectValueAccessor } from './control-value-accessors/select-value-accessor';
|
||||
export { TextValueAccessor } from './control-value-accessors/text-value-accessor';
|
||||
|
||||
export { IonTabs } from './navigation/ion-tabs';
|
||||
export { IonBackButtonDelegate } from './navigation/ion-back-button';
|
||||
export { NavDelegate } from './navigation/nav-delegate';
|
||||
export { TabDelegate } from './navigation/tab-delegate';
|
||||
export { TabsDelegate } from './navigation/tabs-delegate';
|
||||
export { IonRouterOutlet } from './navigation/ion-router-outlet';
|
||||
export { RouterLinkDelegate } from './navigation/router-link-delegate';
|
||||
export { NavParams } from './navigation/nav-params';
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Attribute, ChangeDetectorRef, ComponentFactoryResolver, ComponentRef, Directive, ElementRef, EventEmitter, Injector, Input, NgZone, OnDestroy, OnInit, Optional, Output, ViewContainerRef } from '@angular/core';
|
||||
import { ActivatedRoute, ChildrenOutletContexts, OutletContext, PRIMARY_OUTLET, Router } from '@angular/router';
|
||||
import { RouteView, StackController } from './stack-controller';
|
||||
import { StackController } from './stack-controller';
|
||||
import { NavController } from '../../providers/nav-controller';
|
||||
import { bindLifecycleEvents } from '../../providers/angular-delegate';
|
||||
import { RouteView, getUrl } from './stack-utils';
|
||||
|
||||
@Directive({
|
||||
selector: 'ion-router-outlet',
|
||||
@@ -20,6 +21,8 @@ export class IonRouterOutlet implements OnDestroy, OnInit {
|
||||
private nativeEl: HTMLIonRouterOutletElement;
|
||||
private hasStack = false;
|
||||
|
||||
tabsPrefix: string | undefined;
|
||||
|
||||
@Output('activate') activateEvents = new EventEmitter<any>();
|
||||
@Output('deactivate') deactivateEvents = new EventEmitter<any>();
|
||||
|
||||
@@ -43,18 +46,20 @@ export class IonRouterOutlet implements OnDestroy, OnInit {
|
||||
private location: ViewContainerRef,
|
||||
private resolver: ComponentFactoryResolver,
|
||||
@Attribute('name') name: string,
|
||||
@Optional() @Attribute('stack') stack: any,
|
||||
@Optional() @Attribute('tabs') tabs: string,
|
||||
private changeDetector: ChangeDetectorRef,
|
||||
private navCtrl: NavController,
|
||||
navCtrl: NavController,
|
||||
elementRef: ElementRef,
|
||||
router: Router,
|
||||
zone: NgZone
|
||||
zone: NgZone,
|
||||
activatedRoute: ActivatedRoute
|
||||
) {
|
||||
this.nativeEl = elementRef.nativeElement;
|
||||
this.name = name || PRIMARY_OUTLET;
|
||||
this.tabsPrefix = tabs === 'true' ? getUrl(router, activatedRoute) : undefined;
|
||||
this.stackCtrl = new StackController(this.tabsPrefix, this.nativeEl, router, navCtrl, zone);
|
||||
|
||||
parentContexts.onChildOutletCreated(this.name, this as any);
|
||||
this.hasStack = stack !== 'false' && stack !== false;
|
||||
this.stackCtrl = new StackController(this.hasStack, this.nativeEl, router, this.navCtrl, zone);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
@@ -169,32 +174,32 @@ export class IonRouterOutlet implements OnDestroy, OnInit {
|
||||
enteringView = this.stackCtrl.createView(this.activated, activatedRoute);
|
||||
}
|
||||
|
||||
const { direction, animated } = this.navCtrl.consumeTransition();
|
||||
this.activatedView = enteringView;
|
||||
this.stackCtrl.setActive(enteringView, direction, animated).then(() => {
|
||||
this.stackCtrl.setActive(enteringView).then(() => {
|
||||
this.activateEvents.emit(cmpRef.instance);
|
||||
emitEvent(this.nativeEl);
|
||||
emitEvent(this.nativeEl, enteringView!);
|
||||
});
|
||||
}
|
||||
|
||||
canGoBack(deep = 1) {
|
||||
return this.stackCtrl.canGoBack(deep);
|
||||
canGoBack(deep = 1, stackId?: string) {
|
||||
return this.stackCtrl.canGoBack(deep, stackId);
|
||||
}
|
||||
|
||||
pop(deep = 1) {
|
||||
return this.stackCtrl.pop(deep);
|
||||
pop(deep = 1, stackId?: string) {
|
||||
return this.stackCtrl.pop(deep, stackId);
|
||||
}
|
||||
|
||||
getLastUrl() {
|
||||
const active = this.stackCtrl.getActive();
|
||||
getLastUrl(stackId?: string) {
|
||||
const active = this.stackCtrl.getLastUrl(stackId);
|
||||
return active ? active.url : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function emitEvent(el: HTMLElement) {
|
||||
function emitEvent(el: HTMLElement, view: RouteView) {
|
||||
const ev = new CustomEvent('ionRouterOutletActivated', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
detail: { view }
|
||||
});
|
||||
el.dispatchEvent(ev);
|
||||
}
|
||||
|
||||
69
angular/src/directives/navigation/ion-tabs.ts
Normal file
69
angular/src/directives/navigation/ion-tabs.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { Component, ContentChild, HostListener, ViewChild } from '@angular/core';
|
||||
import { TabButtonClickDetail } from '@ionic/core';
|
||||
import { IonRouterOutlet } from './ion-router-outlet';
|
||||
import { NavController } from '../../providers';
|
||||
import { IonTabBar } from '../proxies';
|
||||
import { RouteView } from './stack-utils';
|
||||
|
||||
@Component({
|
||||
selector: 'ion-tabs',
|
||||
template: `
|
||||
<div class="tabs-inner">
|
||||
<ion-router-outlet #outlet tabs="true"></ion-router-outlet>
|
||||
</div>
|
||||
<ng-content></ng-content>`,
|
||||
styles: [`
|
||||
:host {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
flex-direction: column;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
contain: layout size style;
|
||||
z-index: $z-index-page-container;
|
||||
}
|
||||
.tabs-inner {
|
||||
position: relative;
|
||||
|
||||
flex: 1;
|
||||
|
||||
contain: layout size style;
|
||||
}`
|
||||
]
|
||||
})
|
||||
export class IonTabs {
|
||||
|
||||
@ViewChild('outlet', { read: IonRouterOutlet }) outlet: IonRouterOutlet;
|
||||
@ContentChild(IonTabBar) tabBar: IonTabBar | undefined;
|
||||
|
||||
constructor(
|
||||
private navCtrl: NavController,
|
||||
) {}
|
||||
|
||||
@HostListener('ionRouterOutletActivated', ['$event.detail'])
|
||||
onPageSelected(detail: {view: RouteView}) {
|
||||
if (this.tabBar) {
|
||||
this.tabBar.selectedTab = detail.view.stackId;
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener('ionTabButtonClick', ['$event.detail'])
|
||||
onTabButtonClick(detail: TabButtonClickDetail) {
|
||||
const { tab, selected } = detail;
|
||||
if (tab) {
|
||||
const href = `${this.outlet.tabsPrefix}/${tab}`;
|
||||
const url = selected
|
||||
? href
|
||||
: this.outlet.getLastUrl(tab) || href;
|
||||
|
||||
this.navCtrl.navigateBack(url, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,6 @@ export class RouterLinkDelegate {
|
||||
private updateTargetUrlAndHref() {
|
||||
if (this.routerLink) {
|
||||
const href = this.locationStrategy.prepareExternalUrl(this.router.serializeUrl(this.routerLink.urlTree));
|
||||
console.log(href);
|
||||
this.elementRef.nativeElement.href = href;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import { ComponentRef, NgZone } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
|
||||
import { NavController, NavDirection } from '../../providers/nav-controller';
|
||||
import { RouterDirection } from '@ionic/core';
|
||||
import { RouteView, computeStackId, destroyView, getUrl, insertView, isTabSwitch, toSegments } from './stack-utils';
|
||||
|
||||
|
||||
export class StackController {
|
||||
@@ -10,50 +12,65 @@ export class StackController {
|
||||
private views: RouteView[] = [];
|
||||
private runningTransition?: Promise<boolean>;
|
||||
private skipTransition = false;
|
||||
private tabsPrefix: string[] | undefined;
|
||||
private activeView: RouteView | undefined;
|
||||
private nextId = 0;
|
||||
|
||||
constructor(
|
||||
private stack: boolean,
|
||||
tabsPrefix: string | undefined,
|
||||
private containerEl: HTMLIonRouterOutletElement,
|
||||
private router: Router,
|
||||
private navCtrl: NavController,
|
||||
private zone: NgZone,
|
||||
) {}
|
||||
) {
|
||||
this.tabsPrefix = tabsPrefix ? toSegments(tabsPrefix) : undefined;
|
||||
}
|
||||
|
||||
createView(enteringRef: ComponentRef<any>, activatedRoute: ActivatedRoute): RouteView {
|
||||
const url = getUrl(this.router, activatedRoute);
|
||||
return {
|
||||
id: this.nextId++,
|
||||
ref: enteringRef,
|
||||
element: (enteringRef && enteringRef.location && enteringRef.location.nativeElement) as HTMLElement,
|
||||
url: this.getUrl(activatedRoute)
|
||||
stackId: computeStackId(this.tabsPrefix, url),
|
||||
url,
|
||||
};
|
||||
}
|
||||
|
||||
getExistingView(activatedRoute: ActivatedRoute): RouteView | undefined {
|
||||
const activatedUrlKey = this.getUrl(activatedRoute);
|
||||
const activatedUrlKey = getUrl(this.router, activatedRoute);
|
||||
return this.views.find(vw => vw.url === activatedUrlKey);
|
||||
}
|
||||
|
||||
async setActive(enteringView: RouteView, direction: NavDirection, animated: boolean) {
|
||||
const leavingView = this.getActive();
|
||||
async setActive(enteringView: RouteView) {
|
||||
let { direction, animated } = this.navCtrl.consumeTransition();
|
||||
const leavingView = this.activeView;
|
||||
if (isTabSwitch(enteringView, leavingView)) {
|
||||
direction = 'back';
|
||||
animated = false;
|
||||
}
|
||||
this.insertView(enteringView, direction);
|
||||
await this.transition(enteringView, leavingView, direction, animated, this.canGoBack(1), false);
|
||||
this.cleanup();
|
||||
}
|
||||
|
||||
canGoBack(deep: number): boolean {
|
||||
return this.views.length > deep;
|
||||
canGoBack(deep: number, stackId = this.getActiveStackId()): boolean {
|
||||
return this.getStack(stackId).length > deep;
|
||||
}
|
||||
|
||||
pop(deep: number) {
|
||||
pop(deep: number, stackId = this.getActiveStackId()) {
|
||||
this.zone.run(() => {
|
||||
const view = this.views[this.views.length - deep - 1];
|
||||
const views = this.getStack(stackId);
|
||||
const view = views[views.length - deep - 1];
|
||||
this.navCtrl.navigateBack(view.url);
|
||||
});
|
||||
}
|
||||
|
||||
startBackTransition() {
|
||||
startBackTransition(stackId?: string) {
|
||||
const views = this.getStack(stackId);
|
||||
this.transition(
|
||||
this.views[this.views.length - 2], // entering view
|
||||
this.views[this.views.length - 1], // leaving view
|
||||
views[views.length - 2], // entering view
|
||||
views[views.length - 1], // leaving view
|
||||
'back',
|
||||
true,
|
||||
true,
|
||||
@@ -68,55 +85,41 @@ export class StackController {
|
||||
}
|
||||
}
|
||||
|
||||
getLastUrl(stackId?: string) {
|
||||
const views = this.getStack(stackId);
|
||||
return views.length > 0 ? views[views.length - 1] : undefined;
|
||||
}
|
||||
|
||||
private insertView(enteringView: RouteView, direction: NavDirection) {
|
||||
// no stack
|
||||
if (!this.stack) {
|
||||
this.views = [enteringView];
|
||||
return;
|
||||
}
|
||||
private getActiveStackId(): string | undefined {
|
||||
return this.activeView ? this.activeView.stackId : undefined;
|
||||
}
|
||||
|
||||
// stack setRoot
|
||||
if (direction === 'root') {
|
||||
this.views = [enteringView];
|
||||
return;
|
||||
}
|
||||
private getStack(stackId: string | undefined) {
|
||||
return this.views.filter(v => v.stackId === stackId);
|
||||
}
|
||||
|
||||
// stack
|
||||
const index = this.views.indexOf(enteringView);
|
||||
if (index >= 0) {
|
||||
this.views = this.views.slice(0, index + 1);
|
||||
} else {
|
||||
if (direction === 'forward') {
|
||||
this.views.push(enteringView);
|
||||
} else {
|
||||
this.views = [enteringView];
|
||||
}
|
||||
}
|
||||
private insertView(enteringView: RouteView, direction: RouterDirection) {
|
||||
this.activeView = enteringView;
|
||||
this.views = insertView(this.views, enteringView, direction);
|
||||
}
|
||||
|
||||
private cleanup() {
|
||||
const activeRoute = this.activeView;
|
||||
const views = this.views;
|
||||
this.viewsSnapshot
|
||||
.filter(view => !views.includes(view))
|
||||
.forEach(view => destroyView(view));
|
||||
|
||||
for (let i = 0; i < views.length - 1; i++) {
|
||||
const view = views[i];
|
||||
const element = view.element;
|
||||
element.setAttribute('aria-hidden', 'true');
|
||||
element.classList.add('ion-page-hidden');
|
||||
// TODO
|
||||
// view.ref.changeDetectorRef.detach();
|
||||
}
|
||||
views.forEach(view => {
|
||||
if (view !== activeRoute) {
|
||||
const element = view.element;
|
||||
element.setAttribute('aria-hidden', 'true');
|
||||
element.classList.add('ion-page-hidden');
|
||||
}
|
||||
});
|
||||
this.viewsSnapshot = views.slice();
|
||||
}
|
||||
|
||||
getActive(): RouteView | undefined {
|
||||
const views = this.views;
|
||||
return views.length > 0 ? views[views.length - 1] : undefined;
|
||||
}
|
||||
|
||||
private async transition(
|
||||
enteringView: RouteView | undefined,
|
||||
leavingView: RouteView | undefined,
|
||||
@@ -158,23 +161,4 @@ export class StackController {
|
||||
await this.runningTransition;
|
||||
}
|
||||
}
|
||||
|
||||
private getUrl(activatedRoute: ActivatedRoute) {
|
||||
const urlTree = this.router.createUrlTree(['.'], { relativeTo: activatedRoute });
|
||||
return this.router.serializeUrl(urlTree);
|
||||
}
|
||||
}
|
||||
|
||||
function destroyView(view: RouteView) {
|
||||
if (view) {
|
||||
// TODO lifecycle event
|
||||
view.ref.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
export interface RouteView {
|
||||
url: string;
|
||||
element: HTMLElement;
|
||||
ref: ComponentRef<any>;
|
||||
savedData?: any;
|
||||
}
|
||||
|
||||
90
angular/src/directives/navigation/stack-utils.ts
Normal file
90
angular/src/directives/navigation/stack-utils.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { ComponentRef } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { RouterDirection } from '@ionic/core';
|
||||
|
||||
export function insertView(views: RouteView[], view: RouteView, direction: RouterDirection) {
|
||||
if (direction === 'root') {
|
||||
return setRoot(views, view);
|
||||
} else if (direction === 'forward') {
|
||||
return setForward(views, view);
|
||||
} else {
|
||||
return setBack(views, view);
|
||||
}
|
||||
}
|
||||
|
||||
function setRoot(views: RouteView[], view: RouteView) {
|
||||
views = views.filter(v => v.stackId !== view.stackId);
|
||||
views.push(view);
|
||||
return views;
|
||||
}
|
||||
|
||||
function setForward(views: RouteView[], view: RouteView) {
|
||||
const index = views.indexOf(view);
|
||||
if (index >= 0) {
|
||||
views = views.filter(v => v.stackId !== view.stackId || v.id <= view.id);
|
||||
} else {
|
||||
views.push(view);
|
||||
}
|
||||
return views;
|
||||
}
|
||||
|
||||
function setBack(views: RouteView[], view: RouteView) {
|
||||
const index = views.indexOf(view);
|
||||
if (index >= 0) {
|
||||
return views.filter(v => v.stackId !== view.stackId || v.id <= view.id);
|
||||
} else {
|
||||
return setRoot(views, view);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function getUrl(router: Router, activatedRoute: ActivatedRoute) {
|
||||
const urlTree = router.createUrlTree(['.'], { relativeTo: activatedRoute });
|
||||
return router.serializeUrl(urlTree);
|
||||
}
|
||||
|
||||
export function isTabSwitch(enteringView: RouteView, leavingView: RouteView | undefined) {
|
||||
if (!leavingView) {
|
||||
return false;
|
||||
}
|
||||
return enteringView.stackId !== leavingView.stackId;
|
||||
}
|
||||
|
||||
export function computeStackId(prefixUrl: string[] | undefined, url: string) {
|
||||
if (!prefixUrl) {
|
||||
return undefined;
|
||||
}
|
||||
const segments = toSegments(url);
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
if (i >= prefixUrl.length) {
|
||||
return segments[i];
|
||||
}
|
||||
if (segments[i] !== prefixUrl[i]) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function toSegments(path: string): string[] {
|
||||
return path
|
||||
.split('/')
|
||||
.map(s => s.trim())
|
||||
.filter(s => s !== '');
|
||||
}
|
||||
|
||||
export function destroyView(view: RouteView) {
|
||||
if (view) {
|
||||
// TODO lifecycle event
|
||||
view.ref.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
export interface RouteView {
|
||||
id: number;
|
||||
url: string;
|
||||
stackId: string | undefined;
|
||||
element: HTMLElement;
|
||||
ref: ComponentRef<any>;
|
||||
savedData?: any;
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import { ComponentFactoryResolver, ContentChild, Directive, ElementRef, HostListener, Injector, ViewContainerRef } from '@angular/core';
|
||||
import { AngularDelegate } from '../../providers/angular-delegate';
|
||||
import { IonRouterOutlet } from './ion-router-outlet';
|
||||
|
||||
|
||||
@Directive({
|
||||
selector: 'ion-tab'
|
||||
})
|
||||
export class TabDelegate {
|
||||
|
||||
@ContentChild(IonRouterOutlet) outlet?: IonRouterOutlet;
|
||||
|
||||
private nativeEl: HTMLIonTabElement;
|
||||
|
||||
constructor(
|
||||
elementRef: ElementRef,
|
||||
resolver: ComponentFactoryResolver,
|
||||
injector: Injector,
|
||||
angularDelegate: AngularDelegate,
|
||||
location: ViewContainerRef
|
||||
) {
|
||||
this.nativeEl = elementRef.nativeElement;
|
||||
this.nativeEl.delegate = angularDelegate.create(resolver, injector, location);
|
||||
}
|
||||
|
||||
get tab() {
|
||||
return this.nativeEl.tab;
|
||||
}
|
||||
|
||||
getLastUrl() {
|
||||
return this.outlet ? this.outlet.getLastUrl() : undefined;
|
||||
}
|
||||
|
||||
@HostListener('ionRouterOutletActivated')
|
||||
async onNavChanged() {
|
||||
const tab = this.nativeEl;
|
||||
await tab.componentOnReady();
|
||||
const tabs = tab.closest('ion-tabs');
|
||||
if (tabs) {
|
||||
await tabs.componentOnReady();
|
||||
await tabs.select(tab);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import { ContentChildren, Directive, ElementRef, HostListener, Optional, QueryList } from '@angular/core';
|
||||
import { TabDelegate } from './tab-delegate';
|
||||
import { TabButtonClickDetail } from '@ionic/core';
|
||||
import { NavController } from '../../providers';
|
||||
|
||||
@Directive({
|
||||
selector: 'ion-tabs'
|
||||
})
|
||||
export class TabsDelegate {
|
||||
|
||||
@ContentChildren(TabDelegate) tabs!: QueryList<TabDelegate>;
|
||||
|
||||
constructor(
|
||||
@Optional() private navCtrl: NavController,
|
||||
elementRef: ElementRef
|
||||
) {
|
||||
const nativeEl = elementRef.nativeElement as HTMLIonTabsElement;
|
||||
nativeEl.useRouter = true;
|
||||
}
|
||||
|
||||
urlForTab(tab: string) {
|
||||
const tabDelegate = this.tabs.find(item => item.tab === tab);
|
||||
return tabDelegate ? tabDelegate.getLastUrl() : undefined;
|
||||
}
|
||||
|
||||
@HostListener('ionTabButtonClick', ['$event.detail'])
|
||||
onTabbarClick(detail: TabButtonClickDetail) {
|
||||
const { tab, href, selected } = detail;
|
||||
if (tab && href) {
|
||||
const url = selected
|
||||
? href
|
||||
: this.urlForTab(tab) || href;
|
||||
|
||||
this.navCtrl.navigateBack(url, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,10 +67,8 @@ export const DIRECTIVES = [
|
||||
d.IonSlides,
|
||||
d.IonSpinner,
|
||||
d.IonSplitPane,
|
||||
d.IonTab,
|
||||
d.IonTabBar,
|
||||
d.IonTabButton,
|
||||
d.IonTabs,
|
||||
d.IonText,
|
||||
d.IonTextarea,
|
||||
d.IonThumbnail,
|
||||
|
||||
@@ -834,18 +834,6 @@ export class IonSplitPane {
|
||||
}
|
||||
}
|
||||
|
||||
export declare interface IonTab extends StencilComponents<'IonTab'> {}
|
||||
@Component({ selector: 'ion-tab', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: '<ng-content></ng-content>', inputs: ['active', 'delegate', 'tab', 'component'] })
|
||||
export class IonTab {
|
||||
|
||||
constructor(c: ChangeDetectorRef, r: ElementRef) {
|
||||
c.detach();
|
||||
const el = r.nativeElement;
|
||||
proxyMethods(this, el, ['setActive']);
|
||||
proxyInputs(this, el, ['active', 'delegate', 'tab', 'component']);
|
||||
}
|
||||
}
|
||||
|
||||
export declare interface IonTabBar extends StencilComponents<'IonTabBar'> {}
|
||||
@Component({ selector: 'ion-tab-bar', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: '<ng-content></ng-content>', inputs: ['mode', 'color', 'selectedTab', 'translucent'] })
|
||||
export class IonTabBar {
|
||||
@@ -872,23 +860,6 @@ export class IonTabButton {
|
||||
}
|
||||
}
|
||||
|
||||
export declare interface IonTabs extends StencilComponents<'IonTabs'> {}
|
||||
@Component({ selector: 'ion-tabs', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: '<ng-content></ng-content>', inputs: ['useRouter'] })
|
||||
export class IonTabs {
|
||||
ionChange!: EventEmitter<CustomEvent>;
|
||||
ionNavWillLoad!: EventEmitter<CustomEvent>;
|
||||
ionNavWillChange!: EventEmitter<CustomEvent>;
|
||||
ionNavDidChange!: EventEmitter<CustomEvent>;
|
||||
|
||||
constructor(c: ChangeDetectorRef, r: ElementRef) {
|
||||
c.detach();
|
||||
const el = r.nativeElement;
|
||||
proxyMethods(this, el, ['select', 'setRouteId', 'getRouteId', 'getTab', 'getSelected']);
|
||||
proxyInputs(this, el, ['useRouter']);
|
||||
proxyOutputs(this, el, ['ionChange', 'ionNavWillLoad', 'ionNavWillChange', 'ionNavDidChange']);
|
||||
}
|
||||
}
|
||||
|
||||
export declare interface IonText extends StencilComponents<'IonText'> {}
|
||||
@Component({ selector: 'ion-text', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: '<ng-content></ng-content>', inputs: ['color', 'mode'] })
|
||||
export class IonText {
|
||||
|
||||
@@ -76,10 +76,8 @@ const DECLARATIONS = [
|
||||
d.IonSlides,
|
||||
d.IonSpinner,
|
||||
d.IonSplitPane,
|
||||
d.IonTab,
|
||||
d.IonTabBar,
|
||||
d.IonTabButton,
|
||||
d.IonTabs,
|
||||
d.IonText,
|
||||
d.IonTextarea,
|
||||
d.IonThumbnail,
|
||||
@@ -87,6 +85,8 @@ const DECLARATIONS = [
|
||||
d.IonToolbar,
|
||||
d.IonTitle,
|
||||
|
||||
c.IonTabs,
|
||||
|
||||
// ngModel accessors
|
||||
c.BooleanValueAccessor,
|
||||
c.NumericValueAccessor,
|
||||
@@ -98,8 +98,6 @@ const DECLARATIONS = [
|
||||
c.IonRouterOutlet,
|
||||
c.IonBackButtonDelegate,
|
||||
c.NavDelegate,
|
||||
c.TabDelegate,
|
||||
c.TabsDelegate,
|
||||
c.RouterLinkDelegate,
|
||||
|
||||
// virtual scroll
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Injectable } from '@angular/core';
|
||||
import { Location } from '@angular/common';
|
||||
import { NavigationExtras, NavigationStart, Router, UrlTree } from '@angular/router';
|
||||
import { Platform } from './platform';
|
||||
import { RouterDirection } from '@ionic/core';
|
||||
|
||||
export type NavDirection = 'forward' | 'back' | 'root' | 'auto';
|
||||
|
||||
@@ -10,7 +11,7 @@ export class NavController {
|
||||
|
||||
private direction: NavDirection = DEFAULT_DIRECTION;
|
||||
private animated = DEFAULT_ANIMATED;
|
||||
private guessDirection: NavDirection = 'forward';
|
||||
private guessDirection: RouterDirection = 'forward';
|
||||
private guessAnimation = false;
|
||||
private lastNavId = -1;
|
||||
|
||||
@@ -74,7 +75,7 @@ export class NavController {
|
||||
}
|
||||
|
||||
consumeTransition() {
|
||||
let direction: NavDirection = 'root';
|
||||
let direction: RouterDirection = 'root';
|
||||
let animated = false;
|
||||
|
||||
if (this.direction === 'auto') {
|
||||
|
||||
85
angular/test/test-app/e2e/src/tabs.e2e-spec.ts
Normal file
85
angular/test/test-app/e2e/src/tabs.e2e-spec.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { browser, element, by, ElementFinder } from 'protractor';
|
||||
import { waitTime, testStack, handleErrorMessages } from './utils';
|
||||
|
||||
describe('tabs', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
await browser.get('/tabs');
|
||||
});
|
||||
afterEach(() => {
|
||||
handleErrorMessages();
|
||||
});
|
||||
|
||||
it('should redirect and load tab-account', async () => {
|
||||
await testTabTitle('Tab 1 - Page 1');
|
||||
await testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1']);
|
||||
});
|
||||
|
||||
it('should simulate stack + double tab click', async () => {
|
||||
const tab = await getSelectedTab() as ElementFinder;
|
||||
await tab.$('#goto-tab1-page2').click();
|
||||
await testTabTitle('Tab 1 - Page 2');
|
||||
await testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1', 'app-tabs-tab1-nested']);
|
||||
|
||||
await element(by.css('#tab-button-contact')).click();
|
||||
await testTabTitle('Tab 2 - Page 1');
|
||||
await testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1', 'app-tabs-tab1-nested', 'app-tabs-tab2']);
|
||||
|
||||
await element(by.css('#tab-button-account')).click();
|
||||
await testTabTitle('Tab 1 - Page 2');
|
||||
await testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1', 'app-tabs-tab1-nested', 'app-tabs-tab2']);
|
||||
|
||||
await element(by.css('#tab-button-account')).click();
|
||||
await testTabTitle('Tab 1 - Page 1');
|
||||
await testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1', 'app-tabs-tab2']);
|
||||
});
|
||||
|
||||
it('should simulate stack + back button click', async () => {
|
||||
const tab = await getSelectedTab();
|
||||
await tab.$('#goto-tab1-page2').click();
|
||||
await testTabTitle('Tab 1 - Page 2');
|
||||
|
||||
await element(by.css('#tab-button-contact')).click();
|
||||
await testTabTitle('Tab 2 - Page 1');
|
||||
|
||||
await element(by.css('#tab-button-account')).click();
|
||||
await testTabTitle('Tab 1 - Page 2');
|
||||
|
||||
await element(by.css('ion-back-button')).click();
|
||||
await testTabTitle('Tab 1 - Page 1');
|
||||
await testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1', 'app-tabs-tab2']);
|
||||
});
|
||||
|
||||
it('should switch tabs and go back', async () => {
|
||||
await element(by.css('#tab-button-contact')).click();
|
||||
const tab = await testTabTitle('Tab 2 - Page 1');
|
||||
|
||||
await tab.$('#goto-tab1-page1').click();
|
||||
await testTabTitle('Tab 1 - Page 1');
|
||||
await testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1', 'app-tabs-tab2']);
|
||||
});
|
||||
|
||||
it('should switch tabs and go to nested', async () => {
|
||||
await element(by.css('#tab-button-contact')).click();
|
||||
const tab = await testTabTitle('Tab 2 - Page 1');
|
||||
|
||||
await tab.$('#goto-tab1-page2').click();
|
||||
await testTabTitle('Tab 1 - Page 2');
|
||||
await testStack('ion-tabs ion-router-outlet', ['app-tabs-tab2', 'app-tabs-tab1-nested']);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
async function testTabTitle(title: string) {
|
||||
await waitTime(600);
|
||||
const tab = await getSelectedTab();
|
||||
expect(await tab.$('ion-title').getText()).toEqual(title);
|
||||
return tab;
|
||||
}
|
||||
|
||||
async function getSelectedTab(): Promise<ElementFinder> {
|
||||
const tabs = element.all(by.css('ion-tabs ion-router-outlet > *:not(.ion-page-hidden)'));
|
||||
expect(await tabs.count()).toEqual(1);
|
||||
const tab = tabs.first();
|
||||
return tab;
|
||||
}
|
||||
@@ -59,7 +59,7 @@ export async function testLifeCycle(selector: string, expected: LifeCycleCount)
|
||||
}
|
||||
|
||||
export async function testStack(selector: string, expected: string[]) {
|
||||
const children = browser.executeScript(`
|
||||
const children = await browser.executeScript(`
|
||||
return Array.from(
|
||||
document.querySelector('${selector}').children
|
||||
).map(el => el.tagName.toLowerCase());
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"start": "npm run sync && ng serve",
|
||||
"sync:build": "sh scripts/build-ionic.sh",
|
||||
"sync": "sh scripts/sync.sh",
|
||||
"build": "ng build --prod --no-progress",
|
||||
|
||||
@@ -6,6 +6,9 @@ import { RouterLinkComponent } from './router-link/router-link.component';
|
||||
import { RouterLinkPageComponent } from './router-link-page/router-link-page.component';
|
||||
import { HomePageComponent } from './home-page/home-page.component';
|
||||
import { TabsComponent } from './tabs/tabs.component';
|
||||
import { TabsTab1Component } from './tabs-tab1/tabs-tab1.component';
|
||||
import { TabsTab1NestedComponent } from './tabs-tab1-nested/tabs-tab1-nested.component';
|
||||
import { TabsTab2Component } from './tabs-tab2/tabs-tab2.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', component: HomePageComponent },
|
||||
@@ -13,7 +16,40 @@ const routes: Routes = [
|
||||
{ path: 'modals', component: ModalComponent },
|
||||
{ path: 'router-link', component: RouterLinkComponent },
|
||||
{ path: 'router-link-page', component: RouterLinkPageComponent },
|
||||
{ path: 'tabs', component: TabsComponent }
|
||||
{ path: 'tabs', redirectTo: '/tabs/account', pathMatch: 'full' },
|
||||
{
|
||||
path: 'tabs',
|
||||
component: TabsComponent,
|
||||
children: [
|
||||
{
|
||||
path: 'account',
|
||||
children: [
|
||||
{
|
||||
path: 'nested/:id',
|
||||
component: TabsTab1NestedComponent
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
component: TabsTab1Component
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'contact',
|
||||
children: [
|
||||
{
|
||||
path: 'one',
|
||||
component: TabsTab2Component
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
redirectTo: 'one',
|
||||
pathMatch: 'full'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
||||
@@ -12,6 +12,9 @@ import { RouterLinkComponent } from './router-link/router-link.component';
|
||||
import { RouterLinkPageComponent } from './router-link-page/router-link-page.component';
|
||||
import { HomePageComponent } from './home-page/home-page.component';
|
||||
import { TabsComponent } from './tabs/tabs.component';
|
||||
import { TabsTab1Component } from './tabs-tab1/tabs-tab1.component';
|
||||
import { TabsTab2Component } from './tabs-tab2/tabs-tab2.component';
|
||||
import { TabsTab1NestedComponent } from './tabs-tab1-nested/tabs-tab1-nested.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@@ -22,7 +25,10 @@ import { TabsComponent } from './tabs/tabs.component';
|
||||
RouterLinkComponent,
|
||||
RouterLinkPageComponent,
|
||||
HomePageComponent,
|
||||
TabsComponent
|
||||
TabsComponent,
|
||||
TabsTab1Component,
|
||||
TabsTab2Component,
|
||||
TabsTab1NestedComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Tab 1 - Page 2</ion-title>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button></ion-back-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content padding>
|
||||
<h1>Welcome to NESTED PAGE</h1>
|
||||
<p>
|
||||
<ion-button routerLink="/tabs/account" id="goto-tab1-page1">Go to Tab 1 - Page 1</ion-button>
|
||||
<ion-button routerLink="/tabs/contact" id="goto-tab2-page1">Go to Tab 2 - Page 1</ion-button>
|
||||
</p>
|
||||
</ion-content>
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-tabs-tab1-nested',
|
||||
templateUrl: './tabs-tab1-nested.component.html',
|
||||
})
|
||||
export class TabsTab1NestedComponent { }
|
||||
@@ -0,0 +1,12 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Tab 1 - Page 1</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content padding>
|
||||
<h1>Welcome to Tab1</h1>
|
||||
<p>
|
||||
<ion-button routerLink="/tabs/account/nested/12" id="goto-tab1-page2">Go to Page 2</ion-button>
|
||||
</p>
|
||||
</ion-content>
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-tabs-tab1',
|
||||
templateUrl: './tabs-tab1.component.html',
|
||||
})
|
||||
export class TabsTab1Component { }
|
||||
@@ -0,0 +1,13 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Tab 2 - Page 1</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content padding>
|
||||
<h1>Welcome to Tab 2</h1>
|
||||
<p>
|
||||
<ion-button routerLink="/tabs/account" id="goto-tab1-page1">Go to Tab 1 - Page 1</ion-button>
|
||||
<ion-button routerLink="/tabs/account/nested/12" id="goto-tab1-page2">Go to Tab 1 - Page 2</ion-button>
|
||||
</p>
|
||||
</ion-content>
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-tabs-tab2',
|
||||
templateUrl: './tabs-tab2.component.html',
|
||||
})
|
||||
export class TabsTab2Component { }
|
||||
@@ -1,21 +1,12 @@
|
||||
<ion-tabs>
|
||||
<ion-tab tab="tab-one">
|
||||
<ion-router-outlet name="tab-one"></ion-router-outlet>
|
||||
</ion-tab>
|
||||
|
||||
<ion-tab tab="tab-two">
|
||||
<ion-router-outlet name="tab-one"></ion-router-outlet>
|
||||
</ion-tab>
|
||||
|
||||
<ion-tab-bar slot="bottom">
|
||||
<ion-tab-button tab="tab-one">
|
||||
<ion-tab-bar>
|
||||
<ion-tab-button tab="account" id="account-tab-button">
|
||||
<ion-label>Tab One</ion-label>
|
||||
<ion-icon name="add"></ion-icon>
|
||||
</ion-tab-button>
|
||||
|
||||
<ion-tab-button tab="tab-two">
|
||||
<ion-tab-button tab="contact" id="contact-tab-button">
|
||||
<ion-label>Tab Two</ion-label>
|
||||
<ion-icon name="plane"></ion-icon>
|
||||
<ion-icon name="logo-ionic"></ion-icon>
|
||||
</ion-tab-button>
|
||||
</ion-tab-bar>
|
||||
</ion-tabs>
|
||||
|
||||
@@ -4,6 +4,4 @@ import { Component } from '@angular/core';
|
||||
selector: 'app-tabs',
|
||||
templateUrl: './tabs.component.html',
|
||||
})
|
||||
export class TabsComponent {
|
||||
|
||||
}
|
||||
export class TabsComponent { }
|
||||
|
||||
Reference in New Issue
Block a user