diff --git a/apps/automated/src/data/observable-tests.ts b/apps/automated/src/data/observable-tests.ts index 32efd1f95..f640f91ed 100644 --- a/apps/automated/src/data/observable-tests.ts +++ b/apps/automated/src/data/observable-tests.ts @@ -286,8 +286,8 @@ export var test_Observable_identity = function () { // If you try to add the same callback for a given event name twice, without // distinguishing by its thisArg, the second addition will no-op. - obj.addEventListener(eventName, callback); - obj.addEventListener(eventName, callback); + obj.addEventListener(eventName, callback, false); + obj.addEventListener(eventName, callback, false); obj.set('testName', 1); TKUnit.assert(receivedCount === 1, 'Expected Observable to fire exactly once upon a property change, having passed the same callback into addEventListener() twice'); obj.removeEventListener(eventName, callback); @@ -296,9 +296,9 @@ export var test_Observable_identity = function () { // All truthy thisArgs are distinct, so we have three distinct identities here // and they should all get added. - obj.addEventListener(eventName, callback); - obj.addEventListener(eventName, callback, 1); - obj.addEventListener(eventName, callback, 2); + obj.addEventListener(eventName, callback, false); + obj.addEventListener(eventName, callback, false, 1); + obj.addEventListener(eventName, callback, false, 2); obj.set('testName', 2); TKUnit.assert(receivedCount === 3, 'Expected Observable to fire exactly three times upon a property change, having passed the same callback into addEventListener() three times, with the latter two distinguished by each having a different truthy thisArg'); obj.removeEventListener(eventName, callback); @@ -307,24 +307,24 @@ export var test_Observable_identity = function () { // If you specify thisArg when removing an event listener, it should remove // just the event listener with the corresponding thisArg. - obj.addEventListener(eventName, callback, 1); - obj.addEventListener(eventName, callback, 2); + obj.addEventListener(eventName, callback, false, 1); + obj.addEventListener(eventName, callback, false, 2); obj.set('testName', 3); TKUnit.assert(receivedCount === 2, 'Expected Observable to fire exactly three times upon a property change, having passed the same callback into addEventListener() three times, with the latter two distinguished by each having a different truthy thisArg'); - obj.removeEventListener(eventName, callback, 2); + obj.removeEventListener(eventName, callback, false, 2); TKUnit.assert(obj.hasListeners(eventName), 'Expected removeEventListener(eventName, callback, thisArg) to remove just the event listener that matched the callback and thisArg'); - obj.removeEventListener(eventName, callback, 1); + obj.removeEventListener(eventName, callback, false, 1); TKUnit.assert(!obj.hasListeners(eventName), 'Expected removeEventListener(eventName, callback, thisArg) to remove the remaining event listener that matched the callback and thisArg'); receivedCount = 0; // All falsy thisArgs are treated alike, so these all have the same identity // and only the first should get added. - obj.addEventListener(eventName, callback); - obj.addEventListener(eventName, callback, 0); obj.addEventListener(eventName, callback, false); - obj.addEventListener(eventName, callback, null); - obj.addEventListener(eventName, callback, undefined); - obj.addEventListener(eventName, callback, ''); + obj.addEventListener(eventName, callback, false, 0); + obj.addEventListener(eventName, callback, false, false); + obj.addEventListener(eventName, callback, false, null); + obj.addEventListener(eventName, callback, false, undefined); + obj.addEventListener(eventName, callback, false, ''); obj.set('testName', 4); TKUnit.assert(receivedCount === 1, 'Expected Observable to fire exactly once upon a property change, having passed the same callback into addEventListener() multiple times, each time with a different falsy (and therefore indistinct) thisArg'); obj.removeEventListener(eventName, callback); @@ -376,7 +376,10 @@ export var test_Observable_removeEventListener_SingleEvent_NoCallbackSpecified = obj.addEventListener(Observable.propertyChangeEvent, callback2); obj.set('testName', 1); - obj.removeEventListener(Observable.propertyChangeEvent); + // @ts-expect-error the callback is no longer an optional argument on + // removeEventListener(), but is still optional for off(). + TKUnit.assertThrows(() => obj.removeEventListener(Observable.propertyChangeEvent)); + obj.off(Observable.propertyChangeEvent); TKUnit.assert(!obj.hasListeners(Observable.propertyChangeEvent), 'Expected result for hasObservers is false.'); diff --git a/packages/core/accessibility/accessibility-service.android.ts b/packages/core/accessibility/accessibility-service.android.ts index 757963c35..9fd6fa083 100644 --- a/packages/core/accessibility/accessibility-service.android.ts +++ b/packages/core/accessibility/accessibility-service.android.ts @@ -111,7 +111,7 @@ function ensureStateListener(): SharedA11YObservable { touchExplorationStateChangeListener = null; if (sharedA11YObservable) { - sharedA11YObservable.removeEventListener(Observable.propertyChangeEvent); + sharedA11YObservable.off(Observable.propertyChangeEvent); sharedA11YObservable = null; } diff --git a/packages/core/accessibility/accessibility-service.ios.ts b/packages/core/accessibility/accessibility-service.ios.ts index 116986a49..3cb936a5f 100644 --- a/packages/core/accessibility/accessibility-service.ios.ts +++ b/packages/core/accessibility/accessibility-service.ios.ts @@ -56,7 +56,7 @@ function getSharedA11YObservable(): SharedA11YObservable { nativeObserver = null; if (sharedA11YObservable) { - sharedA11YObservable.removeEventListener(Observable.propertyChangeEvent); + sharedA11YObservable.off(Observable.propertyChangeEvent); sharedA11YObservable = null; } diff --git a/packages/core/data/observable/index.ts b/packages/core/data/observable/index.ts index f587454d5..7c9989413 100644 --- a/packages/core/data/observable/index.ts +++ b/packages/core/data/observable/index.ts @@ -89,6 +89,12 @@ const _globalEventHandlers: { }; } = {}; +/** + * A reusable ready-configured options object for internal use to avoid + * unnecessary allocations. + */ +const onceOption = { once: true } as const satisfies AddEventListenerOptions; + /** * Observable is used when you want to be notified when a change occurs. Use on/off methods to add/remove listener. * Please note that should you be using the `new Observable({})` constructor, it is **obsolete** since v3.0, @@ -160,7 +166,7 @@ export class Observable { * bound. */ public on(eventName: string, callback: (data: EventData) => void, thisArg?: any): void { - this.addEventListener(eventName, callback, thisArg); + this.addEventListener(eventName, callback, onceOption, thisArg); } /** @@ -175,7 +181,7 @@ export class Observable { * bound. */ public once(eventName: string, callback: (data: EventData) => void, thisArg?: any): void { - this.addEventListener(eventName, callback, thisArg, true); + this.addEventListener(eventName, callback, onceOption, thisArg); } /** @@ -188,17 +194,26 @@ export class Observable { * refine search of the correct event listener to be removed. */ public off(eventName: string, callback?: (data: EventData) => void, thisArg?: any): void { - this.removeEventListener(eventName, callback, thisArg); + this.removeEventListener(eventName, callback, false, thisArg); } /** * Adds a listener for the specified event name. - * @param eventName Name of the event to attach to. - * @param callback A function to be called when some of the specified event(s) is raised. - * @param thisArg An optional parameter which when set will be used as "this" in callback method call. + * + * @param eventName The name of the event. + * @param callback The event listener to add. Will be called when an event of + * the given name is raised. + * @param options An object or boolean indicating the EventListenerOptions. + * - If true, is interpreted as { capture: true }. + * - If false, is interpreted as { capture: false }. + * Note that 'capture' is not implemented at this time anyway, however, so the + * value is ignored. It's purely for API-compatibility with DOM Events. + * @param thisArg An optional parameter which, when set, will be bound as the + * `this` context when the callback is called. Falsy values will be not be + * bound. */ - public addEventListener(eventName: string, callback: (data: EventData) => void, thisArg?: any, once?: boolean): void { - once = once || undefined; + public addEventListener(eventName: string, callback: (data: EventData) => void, options?: AddEventListenerOptions | boolean, thisArg?: any): void { + const once = (typeof options === 'object' && options.once) || undefined; thisArg = thisArg || undefined; if (typeof eventName !== 'string') { @@ -223,20 +238,27 @@ export class Observable { } /** - * Removes listener(s) for the specified event name. - * @param eventName Name of the event to attach to. - * @param callback An optional parameter pointing to a specific listener. If not defined, all listeners for the event names will be removed. - * @param thisArg An optional parameter which when set will be used to refine search of the correct callback which will be removed as event listener. + * Removes the listener for the specified event name. + * + * @param eventName The name of the event. + * @param callback The event listener to remove. + * @param _options An object or boolean indicating the EventListenerOptions. + * - If true, is interpreted as { capture: true }. + * - If false, is interpreted as { capture: false }. + * Note that 'capture' is not implemented at this time anyway, however, so the + * value is ignored. It's purely for API-compatibility with DOM Events. + * @param thisArg An optional parameter which, when set, will be used to + * refine search of the correct event listener to be removed. */ - public removeEventListener(eventName: string, callback?: (data: EventData) => void, thisArg?: any): void { + public removeEventListener(eventName: string, callback: (data: EventData) => void, _options?: EventListenerOptions | boolean, thisArg?: any): void { thisArg = thisArg || undefined; if (typeof eventName !== 'string') { throw new TypeError('Events name(s) must be string.'); } - if (callback && typeof callback !== 'function') { - throw new TypeError('callback must be function.'); + if (typeof callback !== 'function') { + throw new TypeError('Callback must be a function.'); } const entries = this._observers[eventName]; @@ -257,8 +279,8 @@ export class Observable { * in future. * @deprecated */ - public static on(eventName: string, callback: (data: EventData) => void, thisArg?: any, once?: boolean): void { - this.addEventListener(eventName, callback, thisArg, once); + public static on(eventName: string, callback: (data: EventData) => void, thisArg?: any): void { + this.addEventListener(eventName, callback, false, thisArg); } /** @@ -267,7 +289,7 @@ export class Observable { * @deprecated */ public static once(eventName: string, callback: (data: EventData) => void, thisArg?: any): void { - this.addEventListener(eventName, callback, thisArg, true); + this.addEventListener(eventName, callback, onceOption, thisArg); } /** @@ -276,7 +298,7 @@ export class Observable { * @deprecated */ public static off(eventName: string, callback?: (data: EventData) => void, thisArg?: any): void { - this.removeEventListener(eventName, callback, thisArg); + this.removeEventListener(eventName, callback, false, thisArg); } private static innerRemoveEventListener(entries: Array, callback?: (data: EventData) => void, thisArg?: any): void { @@ -306,15 +328,15 @@ export class Observable { * in future. * @deprecated */ - public static removeEventListener(eventName: string, callback?: (data: EventData) => void, thisArg?: any): void { + public static removeEventListener(eventName: string, callback: (data: EventData) => void, _options?: EventListenerOptions | boolean, thisArg?: any): void { thisArg = thisArg || undefined; if (typeof eventName !== 'string') { throw new TypeError('Event name must be a string.'); } - if (callback && typeof callback !== 'function') { - throw new TypeError('Callback, if provided, must be function.'); + if (typeof callback !== 'function') { + throw new TypeError('Callback must be a function.'); } const eventClass = this.name === 'Observable' ? '*' : this.name; @@ -343,8 +365,8 @@ export class Observable { * in future. * @deprecated */ - public static addEventListener(eventName: string, callback: (data: EventData) => void, thisArg?: any, once?: boolean): void { - once = once || undefined; + public static addEventListener(eventName: string, callback: (data: EventData) => void, options?: AddEventListenerOptions | boolean, thisArg?: any): void { + const once = (typeof options === 'object' && options.once) || undefined; thisArg = thisArg || undefined; if (typeof eventName !== 'string') { diff --git a/packages/core/ui/core/view/view-common.ts b/packages/core/ui/core/view/view-common.ts index 6b68d42cc..a4a75b394 100644 --- a/packages/core/ui/core/view/view-common.ts +++ b/packages/core/ui/core/view/view-common.ts @@ -297,11 +297,11 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition { return this._gestureObservers[type]; } - public addEventListener(eventNames: string, callback: (data: EventData) => void, thisArg?: any) { + public addEventListener(eventName: string, callback: (data: EventData) => void, options?: AddEventListenerOptions | boolean, thisArg?: any) { thisArg = thisArg || undefined; // Normalize "ontap" -> "tap" - const normalizedName = getEventOrGestureName(eventNames); + const normalizedName = getEventOrGestureName(eventName); // Coerce "tap" -> GestureTypes.tap // Coerce "loaded" -> undefined @@ -313,14 +313,14 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition { return; } - super.addEventListener(normalizedName, callback, thisArg); + super.addEventListener(normalizedName, callback, options, thisArg); } - public removeEventListener(eventNames: string, callback?: (data: EventData) => void, thisArg?: any) { + public removeEventListener(eventName: string, callback?: (data: EventData) => void, options?: EventListenerOptions | boolean, thisArg?: any) { thisArg = thisArg || undefined; // Normalize "ontap" -> "tap" - const normalizedName = getEventOrGestureName(eventNames); + const normalizedName = getEventOrGestureName(eventName); // Coerce "tap" -> GestureTypes.tap // Coerce "loaded" -> undefined @@ -332,7 +332,7 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition { return; } - super.removeEventListener(normalizedName, callback, thisArg); + super.removeEventListener(normalizedName, callback, options, thisArg); } public onBackPressed(): boolean { diff --git a/packages/core/ui/core/weak-event-listener/index.ts b/packages/core/ui/core/weak-event-listener/index.ts index 41b5ec8a3..3788fb147 100644 --- a/packages/core/ui/core/weak-event-listener/index.ts +++ b/packages/core/ui/core/weak-event-listener/index.ts @@ -21,7 +21,7 @@ function getHandlerForEventName(eventName: string): (eventData: EventData) => vo const sourceEventMap = sourcesMap.get(source); if (!sourceEventMap) { // There is no event map for this source - it is safe to detach the listener; - source.removeEventListener(eventName, handlersForEventName.get(eventName)); + source.off(eventName, handlersForEventName.get(eventName)); return; } @@ -46,7 +46,7 @@ function getHandlerForEventName(eventName: string): (eventData: EventData) => vo if (deadPairsIndexes.length === targetHandlerPairList.length) { // There are no alive targets for this event - unsubscribe - source.removeEventListener(eventName, handlersForEventName.get(eventName)); + source.off(eventName, handlersForEventName.get(eventName)); sourceEventMap.delete(eventName); } else { for (let j = deadPairsIndexes.length - 1; j >= 0; j--) { diff --git a/packages/core/ui/scroll-view/scroll-view-common.ts b/packages/core/ui/scroll-view/scroll-view-common.ts index d281a974e..fe21b6ecd 100644 --- a/packages/core/ui/scroll-view/scroll-view-common.ts +++ b/packages/core/ui/scroll-view/scroll-view-common.ts @@ -17,10 +17,10 @@ export abstract class ScrollViewBase extends ContentView implements ScrollViewDe private _addedScrollEvent = false; - public addEventListener(arg: string, callback: (data: EventData) => void, thisArg?: any): void { + public addEventListener(arg: string, callback: (data: EventData) => void, options?: AddEventListenerOptions | boolean, thisArg?: any): void { const hasExistingScrollListeners: boolean = this.hasListeners(ScrollViewBase.scrollEvent); - super.addEventListener(arg, callback, thisArg); + super.addEventListener(arg, callback, options, thisArg); // This indicates that a scroll listener was added for first time if (!hasExistingScrollListeners && this.hasListeners(ScrollViewBase.scrollEvent)) { @@ -32,10 +32,10 @@ export abstract class ScrollViewBase extends ContentView implements ScrollViewDe } } - public removeEventListener(arg: string, callback?: (data: EventData) => void, thisArg?: any): void { + public removeEventListener(arg: string, callback?: (data: EventData) => void, options?: EventListenerOptions | boolean, thisArg?: any): void { const hasExistingScrollListeners: boolean = this.hasListeners(ScrollViewBase.scrollEvent); - super.removeEventListener(arg, callback, thisArg); + super.removeEventListener(arg, callback, options, thisArg); // This indicates that the final scroll listener was removed if (hasExistingScrollListeners && !this.hasListeners(ScrollViewBase.scrollEvent)) { diff --git a/packages/core/ui/text-base/formatted-string.ts b/packages/core/ui/text-base/formatted-string.ts index 939f938d9..2da6f0e94 100644 --- a/packages/core/ui/text-base/formatted-string.ts +++ b/packages/core/ui/text-base/formatted-string.ts @@ -14,7 +14,7 @@ export class FormattedString extends ViewBase implements FormattedStringDefiniti constructor() { super(); this._spans = new ObservableArray(); - this._spans.addEventListener(ObservableArray.changeEvent, this.onSpansCollectionChanged, this); + this._spans.addEventListener(ObservableArray.changeEvent, this.onSpansCollectionChanged, false, this); } get fontFamily(): string { diff --git a/packages/core/ui/text-base/span.ts b/packages/core/ui/text-base/span.ts index 006ba45de..fac34ddbd 100644 --- a/packages/core/ui/text-base/span.ts +++ b/packages/core/ui/text-base/span.ts @@ -109,13 +109,13 @@ export class Span extends ViewBase implements SpanDefinition { return this._tappable; } - addEventListener(arg: string, callback: (data: EventData) => void, thisArg?: any): void { - super.addEventListener(arg, callback, thisArg); + addEventListener(arg: string, callback: (data: EventData) => void, options?: AddEventListenerOptions | boolean, thisArg?: any): void { + super.addEventListener(arg, callback, options, thisArg); this._setTappable(this.hasListeners(Span.linkTapEvent)); } - removeEventListener(arg: string, callback?: (data: EventData) => void, thisArg?: any): void { - super.removeEventListener(arg, callback, thisArg); + removeEventListener(arg: string, callback?: (data: EventData) => void, options?: EventListenerOptions | boolean, thisArg?: any): void { + super.removeEventListener(arg, callback, options, thisArg); this._setTappable(this.hasListeners(Span.linkTapEvent)); }