feat(vue): expand @robonen/vue composable collection

Composables, tests, category barrels, and README for @robonen/vue.
This commit is contained in:
2026-06-08 15:51:16 +07:00
parent 9a912f7a77
commit 59e995d0b5
369 changed files with 36554 additions and 188 deletions
@@ -0,0 +1,115 @@
import { computed, toValue } from 'vue';
import type { ComputedRef, MaybeRefOrGetter } from 'vue';
import { isObject, isString } from '@robonen/stdlib';
/**
* Comparator deciding whether an array element equals the searched value.
*/
export type UseArrayIncludesComparatorFn<T, V>
= (element: T, value: V, index: number, array: T[]) => boolean;
export interface UseArrayIncludesOptions<T, V> {
/**
* Index at which to start searching (negative counts from the end, like `Array.prototype.includes`).
*
* @default 0
*/
fromIndex?: number;
/**
* Custom comparator function, or a key of `T` to compare a single property by.
*/
comparator?: UseArrayIncludesComparatorFn<T, V> | keyof T;
}
export type UseArrayIncludesReturn = ComputedRef<boolean>;
function isArrayIncludesOptions<T, V>(value: unknown): value is UseArrayIncludesOptions<T, V> {
// isObject matches PLAIN objects only, so functions/keys never reach here.
return isObject(value) && ('fromIndex' in value || 'comparator' in value);
}
/**
* @name useArrayIncludes
* @category Array
* @description Reactive `Array.prototype.includes` with an optional comparator and `fromIndex`. The source array and its items may be reactive.
*
* @param {MaybeRefOrGetter<MaybeRefOrGetter<T>[]>} list The source array (items can be reactive)
* @param {MaybeRefOrGetter<V>} value The value to search for (may be reactive)
* @param {UseArrayIncludesComparatorFn<T, V> | keyof T | UseArrayIncludesOptions<T, V>} [comparator] A comparator function, a key of `T` to compare by, or an options object with `comparator`/`fromIndex`
* @returns {UseArrayIncludesReturn} A computed boolean that is `true` when the value is found
*
* @example
* const list = ref([1, 2, 3, 4]);
* const hasThree = useArrayIncludes(list, 3); // true
*
* @example
* const list = ref([{ id: 1 }, { id: 2 }]);
* const hasTwo = useArrayIncludes(list, 2, 'id'); // compare by key
*
* @example
* const list = ref(['a', 'b', 'a']);
* const fromSecond = useArrayIncludes(list, 'a', { fromIndex: 1 }); // true
*
* @since 0.0.15
*/
export function useArrayIncludes<T, V = T>(
list: MaybeRefOrGetter<Array<MaybeRefOrGetter<T>>>,
value: MaybeRefOrGetter<V>,
comparator?: UseArrayIncludesComparatorFn<T, V>,
): UseArrayIncludesReturn;
export function useArrayIncludes<T, V = T>(
list: MaybeRefOrGetter<Array<MaybeRefOrGetter<T>>>,
value: MaybeRefOrGetter<V>,
comparator?: keyof T,
): UseArrayIncludesReturn;
export function useArrayIncludes<T, V = T>(
list: MaybeRefOrGetter<Array<MaybeRefOrGetter<T>>>,
value: MaybeRefOrGetter<V>,
options?: UseArrayIncludesOptions<T, V>,
): UseArrayIncludesReturn;
export function useArrayIncludes<T, V = T>(
list: MaybeRefOrGetter<Array<MaybeRefOrGetter<T>>>,
value: MaybeRefOrGetter<V>,
comparator?: UseArrayIncludesComparatorFn<T, V> | keyof T | UseArrayIncludesOptions<T, V>,
): UseArrayIncludesReturn {
let fromIndex = 0;
let resolved = comparator;
if (isArrayIncludesOptions<T, V>(resolved)) {
fromIndex = resolved.fromIndex ?? 0;
resolved = resolved.comparator;
}
// Resolve the comparator once instead of on every recompute.
let compare: UseArrayIncludesComparatorFn<T, V>;
if (isString(resolved) || typeof resolved === 'symbol' || typeof resolved === 'number') {
const key = resolved as keyof T;
compare = (element, searched) => element[key] === (searched as unknown);
}
else if (typeof resolved === 'function') {
compare = resolved;
}
else {
compare = (element, searched) => (element as unknown) === searched;
}
return computed(() => {
const array = toValue(list);
const searched = toValue(value);
const length = array.length;
// Resolve a negative / out-of-range fromIndex the same way Array.includes does.
let start = fromIndex < 0 ? length + fromIndex : fromIndex;
if (start < 0)
start = 0;
for (let index = start; index < length; index++) {
// `index` is bounded by `length`; `!` drops the index-access undefined.
if (compare(toValue(array[index]!), searched, index, array as T[]))
return true;
}
return false;
});
}