perf(angular): skip zone

This commit is contained in:
Manu Mtz.-Almeida
2019-07-06 19:33:34 +02:00
parent 7953088418
commit e059fc8048
19 changed files with 137 additions and 131 deletions

View File

@ -10,7 +10,14 @@ export function appInitialize(config: Config, doc: Document, zone: NgZone) {
if (win) { if (win) {
const Ionic = win.Ionic = win.Ionic || {}; const Ionic = win.Ionic = win.Ionic || {};
Ionic.config = config; Ionic.config = {
...config,
_zoneGate: (h: any) => zone.run(h)
};
const aelFn = '__zone_symbol__addEventListener' in (document.body as any)
? '__zone_symbol__addEventListener'
: 'addEventListener';
return applyPolyfills().then(() => { return applyPolyfills().then(() => {
return defineCustomElements(win, { return defineCustomElements(win, {
@ -23,41 +30,13 @@ export function appInitialize(config: Config, doc: Document, zone: NgZone) {
}); });
}, },
ael(elm, eventName, cb, opts) { ael(elm, eventName, cb, opts) {
if ((elm as any).__zone_symbol__addEventListener && skipZone(eventName)) { (elm as any)[aelFn](eventName, cb, opts);
(elm as any).__zone_symbol__addEventListener(eventName, cb, opts);
} else {
elm.addEventListener(eventName, cb, opts);
}
}, },
rel(elm, eventName, cb, opts) { rel(elm, eventName, cb, opts) {
if ((elm as any).__zone_symbol__removeEventListener && skipZone(eventName)) { elm.removeEventListener(eventName, cb, opts);
(elm as any).__zone_symbol__removeEventListener(eventName, cb, opts);
} else {
elm.removeEventListener(eventName, cb, opts);
}
} }
}); });
}); });
} }
}; };
} }
const SKIP_ZONE = [
'scroll',
'resize',
'touchstart',
'touchmove',
'touchend',
'mousedown',
'mousemove',
'mouseup',
'ionStyle',
'ionTabButtonClick'
];
function skipZone(eventName: string) {
return SKIP_ZONE.indexOf(eventName) >= 0;
}

View File

