From 2166d1e4150200959bf2cecada326eb16102ba2b Mon Sep 17 00:00:00 2001 From: Panayot Cankov Date: Thu, 25 Jan 2018 11:38:27 +0200 Subject: [PATCH] feat(observable): Implement observable .once (#5309) Node has a handy one-liner method on its EventEmitter called "once". It is similar to "on", but fires a single time and automatically unsubscribes. --- .../data/observable/observable.d.ts | 8 +++ .../data/observable/observable.ts | 9 +++ unit-tests/observable/observable.ts | 61 +++++++++++++++++++ 3 files changed, 78 insertions(+) create mode 100644 unit-tests/observable/observable.ts diff --git a/tns-core-modules/data/observable/observable.d.ts b/tns-core-modules/data/observable/observable.d.ts index f2cdf9150..ec147217f 100644 --- a/tns-core-modules/data/observable/observable.d.ts +++ b/tns-core-modules/data/observable/observable.d.ts @@ -95,6 +95,14 @@ export class Observable { */ on(event: "propertyChange", callback: (data: EventData) => void, thisArg?: any); + /** + * Adds one-time listener function for the event named `event`. + * @param event Name of the event to attach to. + * @param callback A function to be called when the specified event is raised. + * @param thisArg An optional parameter which when set will be used as "this" in callback method call. + */ + once(event: string, callback: (data: EventData) => void, thisArg?: any); + /** * Shortcut alias to the removeEventListener method. */ diff --git a/tns-core-modules/data/observable/observable.ts b/tns-core-modules/data/observable/observable.ts index 29f0f4a16..715b30f27 100644 --- a/tns-core-modules/data/observable/observable.ts +++ b/tns-core-modules/data/observable/observable.ts @@ -3,6 +3,7 @@ interface ListenerEntry { callback: (data: EventData) => void; thisArg: any; + once?: true; } let _wrappedIndex = 0; @@ -56,6 +57,11 @@ export class Observable implements ObservableDefinition { this.addEventListener(eventNames, callback, thisArg); } + public once(event: string, callback: (data: EventData) => void, thisArg?: any) { + const list = this._getEventList(event, true); + list.push({ callback, thisArg, once: true }); + } + public off(eventNames: string, callback?: any, thisArg?: any) { this.removeEventListener(eventNames, callback, thisArg); } @@ -120,6 +126,9 @@ export class Observable implements ObservableDefinition { for (let i = observers.length - 1; i >= 0; i--) { let entry = observers[i]; + if (entry.once) { + observers.splice(i, 1); + } if (entry.thisArg) { entry.callback.apply(entry.thisArg, [data]); } else { diff --git a/unit-tests/observable/observable.ts b/unit-tests/observable/observable.ts new file mode 100644 index 000000000..3fc2338c4 --- /dev/null +++ b/unit-tests/observable/observable.ts @@ -0,0 +1,61 @@ +import { Observable } from "tns-core-modules/data/observable"; +import { assert } from "chai"; + +describe("observable", () => { + describe("once", () => { + + let observable: Observable; + let handler: () => void; + let callCount: number = 0; + + beforeEach("create handlers", () => { + handler = function() { + callCount++; + } + observable = new Observable(); + observable.once("test", handler); + }); + afterEach("reset handlers", () => { + callCount = 0; + handler = null; + observable = null; + }); + + function notify() { + observable.notify({ eventName: "test", object: observable }); + } + function notifyWrong() { + observable.notify({ eventName: "test2", object: observable }); + } + + it("fires just once", () => { + notify(); + notify(); + assert.equal(callCount, 1, "Expected the handler to be called exactly once"); + }); + it("does not fire for other events", () => { + notifyWrong(); + assert.equal(callCount, 0, "Expected the handler to not be called, when other events fire"); + }); + }); + + describe("once", () => { + it("fire once when fired recursively", () => { + const observable = new Observable(); + let callCount1 = 0; + let callCount2 = 0; + const handler2 = function () { + callCount2++; + } + const handler1 = function () { + callCount1++; + observable.once("test", handler2); + observable.notify({ eventName: "test", object: observable }); + } + observable.once("test", handler1); + observable.notify({ eventName: "test", object: observable }); + assert.equal(callCount1, 1, "Expected the first handler to unsubscribe before being fired and to notify just once"); + assert.equal(callCount2, 1, "Expected the second handler to be fired once when recursively notified by the first handler"); + }); + }); +}); \ No newline at end of file