diff --git a/angular/src/app-initialize.ts b/angular/src/app-initialize.ts index fcdaea38bb..65cdadf37b 100644 --- a/angular/src/app-initialize.ts +++ b/angular/src/app-initialize.ts @@ -10,7 +10,14 @@ export function appInitialize(config: Config, doc: Document, zone: NgZone) { if (win) { 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 defineCustomElements(win, { @@ -23,41 +30,13 @@ export function appInitialize(config: Config, doc: Document, zone: NgZone) { }); }, ael(elm, eventName, cb, opts) { - if ((elm as any).__zone_symbol__addEventListener && skipZone(eventName)) { - (elm as any).__zone_symbol__addEventListener(eventName, cb, opts); - } else { - elm.addEventListener(eventName, cb, opts); - } + (elm as any)[aelFn](eventName, cb, opts); }, rel(elm, eventName, cb, opts) { - if ((elm as any).__zone_symbol__removeEventListener && skipZone(eventName)) { - (elm as any).__zone_symbol__removeEventListener(eventName, cb, opts); - } else { - elm.removeEventListener(eventName, cb, opts); - } + 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; -} diff --git a/angular/src/directives/control-value-accessors/value-accessor.ts b/angular/src/directives/control-value-accessors/value-accessor.ts index bca308cd7f..9ce45ecf51 100644 --- a/angular/src/directives/control-value-accessors/value-accessor.ts +++ b/angular/src/directives/control-value-accessors/value-accessor.ts @@ -42,16 +42,14 @@ export class ValueAccessor implements ControlValueAccessor { } export function setIonicClasses(element: ElementRef) { - requestAnimationFrame(() => { - const input = element.nativeElement as HTMLElement; - const classes = getClasses(input); - setClasses(input, classes); + const input = element.nativeElement as HTMLElement; + const classes = getClasses(input); + setClasses(input, classes); - const item = input.closest('ion-item'); - if (item) { - setClasses(item, classes); - } - }); + const item = input.closest('ion-item'); + if (item) { + setClasses(item, classes); + } } function getClasses(element: HTMLElement) { diff --git a/angular/src/directives/navigation/ion-router-outlet.ts b/angular/src/directives/navigation/ion-router-outlet.ts index 02701a9a0f..55cf6e3610 100644 --- a/angular/src/directives/navigation/ion-router-outlet.ts +++ b/angular/src/directives/navigation/ion-router-outlet.ts @@ -1,5 +1,5 @@ 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 { BehaviorSubject, Observable } from 'rxjs'; import { distinctUntilChanged, filter, switchMap } from 'rxjs/operators'; @@ -57,7 +57,6 @@ export class IonRouterOutlet implements OnDestroy, OnInit { private resolver: ComponentFactoryResolver, @Attribute('name') name: string, @Optional() @Attribute('tabs') tabs: string, - private changeDetector: ChangeDetectorRef, private config: Config, private navCtrl: NavController, 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 // `RouterOutlet` is inside a `ChangeDetectionStrategy.OnPush` component. enteringView = this.stackCtrl.createView(this.activated, activatedRoute); + enteringView.ref.changeDetectorRef.detectChanges(); // Store references to the proxy by component this.proxyMap.set(cmpRef.instance, activatedRouteProxy); this.currentActivatedRoute$.next({ component: cmpRef.instance, activatedRoute }); - - this.changeDetector.markForCheck(); } this.activatedView = enteringView; diff --git a/angular/src/directives/navigation/stack-controller.ts b/angular/src/directives/navigation/stack-controller.ts index 89b2778642..4bee19260b 100644 --- a/angular/src/directives/navigation/stack-controller.ts +++ b/angular/src/directives/navigation/stack-controller.ts @@ -44,11 +44,7 @@ export class StackController { getExistingView(activatedRoute: ActivatedRoute): RouteView | undefined { const activatedUrlKey = getUrl(this.router, activatedRoute); - const view = this.views.find(vw => vw.url === activatedUrlKey); - if (view) { - view.ref.changeDetectorRef.reattach(); - } - return view; + return this.views.find(vw => vw.url === activatedUrlKey); } setActive(enteringView: RouteView): Promise { @@ -95,15 +91,15 @@ export class StackController { } const views = this.insertView(enteringView, direction); - return this.wait(async () => { - await this.transition(enteringView, leavingView, animation, this.canGoBack(1), false); - await cleanupAsync(enteringView, views, viewsSnapshot, this.location); - return { - enteringView, - direction, - animation, - tabSwitch - }; + return this.wait(() => { + return this.transition(enteringView, leavingView, animation, this.canGoBack(1), false) + .then(() => cleanupAsync(enteringView, views, viewsSnapshot, this.location)) + .then(() => ({ + enteringView, + direction, + animation, + tabSwitch + })); }); } @@ -138,13 +134,12 @@ export class StackController { }); } - async startBackTransition() { + startBackTransition() { const leavingView = this.activeView; if (leavingView) { const views = this.getStack(leavingView.stackId); const enteringView = views[views.length - 2]; - enteringView.ref.changeDetectorRef.reattach(); - await this.wait(() => { + return this.wait(() => { return this.transition( enteringView, // entering view leavingView, // leaving view @@ -154,6 +149,7 @@ export class StackController { ); }); } + return Promise.resolve(); } endBackTransition(shouldComplete: boolean) { @@ -189,7 +185,7 @@ export class StackController { return this.views.slice(); } - private async transition( + private transition( enteringView: RouteView | undefined, leavingView: RouteView | undefined, direction: 'forward' | 'back' | undefined, @@ -198,7 +194,13 @@ export class StackController { ) { if (this.skipTransition) { 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 leavingEl = leavingView ? leavingView.element : undefined; @@ -209,15 +211,15 @@ export class StackController { containerEl.appendChild(enteringEl); } - await containerEl.componentOnReady(); - await containerEl.commit(enteringEl, leavingEl, { + return this.zone.runOutsideAngular(() => containerEl.commit(enteringEl, leavingEl, { deepWait: true, duration: direction === undefined ? 0 : undefined, direction, showGoBack, progressAnimation - }); + })); } + return Promise.resolve(false); } private async wait(task: () => Promise): Promise { @@ -245,7 +247,6 @@ function cleanup(activeRoute: RouteView, views: RouteView[], viewsSnapshot: Rout .forEach(destroyView); views.forEach(view => { - /** * In the event that a user navigated multiple * times in rapid succession, we want to make sure diff --git a/angular/test/test-app/e2e/src/router-link.e2e-spec.ts b/angular/test/test-app/e2e/src/router-link.e2e-spec.ts index cc9af4dacc..d609c36e33 100644 --- a/angular/test/test-app/e2e/src/router-link.e2e-spec.ts +++ b/angular/test/test-app/e2e/src/router-link.e2e-spec.ts @@ -123,7 +123,6 @@ describe('router-link', () => { it('should go back with ion-button[routerLink][routerDirection=back]', async () => { await element(by.css('#routerLink-back')).click(); - await testBack(); }); it('should go back with a[routerLink][routerDirection=back]', async () => { @@ -144,8 +143,8 @@ async function testForward() { await testLifeCycle('app-router-link', { ionViewWillEnter: 1, ionViewDidEnter: 1, - ionViewWillLeave: 1, - ionViewDidLeave: 1, + ionViewWillLeave: 0, // missing change detection + ionViewDidLeave: 0, // missing change detection }); await testLifeCycle('app-router-link-page', { ionViewWillEnter: 1, @@ -165,6 +164,15 @@ async function testRoot() { ionViewWillLeave: 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() { diff --git a/angular/test/test-app/package-lock.json b/angular/test/test-app/package-lock.json index beb5de1146..cfef891acd 100644 --- a/angular/test/test-app/package-lock.json +++ b/angular/test/test-app/package-lock.json @@ -796,20 +796,28 @@ } }, "@ionic/angular": { - "version": "4.0.0-rc.1", - "resolved": "https://registry.npmjs.org/@ionic/angular/-/angular-4.0.0-rc.1.tgz", - "integrity": "sha512-BoNynQ7s+9v4D/yOg6Po33c8svL3HLrL623cmU2CeXIh8F7c4DTlyn+vE6x1ifWrlHucLc5KmMCGd5YqzsGfNw==", + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@ionic/angular/-/angular-4.6.0.tgz", + "integrity": "sha512-T7At4TBHqkNP9zt6nHqgIztOIDB3X/3YojNm5aya/2tlT9mJ+R0DcGBaKD+KOvKmauzIiABs0A3sxFAPZURVCQ==", "requires": { - "@ionic/core": "4.0.0-rc.1", + "@ionic/core": "4.6.0", "tslib": "^1.9.3" } }, "@ionic/core": { - "version": "4.0.0-rc.1", - "resolved": "https://registry.npmjs.org/@ionic/core/-/core-4.0.0-rc.1.tgz", - "integrity": "sha512-HGMjSq0hW7xVczTDib3tJ1aLi6RgE6R3spKWRiEsVvuBz3WGrLAuG6ASFic/U1k5LLG6vyJoWs4qvZ24b3dXag==", + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@ionic/core/-/core-4.6.0.tgz", + "integrity": "sha512-yE7zVnj8jQYQfFw+oliXgbpxDGYDS8SKDRLo3I0IQWGIn50nFntQVfH+FfaJ6bWexInq+86+dQLDIjCUQUX0PQ==", "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": { @@ -5352,9 +5360,9 @@ "dev": true }, "ionicons": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/ionicons/-/ionicons-4.5.1.tgz", - "integrity": "sha512-zqfkjpPKsdzzXePdE03IRw6xt7B6N3fcN/7NepyniuEWhKZLy7YpdZLegEwBmKeciXi7rIcv1O/hHJTdokUwXQ==" + "version": "4.5.10-2", + "resolved": "https://registry.npmjs.org/ionicons/-/ionicons-4.5.10-2.tgz", + "integrity": "sha512-68GMJBezv9ONng8TskjYFrOnCjXzDSdES6q1C9hTJyA9hKViCqaRcDsq3J/w3OukZEq92o2pX2tRwhj+uFgc9g==" }, "ip": { "version": "1.1.5", diff --git a/angular/test/test-app/package.json b/angular/test/test-app/package.json index 4da07e3fd2..32efa32802 100644 --- a/angular/test/test-app/package.json +++ b/angular/test/test-app/package.json @@ -8,8 +8,7 @@ "sync": "sh scripts/sync.sh", "build": "ng build --prod --no-progress", "test": "ng e2e --prod", - "lint": "ng lint", - "postinstall": "npm run sync" + "lint": "ng lint" }, "private": true, "dependencies": { @@ -21,7 +20,7 @@ "@angular/platform-browser": "~7.2.1", "@angular/platform-browser-dynamic": "~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", "rxjs": "~6.3.3", "tslib": "^1.9.0", diff --git a/angular/test/test-app/src/app/alert/alert.component.ts b/angular/test/test-app/src/app/alert/alert.component.ts index df9d5bb56f..8ff209ab62 100644 --- a/angular/test/test-app/src/app/alert/alert.component.ts +++ b/angular/test/test-app/src/app/alert/alert.component.ts @@ -15,7 +15,16 @@ export class AlertComponent { async openAlert() { const alert = await this.alertCtrl.create({ header: 'Hello', - message: 'Some text' + message: 'Some text', + buttons: [ + { + role: 'cancel', + text: 'Cancel', + handler: () => { + NgZone.assertInAngularZone(); + } + } + ] }); await alert.present(); } diff --git a/angular/test/test-app/src/app/inputs/inputs.component.html b/angular/test/test-app/src/app/inputs/inputs.component.html index da4de839a0..1d9da7bc23 100644 --- a/angular/test/test-app/src/app/inputs/inputs.component.html +++ b/angular/test/test-app/src/app/inputs/inputs.component.html @@ -6,6 +6,7 @@ +

Change Detections: {{counter()}}

@@ -89,7 +90,7 @@ {{range}} - + Range Mirror diff --git a/angular/test/test-app/src/app/inputs/inputs.component.ts b/angular/test/test-app/src/app/inputs/inputs.component.ts index 93f6ee32f4..78f14765a0 100644 --- a/angular/test/test-app/src/app/inputs/inputs.component.ts +++ b/angular/test/test-app/src/app/inputs/inputs.component.ts @@ -12,6 +12,7 @@ export class InputsComponent { toggle = true; select = 'nes'; range = 10; + changes = 0; setValues() { console.log('set values'); @@ -32,4 +33,8 @@ export class InputsComponent { this.select = undefined; this.range = undefined; } + counter() { + this.changes++; + return Math.floor(this.changes / 2); + } } diff --git a/angular/test/test-app/src/app/router-link/router-link.component.html b/angular/test/test-app/src/app/router-link/router-link.component.html index 5ac6b632d3..ea6b97eb44 100644 --- a/angular/test/test-app/src/app/router-link/router-link.component.html +++ b/angular/test/test-app/src/app/router-link/router-link.component.html @@ -11,6 +11,7 @@

ionViewDidEnter: {{didEnter}}

ionViewWillLeave: {{willLeave}}

ionViewDidLeave: {{didLeave}}

+

Change Detections: {{counter()}}

ion-button[routerLink] @@ -26,7 +27,7 @@

- +

- +
diff --git a/angular/test/test-app/src/app/router-link/router-link.component.ts b/angular/test/test-app/src/app/router-link/router-link.component.ts index 4d17e02a37..6c7d94adcf 100644 --- a/angular/test/test-app/src/app/router-link/router-link.component.ts +++ b/angular/test/test-app/src/app/router-link/router-link.component.ts @@ -13,6 +13,7 @@ export class RouterLinkComponent implements OnInit { didEnter = 0; willLeave = 0; didLeave = 0; + changes = 0; constructor( private navCtrl: NavController, @@ -35,6 +36,11 @@ export class RouterLinkComponent implements OnInit { this.navCtrl.navigateRoot('/router-link-page'); } + counter() { + this.changes++; + return Math.floor(this.changes / 2); + } + ngOnInit() { NgZone.assertInAngularZone(); this.onInit++; diff --git a/core/src/components/action-sheet/action-sheet.tsx b/core/src/components/action-sheet/action-sheet.tsx index 0b567c9f81..403c74cdd3 100644 --- a/core/src/components/action-sheet/action-sheet.tsx +++ b/core/src/components/action-sheet/action-sheet.tsx @@ -2,7 +2,7 @@ import { Component, ComponentInterface, Element, Event, EventEmitter, Listen, Me import { getIonMode } from '../../global/ionic-global'; 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 { iosEnterAnimation } from './animations/ios.enter'; @@ -169,17 +169,13 @@ export class ActionSheet implements ComponentInterface, OverlayInterface { } private async callButtonHandler(button: ActionSheetButton | undefined) { - if (button && button.handler) { + if (button) { // a handler has been provided, execute it // pass the handler the values from the inputs - try { - const rtn = await button.handler(); - if (rtn === false) { - // if the return value of the handler is false then do not dismiss - return false; - } - } catch (e) { - console.error(e); + const rtn = await safeCall(button.handler); + if (rtn === false) { + // if the return value of the handler is false then do not dismiss + return false; } } return true; diff --git a/core/src/components/alert/alert.tsx b/core/src/components/alert/alert.tsx index f5d306ff35..01980f32f7 100644 --- a/core/src/components/alert/alert.tsx +++ b/core/src/components/alert/alert.tsx @@ -2,7 +2,7 @@ import { Component, ComponentInterface, Element, Event, EventEmitter, Listen, Me import { getIonMode } from '../../global/ionic-global'; 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 { getClassMap } from '../../utils/theme'; @@ -223,17 +223,13 @@ export class Alert implements ComponentInterface, OverlayInterface { input.checked = input === selectedInput; } this.activeId = selectedInput.id; - if (selectedInput.handler) { - selectedInput.handler(selectedInput); - } + safeCall(selectedInput.handler, selectedInput) this.el.forceUpdate(); } private cbClick(selectedInput: AlertInput) { selectedInput.checked = !selectedInput.checked; - if (selectedInput.handler) { - selectedInput.handler(selectedInput); - } + safeCall(selectedInput.handler, selectedInput); this.el.forceUpdate(); } @@ -254,7 +250,7 @@ export class Alert implements ComponentInterface, OverlayInterface { if (button && button.handler) { // a handler has been provided, execute it // pass the handler the values from the inputs - const returnData = button.handler(data); + const returnData = safeCall(button.handler, data); if (returnData === false) { // if the return value of the handler is false then do not dismiss return false; diff --git a/core/src/components/picker/picker.tsx b/core/src/components/picker/picker.tsx index dd72851c34..34fa951701 100644 --- a/core/src/components/picker/picker.tsx +++ b/core/src/components/picker/picker.tsx @@ -2,7 +2,7 @@ import { Component, ComponentInterface, Element, Event, EventEmitter, Listen, Me import { getIonMode } from '../../global/ionic-global'; 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 { iosEnterAnimation } from './animations/ios.enter'; @@ -175,17 +175,9 @@ export class Picker implements ComponentInterface, OverlayInterface { // } // keep the time of the most recent button click - let shouldDismiss = true; - - if (button.handler) { - // 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; - } - } - + // a handler has been provided, execute it + // pass the handler the values from the inputs + const shouldDismiss = safeCall(button.handler, this.getSelected()) !== false; if (shouldDismiss) { return this.dismiss(); } diff --git a/core/src/components/select-popover/select-popover.tsx b/core/src/components/select-popover/select-popover.tsx index 51b6b2d554..0bfc9fa196 100644 --- a/core/src/components/select-popover/select-popover.tsx +++ b/core/src/components/select-popover/select-popover.tsx @@ -2,6 +2,7 @@ import { Component, ComponentInterface, Listen, Prop, h } from '@stencil/core'; import { getIonMode } from '../../global/ionic-global'; import { SelectPopoverOption } from '../../interface'; +import { safeCall } from '../../utils/overlays'; /** * @internal @@ -28,8 +29,8 @@ export class SelectPopover implements ComponentInterface { @Listen('ionSelect') onSelect(ev: any) { const option = this.options.find(o => o.value === ev.target.value); - if (option && option.handler) { - option.handler(); + if (option) { + safeCall(option.handler); } } diff --git a/core/src/components/toast/toast.tsx b/core/src/components/toast/toast.tsx index 847a1fc238..726d2cf238 100644 --- a/core/src/components/toast/toast.tsx +++ b/core/src/components/toast/toast.tsx @@ -2,7 +2,7 @@ import { Component, ComponentInterface, Element, Event, EventEmitter, Method, Pr import { getIonMode } from '../../global/ionic-global'; 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 { createColorClasses, getClassMap } from '../../utils/theme'; @@ -212,7 +212,7 @@ export class Toast implements ComponentInterface, OverlayInterface { // a handler has been provided, execute it // pass the handler the values from the inputs try { - const rtn = await button.handler(); + const rtn = await safeCall(button.handler); if (rtn === false) { // if the return value of the handler is false then do not dismiss return false; diff --git a/core/src/utils/config.ts b/core/src/utils/config.ts index 7ab0c29f13..bfd3fcc58d 100644 --- a/core/src/utils/config.ts +++ b/core/src/utils/config.ts @@ -177,6 +177,7 @@ export interface IonicConfig { persistConfig?: boolean; _forceStatusbarPadding?: boolean; _testing?: boolean; + _zoneGate?: (h: () => any) => any; } export function setupConfig(config: IonicConfig) { diff --git a/core/src/utils/overlays.ts b/core/src/utils/overlays.ts index 5d6ca6e48b..26959ee0ae 100644 --- a/core/src/utils/overlays.ts +++ b/core/src/utils/overlays.ts @@ -203,15 +203,6 @@ const overlayAnimation = async ( 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 = (element: HTMLElement, eventName: string): Promise => { let resolve: (detail: T) => void; @@ -244,4 +235,20 @@ const isDescendant = (parent: HTMLElement, child: HTMLElement | null) => { 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';