39 lines
1.3 KiB
TypeScript
39 lines
1.3 KiB
TypeScript
import { type Accessor, createSignal } from "solid-js";
|
|
|
|
export interface CreateControllableSignalOptions<T> {
|
|
/** Returns the controlled value, or undefined if uncontrolled. */
|
|
value: Accessor<T | undefined>;
|
|
/** Default value used when uncontrolled. Only read once at creation time — not reactive after mount. */
|
|
defaultValue: Accessor<T>;
|
|
/** Called whenever the value changes (both modes). */
|
|
onChange?: ((value: T) => void) | undefined;
|
|
}
|
|
|
|
/**
|
|
* Handles controlled vs uncontrolled state for any stateful component.
|
|
* When `value()` is not undefined, the component is controlled — the external
|
|
* value is the source of truth. Otherwise, an internal signal manages state.
|
|
*/
|
|
export function createControllableSignal<T>(
|
|
options: CreateControllableSignalOptions<T>,
|
|
): [Accessor<T>, (value: T) => void] {
|
|
const [internalValue, setInternalValue] = createSignal<T>(options.defaultValue());
|
|
|
|
const get: Accessor<T> = () => {
|
|
const controlled = options.value();
|
|
if (controlled !== undefined) return controlled;
|
|
return internalValue();
|
|
};
|
|
|
|
const set = (value: T) => {
|
|
const isControlled = options.value() !== undefined;
|
|
if (!isControlled) {
|
|
// Use functional form so SolidJS does not interpret T-as-function as an updater.
|
|
setInternalValue(() => value);
|
|
}
|
|
options.onChange?.(value);
|
|
};
|
|
|
|
return [get, set];
|
|
}
|