114 lines
4.1 KiB
TypeScript
114 lines
4.1 KiB
TypeScript
import { reactive, toValue } from 'vue';
|
|
import type { UnwrapRef } from 'vue';
|
|
import { isFunction } from '@robonen/stdlib';
|
|
|
|
/**
|
|
* The reactive object produced by {@link reactivePick}: a subset of `T`
|
|
* restricted to the picked keys `K`, with each value unwrapped.
|
|
*/
|
|
export type ReactivePickReturn<T extends object, K extends keyof T>
|
|
= { [Key in K]: UnwrapRef<T[Key]> };
|
|
|
|
/**
|
|
* Predicate form: receive each `(value, key)` pair of the source object and
|
|
* return `true` to keep the key in the resulting reactive view.
|
|
*/
|
|
export type ReactivePickPredicate<T extends object>
|
|
= (value: T[keyof T], key: keyof T) => boolean;
|
|
|
|
/**
|
|
* @name reactivePick
|
|
* @category Reactivity
|
|
* @description Reactively pick a subset of keys (or keys matched by a
|
|
* predicate) from a reactive object. The result is a live `reactive` proxy:
|
|
* reads forward to the source's current value (so it tracks reassignment of
|
|
* nested refs) and writes pass straight back to the source. Unlike a
|
|
* `computed` of an object literal, no new object is allocated per recompute —
|
|
* the proxy is built once and lookups are resolved lazily on access.
|
|
*
|
|
* @param {T} obj The reactive source object (a `reactive`, `ref` value, or plain object)
|
|
* @param {...((K | K[]))} keys One or more keys (or arrays of keys) to pick
|
|
* @returns {ReactivePickReturn<T, K>} A reactive view limited to the picked keys
|
|
*
|
|
* @example
|
|
* const state = reactive({ x: 1, y: 2, z: 3 });
|
|
* const picked = reactivePick(state, 'x', 'y');
|
|
* picked.x; // 1 — stays in sync with state.x
|
|
* picked.x = 10; // writes back: state.x === 10
|
|
*
|
|
* @example
|
|
* // predicate form — keep only numeric values
|
|
* const filtered = reactivePick(state, (value) => typeof value === 'number');
|
|
*
|
|
* @since 0.0.14
|
|
*/
|
|
export function reactivePick<T extends object, K extends keyof T>(
|
|
obj: T,
|
|
...keys: Array<K | K[]>
|
|
): ReactivePickReturn<T, K>;
|
|
export function reactivePick<T extends object>(
|
|
obj: T,
|
|
predicate: ReactivePickPredicate<T>,
|
|
): ReactivePickReturn<T, keyof T>;
|
|
export function reactivePick<T extends object, K extends keyof T>(
|
|
obj: T,
|
|
...keys: Array<K | K[] | ReactivePickPredicate<T>>
|
|
): ReactivePickReturn<T, K> {
|
|
const first = keys[0];
|
|
|
|
// Predicate form: the set of kept keys is data-dependent, so it must be
|
|
// evaluated on every key-set access (`ownKeys`/`has`/`get`). Value reads
|
|
// still forward straight to the source, keeping the view reactive.
|
|
if (isFunction(first)) {
|
|
const predicate = first as ReactivePickPredicate<T>;
|
|
const keeps = (key: PropertyKey): boolean =>
|
|
key in obj && predicate(toValue((obj as Record<PropertyKey, unknown>)[key]) as T[keyof T], key as keyof T);
|
|
|
|
return reactive(new Proxy({} as ReactivePickReturn<T, K>, {
|
|
get(_, key, receiver) {
|
|
return keeps(key) ? Reflect.get(obj, key, receiver) : undefined;
|
|
},
|
|
set(_, key, value) {
|
|
return keeps(key) ? Reflect.set(obj as object, key, value) : true;
|
|
},
|
|
has(_, key) {
|
|
return keeps(key);
|
|
},
|
|
ownKeys() {
|
|
return Reflect.ownKeys(obj).filter(keeps);
|
|
},
|
|
getOwnPropertyDescriptor(_, key) {
|
|
if (!keeps(key))
|
|
return undefined;
|
|
|
|
return { enumerable: true, configurable: true };
|
|
},
|
|
})) as ReactivePickReturn<T, K>;
|
|
}
|
|
|
|
// Key form: flatten the (possibly nested) key arguments into a stable Set
|
|
// once at construction time, then resolve membership in O(1) per access.
|
|
const picked = new Set<PropertyKey>((keys as Array<K | K[]>).flat());
|
|
|
|
return reactive(new Proxy({} as ReactivePickReturn<T, K>, {
|
|
get(_, key, receiver) {
|
|
return picked.has(key) ? Reflect.get(obj, key, receiver) : undefined;
|
|
},
|
|
set(_, key, value) {
|
|
return picked.has(key) ? Reflect.set(obj as object, key, value) : true;
|
|
},
|
|
has(_, key) {
|
|
return picked.has(key) && key in obj;
|
|
},
|
|
ownKeys() {
|
|
return Reflect.ownKeys(obj).filter(key => picked.has(key));
|
|
},
|
|
getOwnPropertyDescriptor(_, key) {
|
|
if (!picked.has(key) || !(key in obj))
|
|
return undefined;
|
|
|
|
return { enumerable: true, configurable: true };
|
|
},
|
|
})) as ReactivePickReturn<T, K>;
|
|
}
|