Skip to content

Commit 652b8a7

Browse files
committed
Add onFirstWatch and onLastWatch to watchable
So that we can create custom watchable objects that can add/remove event listeners as necessary.
1 parent 29ad0f1 commit 652b8a7

File tree

2 files changed

+67
-2
lines changed

2 files changed

+67
-2
lines changed

packages/element-web-module-api/src/api/watchable.test.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
55
Please see LICENSE files in the repository root for full details.
66
*/
77

8-
import { expect, test, vitest } from "vitest";
8+
import { expect, test, vi, vitest } from "vitest";
99

1010
import { Watchable } from "./watchable";
1111

@@ -56,3 +56,44 @@ test("when value is an object, shallow comparison works", () => {
5656

5757
watchable.unwatch(listener); // Clean up after the test
5858
});
59+
60+
test("onFirstWatch and onLastWatch are called when appropriate", () => {
61+
const onFirstWatch = vi.fn();
62+
const onLastWatch = vi.fn();
63+
class CustomWatchable extends Watchable<number> {
64+
protected onFirstWatch(): void {
65+
onFirstWatch();
66+
}
67+
protected onLastWatch(): void {
68+
onLastWatch();
69+
}
70+
}
71+
72+
const watchable = new CustomWatchable(10);
73+
// No listeners yet, so expect no calls
74+
expect(onFirstWatch).not.toHaveBeenCalled();
75+
expect(onLastWatch).not.toHaveBeenCalled();
76+
77+
// Let's say that we have three listeners
78+
const listeners = [vi.fn(), vi.fn(), vi.fn()];
79+
80+
// Let's add all of them via watch
81+
for (const listener of listeners) {
82+
watchable.watch(listener);
83+
}
84+
85+
// Only expect onFirstWatch() to have been called once
86+
expect(onFirstWatch).toHaveBeenCalledOnce();
87+
88+
// Let's remove all the listeners
89+
for (const listener of listeners) {
90+
watchable.unwatch(listener);
91+
}
92+
93+
// Only expect onLastWatch to have been called once
94+
expect(onLastWatch).toHaveBeenCalledOnce();
95+
96+
// Should call onFirstWatch again once we have more listeners
97+
watchable.watch(vi.fn());
98+
expect(onFirstWatch).toHaveBeenCalledTimes(2);
99+
});

packages/element-web-module-api/src/api/watchable.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ export class Watchable<T> {
3030

3131
public constructor(private currentValue: T) {}
3232

33+
/**
34+
* The value stored in this watchable.
35+
* Warning: Could potentially return stale data if you haven't called {@link Watchable#watch}.
36+
*/
3337
public get value(): T {
3438
return this.currentValue;
3539
}
@@ -50,12 +54,32 @@ export class Watchable<T> {
5054
}
5155

5256
public watch(listener: (value: T) => void): void {
57+
// Call onFirstWatch if there was no listener before.
58+
if (this.listeners.size === 0) {
59+
this.onFirstWatch();
60+
}
5361
this.listeners.add(listener);
5462
}
5563

5664
public unwatch(listener: (value: T) => void): void {
57-
this.listeners.delete(listener);
65+
const hasDeleted = this.listeners.delete(listener);
66+
// Call onLastWatch if every listener has been removed.
67+
if (hasDeleted && this.listeners.size === 0) {
68+
this.onLastWatch();
69+
}
5870
}
71+
72+
/**
73+
* This is called when the number of listeners go from zero to one.
74+
* Could be used to add external event listeners.
75+
*/
76+
protected onFirstWatch(): void {}
77+
78+
/**
79+
* This is called when the number of listeners go from one to zero.
80+
* Could be used to remove external event listeners.
81+
*/
82+
protected onLastWatch(): void {}
5983
}
6084

6185
/**

0 commit comments

Comments
 (0)