Iterate
This commit is contained in:
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user