This commit is contained in:
Michael Telatynski
2025-06-24 09:50:02 +01:00
parent 9763807c42
commit 20098f132b
6 changed files with 103 additions and 14 deletions
@@ -26,7 +26,7 @@ export type DialogHandle<M> = {
/**
* Promise that resolves when the dialog is finished.
*/
finished: Promise<{ ok: boolean; model: M }>;
finished: Promise<{ ok: boolean; model: M | null }>;
/**
* Method to close the dialog.
*/
@@ -46,7 +46,7 @@ export type DialogProps<M> = {
/**
* Cancel the dialog programmatically.
*/
cancel(): void;
onCancel(): void;
};
/**
@@ -60,9 +60,9 @@ export interface DialogApiExtension {
* @param dialog - The body component to render in the dialog. This component should accept props of type `P`.
* @param props - Additional props to pass to the body
*/
openDialog<M, P>(
openDialog<M, P extends object>(
initialOptions: DialogOptions,
dialog: ComponentType<P & DialogProps<M>>,
props?: P,
props: P,
): DialogHandle<M>;
}
@@ -0,0 +1,58 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { expect, test, vitest } from "vitest";
import { Watchable } from "./watchable";
test("initial value is set correctly", () => {
const watchable = new Watchable(42);
expect(watchable.value).toBe(42);
});
test("value can be updated", () => {
const watchable = new Watchable(100);
watchable.value = 200;
expect(watchable.value).toBe(200);
});
test("watchers are notified on value change", () => {
const watchable = new Watchable(1);
const listener = vitest.fn();
watchable.watch(listener);
watchable.value = 2; // This should trigger the listener
expect(listener).toHaveBeenCalledExactlyOnceWith(2);
watchable.unwatch(listener); // Clean up after the test
});
test("watchers are not notified if value does not change", () => {
const watchable = new Watchable(10);
const listener = vitest.fn();
watchable.watch(listener);
watchable.value = 10; // This should not trigger the listener
expect(listener).not.toHaveBeenCalled();
watchable.unwatch(listener); // Clean up after the test
});
test("when value is an object, shallow comparison works", () => {
const watchable = new Watchable({ a: 1, b: 2 });
const listener = vitest.fn();
watchable.watch(listener);
// Update with a new object that has the same properties
watchable.value = { a: 3, b: 2 }; // This should trigger the listener
expect(listener).toHaveBeenCalledExactlyOnceWith({ a: 3, b: 2 });
listener.mockClear();
watchable.value = { a: 3, b: 2 }; // This should not trigger the listener again
expect(listener).not.toHaveBeenCalled();
watchable.unwatch(listener); // Clean up after the test
});
@@ -7,8 +7,21 @@ Please see LICENSE files in the repository root for full details.
type WatchFn<T> = (value: T) => void;
function shallowCompare<T extends object>(obj1: T, obj2: T): boolean {
return (
Object.keys(obj1).length === Object.keys(obj2).length &&
Object.keys(obj1).every((key) => obj1[key as keyof T] === obj2[key as keyof T])
);
}
function isObject(value: unknown): value is object {
return value !== null && typeof value === "object";
}
/**
* Utility class to wrap a value and allow listeners to be notified when the value changes.
* If T is an object, it will use a shallow comparison to determine if the value has changed.
* @public
*/
export class Watchable<T> {
private readonly listeners = new Set<WatchFn<T>>();
@@ -20,11 +33,17 @@ export class Watchable<T> {
}
public set value(value: T) {
if (this.currentValue !== value) {
this.currentValue = value;
for (const listener of this.listeners) {
listener(this.currentValue);
}
// If the value hasn't changed, do nothing.
if (value === this.currentValue) {
return;
}
if (isObject(value) && isObject(this.currentValue) && shallowCompare(this.currentValue as object, value)) {
return;
}
this.currentValue = value;
for (const listener of this.listeners) {
listener(this.currentValue);
}
}
@@ -17,3 +17,4 @@ export type * from "./api/auth";
export type * from "./api/dialog";
export type * from "./api/profile";
export type * from "./api/navigation";
export * from "./api/watchable";