@ -42,16 +42,14 @@ export class ValueAccessor implements ControlValueAccessor {
} }
export function setIonicClasses(element: ElementRef) { export function setIonicClasses(element: ElementRef) {
requestAnimationFrame(() => { const input = element.nativeElement as HTMLElement;
const input = element.nativeElement as HTMLElement; const classes = getClasses(input);
const classes = getClasses(input); setClasses(input, classes);
setClasses(input, classes);
const item = input.closest('ion-item'); const item = input.closest('ion-item');
if (item) { if (item) {
setClasses(item, classes); setClasses(item, classes);
} }
});
} }
function getClasses(element: HTMLElement) { function getClasses(element: HTMLElement) {

View File

@ -1,5 +1,5 @@
import { Location } from '@angular/common'; import { Location } from '@angular/common';
import { Attribute, ChangeDetectorRef, ComponentFactoryResolver, ComponentRef, Directive, ElementRef, EventEmitter, Injector, NgZone, OnDestroy, OnInit, Optional, Output, SkipSelf, ViewContainerRef } from '@angular/core'; import { Attribute, ComponentFactoryResolver, ComponentRef, Directive, ElementRef, EventEmitter, Injector, NgZone, OnDestroy, OnInit, Optional, Output, SkipSelf, ViewContainerRef } from '@angular/core';
import { ActivatedRoute, ChildrenOutletContexts, OutletContext, PRIMARY_OUTLET, Router } from '@angular/router'; import { ActivatedRoute, ChildrenOutletContexts, OutletContext, PRIMARY_OUTLET, Router } from '@angular/router';
import { BehaviorSubject, Observable } from 'rxjs'; import { BehaviorSubject, Observable } from 'rxjs';
import { distinctUntilChanged, filter, switchMap } from 'rxjs/operators'; import { distinctUntilChanged, filter, switchMap } from 'rxjs/operators';
@ -57,7 +57,6 @@ export class IonRouterOutlet implements OnDestroy, OnInit {
private resolver: ComponentFactoryResolver, private resolver: ComponentFactoryResolver,
@Attribute('name') name: string, @Attribute('name') name: string,
@Optional() @Attribute('tabs') tabs: string, @Optional() @Attribute('tabs') tabs: string,
private changeDetector: ChangeDetectorRef,
private config: Config, private config: Config,
private navCtrl: NavController, private navCtrl: NavController,
commonLocation: Location, commonLocation: Location,
@ -206,12 +205,11 @@ export class IonRouterOutlet implements OnDestroy, OnInit {
// 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.
enteringView = this.stackCtrl.createView(this.activated, activatedRoute); enteringView = this.stackCtrl.createView(this.activated, activatedRoute);
enteringView.ref.changeDetectorRef.detectChanges();
// Store references to the proxy by component // Store references to the proxy by component
this.proxyMap.set(cmpRef.instance, activatedRouteProxy); this.proxyMap.set(cmpRef.instance, activatedRouteProxy);
this.currentActivatedRoute$.next({ component: cmpRef.instance, activatedRoute }); this.currentActivatedRoute$.next({ component: cmpRef.instance, activatedRoute });
this.changeDetector.markForCheck();
} }
this.activatedView = enteringView; this.activatedView = enteringView;

View File

@ -44,11 +44,7 @@ export class StackController {
getExistingView(activatedRoute: ActivatedRoute): RouteView | undefined { getExistingView(activatedRoute: ActivatedRoute): RouteView | undefined {
const activatedUrlKey = getUrl(this.router, activatedRoute); const activatedUrlKey = getUrl(this.router, activatedRoute);
const view = this.views.find(vw => vw.url === activatedUrlKey); return this.views.find(vw => vw.url === activatedUrlKey);
if (view) {
view.ref.changeDetectorRef.reattach();
}
return view;
} }
setActive(enteringView: RouteView): Promise<StackEvent> { setActive(enteringView: RouteView): Promise<StackEvent> {
@ -95,15 +91,15 @@ export class StackController {
} }
const views = this.insertView(enteringView, direction); const views = this.insertView(enteringView, direction);
return this.wait(async () => { return this.wait(() => {
await this.transition(enteringView, leavingView, animation, this.canGoBack(1), false); return this.transition(enteringView, leavingView, animation, this.canGoBack(1), false)
await cleanupAsync(enteringView, views, viewsSnapshot, this.location); .then(() => cleanupAsync(enteringView, views, viewsSnapshot, this.location))
return { .then(() => ({
enteringView, enteringView,
direction, direction,
animation, animation,
tabSwitch tabSwitch
}; }));
}); });
} }
@ -138,13 +134,12 @@ export class StackController {
}); });
} }
async startBackTransition() { startBackTransition() {
const leavingView = this.activeView; const leavingView = this.activeView;
if (leavingView) { if (leavingView) {
const views = this.getStack(leavingView.stackId); const views = this.getStack(leavingView.stackId);
const enteringView = views[views.length - 2]; const enteringView = views[views.length - 2];
enteringView.ref.changeDetectorRef.reattach(); return this.wait(() => {
await this.wait(() => {
return this.transition( return this.transition(
enteringView, // entering view enteringView, // entering view
leavingView, // leaving view leavingView, // leaving view
@ -154,6 +149,7 @@ export class StackController {
); );
}); });
} }
return Promise.resolve();
} }
endBackTransition(shouldComplete: boolean) { endBackTransition(shouldComplete: boolean) {
@ -189,7 +185,7 @@ export class StackController {
return this.views.slice(); return this.views.slice();
} }
private async transition( private transition(
enteringView: RouteView | undefined, enteringView: RouteView | undefined,
leavingView: RouteView | undefined, leavingView: RouteView | undefined,
direction: 'forward' | 'back' | undefined, direction: 'forward' | 'back' | undefined,
@ -198,7 +194,13 @@ export class StackController {
) { ) {
if (this.skipTransition) { if (this.skipTransition) {
this.skipTransition = false; this.skipTransition = false;
return; return Promise.resolve(false);
}
if (enteringView) {
enteringView.ref.changeDetectorRef.reattach();
}
if (leavingView) {
leavingView.ref.changeDetectorRef.detach();
} }
const enteringEl = enteringView ? enteringView.element : undefined; const enteringEl = enteringView ? enteringView.element : undefined;
const leavingEl = leavingView ? leavingView.element : undefined; const leavingEl = leavingView ? leavingView.element : undefined;
@ -209,15 +211,15 @@ export class StackController {
containerEl.appendChild(enteringEl); containerEl.appendChild(enteringEl);
} }
await containerEl.componentOnReady(); return this.zone.runOutsideAngular(() => containerEl.commit(enteringEl, leavingEl, {
await containerEl.commit(enteringEl, leavingEl, {
deepWait: true, deepWait: true,
duration: direction === undefined ? 0 : undefined, duration: direction === undefined ? 0 : undefined,
direction, direction,
showGoBack, showGoBack,
progressAnimation progressAnimation
}); }));
} }
return Promise.resolve(false);
} }
private async wait<T>(task: () => Promise<T>): Promise<T> { private async wait<T>(task: () => Promise<T>): Promise<T> {
@ -245,7 +247,6 @@ function cleanup(activeRoute: RouteView, views: RouteView[], viewsSnapshot: Rout
.forEach(destroyView); .forEach(destroyView);
views.forEach(view => { views.forEach(view => {
/** /**
* In the event that a user navigated multiple * In the event that a user navigated multiple
* times in rapid succession, we want to make sure * times in rapid succession, we want to make sure

View File

@ -123,7 +123,6 @@ describe('router-link', () => {
it('should go back with ion-button[routerLink][routerDirection=back]', async () => { it('should go back with ion-button[routerLink][routerDirection=back]', async () => {
await element(by.css('#routerLink-back')).click(); await element(by.css('#routerLink-back')).click();
await testBack();
}); });
it('should go back with a[routerLink][routerDirection=back]', async () => { it('should go back with a[routerLink][routerDirection=back]', async () => {
@ -144,8 +143,8 @@ async function testForward() {
await testLifeCycle('app-router-link', { await testLifeCycle('app-router-link', {
ionViewWillEnter: 1, ionViewWillEnter: 1,
ionViewDidEnter: 1, ionViewDidEnter: 1,
ionViewWillLeave: 1, ionViewWillLeave: 0, // missing change detection
ionViewDidLeave: 1, ionViewDidLeave: 0, // missing change detection
}); });
await testLifeCycle('app-router-link-page', { await testLifeCycle('app-router-link-page', {
ionViewWillEnter: 1, ionViewWillEnter: 1,
@ -165,6 +164,15 @@ async function testRoot() {
ionViewWillLeave: 0, ionViewWillLeave: 0,
ionViewDidLeave: 0, ionViewDidLeave: 0,
}); });
await browser.navigate().back();
await waitTime(100);
await testStack('ion-router-outlet', ['app-router-link']);
await testLifeCycle('app-router-link', {
ionViewWillEnter: 1,
ionViewDidEnter: 1,
ionViewWillLeave: 0,
ionViewDidLeave: 0,
});
} }
async function testBack() { async function testBack() {

View File

@ -796,20 +796,28 @@
} }
}, },
"@ionic/angular": { "@ionic/angular": {
"version": "4.0.0-rc.1", "version": "4.6.0",
"resolved": "https://registry.npmjs.org/@ionic/angular/-/angular-4.0.0-rc.1.tgz", "resolved": "https://registry.npmjs.org/@ionic/angular/-/angular-4.6.0.tgz",
"integrity": "sha512-BoNynQ7s+9v4D/yOg6Po33c8svL3HLrL623cmU2CeXIh8F7c4DTlyn+vE6x1ifWrlHucLc5KmMCGd5YqzsGfNw==", "integrity": "sha512-T7At4TBHqkNP9zt6nHqgIztOIDB3X/3YojNm5aya/2tlT9mJ+R0DcGBaKD+KOvKmauzIiABs0A3sxFAPZURVCQ==",
"requires": { "requires": {
"@ionic/core": "4.0.0-rc.1", "@ionic/core": "4.6.0",
"tslib": "^1.9.3" "tslib": "^1.9.3"
} }
}, },
"@ionic/core": { "@ionic/core": {
"version": "4.0.0-rc.1", "version": "4.6.0",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-4.0.0-rc.1.tgz", "resolved": "https://registry.npmjs.org/@ionic/core/-/core-4.6.0.tgz",
"integrity": "sha512-HGMjSq0hW7xVczTDib3tJ1aLi6RgE6R3spKWRiEsVvuBz3WGrLAuG6ASFic/U1k5LLG6vyJoWs4qvZ24b3dXag==", "integrity": "sha512-yE7zVnj8jQYQfFw+oliXgbpxDGYDS8SKDRLo3I0IQWGIn50nFntQVfH+FfaJ6bWexInq+86+dQLDIjCUQUX0PQ==",
"requires": { "requires": {
"ionicons": "4.5.1" "ionicons": "4.5.10-2",
"tslib": "^1.10.0"
},
"dependencies": {
"tslib": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz",
"integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ=="
}
} }
}, },
"@ngtools/webpack": { "@ngtools/webpack": {
@ -5352,9 +5360,9 @@
"dev": true "dev": true
}, },
"ionicons": { "ionicons": {
"version": "4.5.1", "version": "4.5.10-2",
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-4.5.1.tgz", "resolved": "https://registry.npmjs.org/ionicons/-/ionicons-4.5.10-2.tgz",
"integrity": "sha512-zqfkjpPKsdzzXePdE03IRw6xt7B6N3fcN/7NepyniuEWhKZLy7YpdZLegEwBmKeciXi7rIcv1O/hHJTdokUwXQ==" "integrity": "sha512-68GMJBezv9ONng8TskjYFrOnCjXzDSdES6q1C9hTJyA9hKViCqaRcDsq3J/w3OukZEq92o2pX2tRwhj+uFgc9g=="
}, },
"ip": { "ip": {
"version": "1.1.5", "version": "1.1.5",

View File

@ -8,8 +8,7 @@
"sync": "sh scripts/sync.sh", "sync": "sh scripts/sync.sh",
"build": "ng build --prod --no-progress", "build": "ng build --prod --no-progress",
"test": "ng e2e --prod", "test": "ng e2e --prod",
"lint": "ng lint", "lint": "ng lint"
"postinstall": "npm run sync"
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
@ -21,7 +20,7 @@
"@angular/platform-browser": "~7.2.1", "@angular/platform-browser": "~7.2.1",
"@angular/platform-browser-dynamic": "~7.2.1", "@angular/platform-browser-dynamic": "~7.2.1",
"@angular/router": "~7.2.1", "@angular/router": "~7.2.1",
"@ionic/angular": "^4.0.0-rc.1", "@ionic/angular": "^4.5.0",
"core-js": "^2.6.2", "core-js": "^2.6.2",
"rxjs": "~6.3.3", "rxjs": "~6.3.3",
"tslib": "^1.9.0", "tslib": "^1.9.0",

View File

@ -15,7 +15,16 @@ export class AlertComponent {
async openAlert() { async openAlert() {
const alert = await this.alertCtrl.create({ const alert = await this.alertCtrl.create({
header: 'Hello', header: 'Hello',
message: 'Some text' message: 'Some text',
buttons: [
{
role: 'cancel',
text: 'Cancel',
handler: () => {
NgZone.assertInAngularZone();
}
}
]
}); });
await alert.present(); await alert.present();
} }

View File

@ -6,6 +6,7 @@
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
<ion-content> <ion-content>
<p>Change Detections: <span id="counter">{{counter()}}</span></p>
<ion-list> <ion-list>
<ion-item> <ion-item>
@ -89,7 +90,7 @@
<ion-range [(ngModel)]="range"></ion-range> <ion-range [(ngModel)]="range"></ion-range>
<ion-note slot="end" id="range-note">{{range}}</ion-note> <ion-note slot="end" id="range-note">{{range}}</ion-note>
</ion-item> </ion-item>
<ion-item color="dark"> <ion-item color="dark">
<ion-label>Range Mirror</ion-label> <ion-label>Range Mirror</ion-label>
<ion-range [(ngModel)]="range"></ion-range> <ion-range [(ngModel)]="range"></ion-range>

View File

@ -12,6 +12,7 @@ export class InputsComponent {
toggle = true; toggle = true;
select = 'nes'; select = 'nes';
range = 10; range = 10;
changes = 0;
setValues() { setValues() {
console.log('set values'); console.log('set values');
@ -32,4 +33,8 @@ export class InputsComponent {
this.select = undefined; this.select = undefined;
this.range = undefined; this.range = undefined;
} }
counter() {
this.changes++;
return Math.floor(this.changes / 2);
}
} }

View File

@ -11,6 +11,7 @@
<p>ionViewDidEnter: <span id="ionViewDidEnter">{{didEnter}}</span></p> <p>ionViewDidEnter: <span id="ionViewDidEnter">{{didEnter}}</span></p>
<p>ionViewWillLeave: <span id="ionViewWillLeave">{{willLeave}}</span></p> <p>ionViewWillLeave: <span id="ionViewWillLeave">{{willLeave}}</span></p>
<p>ionViewDidLeave: <span id="ionViewDidLeave">{{didLeave}}</span></p> <p>ionViewDidLeave: <span id="ionViewDidLeave">{{didLeave}}</span></p>
<p>Change Detections: <span id="counter">{{counter()}}</span></p>
<p> <p>
<ion-button routerLink="/router-link-page" expand="block" color="dark" id="routerLink">ion-button[routerLink]</ion-button> <ion-button routerLink="/router-link-page" expand="block" color="dark" id="routerLink">ion-button[routerLink]</ion-button>
@ -26,7 +27,7 @@
<p><button (click)="navigateForward()" id="button-forward">navigateForward</button></p> <p><button (click)="navigateForward()" id="button-forward">navigateForward</button></p>
<p><button (click)="navigateRoot()" id="button-root">navigateForward</button></p> <p><button (click)="navigateRoot()" id="button-root">navigateForward</button></p>
<p><button (click)="navigateBack()" id="button-back">navigateBack</button></p> <p><button (click)="navigateBack()" id="button-back">navigateBack</button></p>
<p><button id="queryParamsFragment" routerLink="/router-link-page2/MyPageID==" [queryParams]="{ token: 'A&=#Y' }" fragment="myDiv1">Query Params and Fragment</button></p> <p><button id="queryParamsFragment" routerLink="/router-link-page2/MyPageID==" [queryParams]="{ token: 'A&=#Y' }" fragment="myDiv1">Query Params and Fragment</button></p>
</ion-content> </ion-content>

View File

@ -13,6 +13,7 @@ export class RouterLinkComponent implements OnInit {
didEnter = 0; didEnter = 0;
willLeave = 0; willLeave = 0;
didLeave = 0; didLeave = 0;
changes = 0;
constructor( constructor(
private navCtrl: NavController, private navCtrl: NavController,
@ -35,6 +36,11 @@ export class RouterLinkComponent implements OnInit {
this.navCtrl.navigateRoot('/router-link-page'); this.navCtrl.navigateRoot('/router-link-page');
} }
counter() {
this.changes++;
return Math.floor(this.changes / 2);
}
ngOnInit() { ngOnInit() {
NgZone.assertInAngularZone(); NgZone.assertInAngularZone();
this.onInit++; this.onInit++;

View File

@ -2,7 +2,7 @@ import { Component, ComponentInterface, Element, Event, EventEmitter, Listen, Me
import { getIonMode } from '../../global/ionic-global'; import { getIonMode } from '../../global/ionic-global';
import { ActionSheetButton, Animation, AnimationBuilder, CssClassMap, OverlayEventDetail, OverlayInterface } from '../../interface'; import { ActionSheetButton, Animation, AnimationBuilder, CssClassMap, OverlayEventDetail, OverlayInterface } from '../../interface';
import { BACKDROP, dismiss, eventMethod, isCancel, present } from '../../utils/overlays'; import { BACKDROP, dismiss, eventMethod, isCancel, present, safeCall } from '../../utils/overlays';
import { getClassMap } from '../../utils/theme'; import { getClassMap } from '../../utils/theme';
import { iosEnterAnimation } from './animations/ios.enter'; import { iosEnterAnimation } from './animations/ios.enter';
@ -169,17 +169,13 @@ export class ActionSheet implements ComponentInterface, OverlayInterface {
} }
private async callButtonHandler(button: ActionSheetButton | undefined) { private async callButtonHandler(button: ActionSheetButton | undefined) {
if (button && button.handler) { if (button) {
// a handler has been provided, execute it // a handler has been provided, execute it
// pass the handler the values from the inputs // pass the handler the values from the inputs
try { const rtn = await safeCall(button.handler);
const rtn = await button.handler(); if (rtn === false) {
if (rtn === false) { // if the return value of the handler is false then do not dismiss
// if the return value of the handler is false then do not dismiss return false;
return false;
}
} catch (e) {
console.error(e);
} }
} }
return true; return true;

View File

@ -2,7 +2,7 @@ import { Component, ComponentInterface, Element, Event, EventEmitter, Listen, Me
import { getIonMode } from '../../global/ionic-global'; import { getIonMode } from '../../global/ionic-global';
import { AlertButton, AlertInput, Animation, AnimationBuilder, CssClassMap, OverlayEventDetail, OverlayInterface } from '../../interface'; import { AlertButton, AlertInput, Animation, AnimationBuilder, CssClassMap, OverlayEventDetail, OverlayInterface } from '../../interface';
import { BACKDROP, dismiss, eventMethod, isCancel, present } from '../../utils/overlays'; import { BACKDROP, dismiss, eventMethod, isCancel, present, safeCall } from '../../utils/overlays';
import { sanitizeDOMString } from '../../utils/sanitization'; import { sanitizeDOMString } from '../../utils/sanitization';
import { getClassMap } from '../../utils/theme'; import { getClassMap } from '../../utils/theme';
@ -223,17 +223,13 @@ export class Alert implements ComponentInterface, OverlayInterface {
input.checked = input === selectedInput; input.checked = input === selectedInput;
} }
this.activeId = selectedInput.id; this.activeId = selectedInput.id;
if (selectedInput.handler) { safeCall(selectedInput.handler, selectedInput)
selectedInput.handler(selectedInput);
}
this.el.forceUpdate(); this.el.forceUpdate();
} }
private cbClick(selectedInput: AlertInput) { private cbClick(selectedInput: AlertInput) {
selectedInput.checked = !selectedInput.checked; selectedInput.checked = !selectedInput.checked;
if (selectedInput.handler) { safeCall(selectedInput.handler, selectedInput);
selectedInput.handler(selectedInput);
}
this.el.forceUpdate(); this.el.forceUpdate();
} }
@ -254,7 +250,7 @@ export class Alert implements ComponentInterface, OverlayInterface {
if (button && button.handler) { if (button && button.handler) {
// a handler has been provided, execute it // a handler has been provided, execute it
// pass the handler the values from the inputs // pass the handler the values from the inputs
const returnData = button.handler(data); const returnData = safeCall(button.handler, data);
if (returnData === false) { if (returnData === false) {
// if the return value of the handler is false then do not dismiss // if the return value of the handler is false then do not dismiss
return false; return false;

View File

@ -2,7 +2,7 @@ import { Component, ComponentInterface, Element, Event, EventEmitter, Listen, Me
import { getIonMode } from '../../global/ionic-global'; import { getIonMode } from '../../global/ionic-global';
import { Animation, AnimationBuilder, CssClassMap, OverlayEventDetail, OverlayInterface, PickerButton, PickerColumn } from '../../interface'; import { Animation, AnimationBuilder, CssClassMap, OverlayEventDetail, OverlayInterface, PickerButton, PickerColumn } from '../../interface';
import { dismiss, eventMethod, present } from '../../utils/overlays'; import { dismiss, eventMethod, present, safeCall } from '../../utils/overlays';
import { getClassMap } from '../../utils/theme'; import { getClassMap } from '../../utils/theme';
import { iosEnterAnimation } from './animations/ios.enter'; import { iosEnterAnimation } from './animations/ios.enter';
@ -175,17 +175,9 @@ export class Picker implements ComponentInterface, OverlayInterface {
// } // }
// keep the time of the most recent button click // keep the time of the most recent button click
let shouldDismiss = true; // a handler has been provided, execute it
// pass the handler the values from the inputs
if (button.handler) { const shouldDismiss = safeCall(button.handler, this.getSelected()) !== false;
// a handler has been provided, execute it
// pass the handler the values from the inputs
if (button.handler(this.getSelected()) === false) {
// if the return value of the handler is false then do not dismiss
shouldDismiss = false;
}
}
if (shouldDismiss) { if (shouldDismiss) {
return this.dismiss(); return this.dismiss();
} }

View File

@ -2,6 +2,7 @@ import { Component, ComponentInterface, Listen, Prop, h } from '@stencil/core';
import { getIonMode } from '../../global/ionic-global'; import { getIonMode } from '../../global/ionic-global';
import { SelectPopoverOption } from '../../interface'; import { SelectPopoverOption } from '../../interface';
import { safeCall } from '../../utils/overlays';
/** /**
* @internal * @internal
@ -28,8 +29,8 @@ export class SelectPopover implements ComponentInterface {
@Listen('ionSelect') @Listen('ionSelect')
onSelect(ev: any) { onSelect(ev: any) {
const option = this.options.find(o => o.value === ev.target.value); const option = this.options.find(o => o.value === ev.target.value);
if (option && option.handler) { if (option) {
option.handler(); safeCall(option.handler);
} }
} }

View File

@ -2,7 +2,7 @@ import { Component, ComponentInterface, Element, Event, EventEmitter, Method, Pr
import { getIonMode } from '../../global/ionic-global'; import { getIonMode } from '../../global/ionic-global';
import { Animation, AnimationBuilder, Color, CssClassMap, OverlayEventDetail, OverlayInterface, ToastButton } from '../../interface'; import { Animation, AnimationBuilder, Color, CssClassMap, OverlayEventDetail, OverlayInterface, ToastButton } from '../../interface';
import { dismiss, eventMethod, isCancel, present } from '../../utils/overlays'; import { dismiss, eventMethod, isCancel, present, safeCall } from '../../utils/overlays';
import { sanitizeDOMString } from '../../utils/sanitization'; import { sanitizeDOMString } from '../../utils/sanitization';
import { createColorClasses, getClassMap } from '../../utils/theme'; import { createColorClasses, getClassMap } from '../../utils/theme';
@ -212,7 +212,7 @@ export class Toast implements ComponentInterface, OverlayInterface {
// a handler has been provided, execute it // a handler has been provided, execute it
// pass the handler the values from the inputs // pass the handler the values from the inputs
try { try {
const rtn = await button.handler(); const rtn = await safeCall(button.handler);
if (rtn === false) { if (rtn === false) {
// if the return value of the handler is false then do not dismiss // if the return value of the handler is false then do not dismiss
return false; return false;

View File

@ -177,6 +177,7 @@ export interface IonicConfig {
persistConfig?: boolean; persistConfig?: boolean;
_forceStatusbarPadding?: boolean; _forceStatusbarPadding?: boolean;
_testing?: boolean; _testing?: boolean;
_zoneGate?: (h: () => any) => any;
} }
export function setupConfig(config: IonicConfig) { export function setupConfig(config: IonicConfig) {

View File

@ -203,15 +203,6 @@ const overlayAnimation = async (
return hasCompleted; return hasCompleted;
}; };
export const autoFocus = (containerEl: HTMLElement): HTMLElement | undefined => {
const focusableEls = containerEl.querySelectorAll('a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), [tabindex="0"]');
if (focusableEls.length > 0) {
const el = focusableEls[0] as HTMLInputElement;
el.focus();
return el;
}
return undefined;
};
export const eventMethod = <T>(element: HTMLElement, eventName: string): Promise<T> => { export const eventMethod = <T>(element: HTMLElement, eventName: string): Promise<T> => {
let resolve: (detail: T) => void; let resolve: (detail: T) => void;
@ -244,4 +235,20 @@ const isDescendant = (parent: HTMLElement, child: HTMLElement | null) => {
return false; return false;
}; };
const defaultGate = (h: any) => h();
export const safeCall = (handler: any, arg?: any) => {
if (typeof handler === 'function') {
const jmp = config.get('_zoneGate', defaultGate);
return jmp(() => {
try {
return handler(arg);
} catch (e) {
console.error(e);
}
});
}
return undefined;
};
export const BACKDROP = 'backdrop'; export const BACKDROP = 'backdrop';