UNPKG

@solid-primitives/refs

Version:

Library of primitives, components and directives for SolidJS that help managing references to JSX elements.

151 lines (150 loc) 5.33 kB
import { chain, arrayEquals } from "@solid-primitives/utils"; import { children, createComputed, createMemo, onCleanup, untrack } from "solid-js"; import { isServer } from "solid-js/web"; /** * Utility for chaining multiple `ref` assignments with `props.ref` forwarding. * @param refs list of ref setters. Can be a `props.ref` prop for ref forwarding or a setter to a local variable (`el => ref = el`). * @example * ```tsx * interface ButtonProps { * ref?: Ref<HTMLButtonElement> * } * function Button (props: ButtonProps) { * let ref: HTMLButtonElement | undefined * onMount(() => { * // use the local ref * }) * return <button ref={mergeRefs(props.ref, el => ref = el)} /> * } * * // in consumer's component: * let ref: HTMLButtonElement | undefined * <Button ref={ref} /> * ``` */ export function mergeRefs(...refs) { return chain(refs); } /** * Default predicate used in `resolveElements()` and `resolveFirst()` to filter Elements. * * On the client it uses `instanceof Element` check, on the server it checks for the object with `t` property. (generated by compiling JSX) */ export const defaultElementPredicate = isServer ? (item) => item != null && typeof item === "object" && "t" in item : (item) => item instanceof Element; /** * Utility for resolving recursively nested JSX children to a single element or an array of elements using a predicate. * * It does **not** create a computation - should be wrapped in one to repeat the resolution on changes. * * @param value JSX children * @param predicate predicate to filter elements * @returns single element or an array of elements or `null` if no elements were found */ export function getResolvedElements(value, predicate) { if (predicate(value)) return value; if (typeof value === "function" && !value.length) return getResolvedElements(value(), predicate); if (Array.isArray(value)) { const results = []; for (const item of value) { const result = getResolvedElements(item, predicate); if (result) Array.isArray(result) ? results.push.apply(results, result) : results.push(result); } return results.length ? results : null; } return null; } export function resolveElements(fn, predicate = defaultElementPredicate, serverPredicate = defaultElementPredicate) { const children = createMemo(fn); const memo = createMemo(() => getResolvedElements(children(), isServer ? serverPredicate : predicate)); memo.toArray = () => { const value = memo(); return Array.isArray(value) ? value : value ? [value] : []; }; return memo; } /** * Utility for resolving recursively nested JSX children in search of the first element that matches a predicate. * * It does **not** create a computation - should be wrapped in one to repeat the resolution on changes. * * @param value JSX children * @param predicate predicate to filter elements * @returns single found element or `null` if no elements were found */ export function getFirstChild(value, predicate) { if (predicate(value)) return value; if (typeof value === "function" && !value.length) return getFirstChild(value(), predicate); if (Array.isArray(value)) { for (const item of value) { const result = getFirstChild(item, predicate); if (result) return result; } } return null; } export function resolveFirst(fn, predicate = defaultElementPredicate, serverPredicate = defaultElementPredicate) { const children = createMemo(fn); return createMemo(() => getFirstChild(children(), isServer ? serverPredicate : predicate)); } /** * Get up-to-date references of the multiple children elements. * @param ref Getter of current array of elements * @see https://github.com/solidjs-community/solid-primitives/tree/main/packages/refs#Refs * @example * ```tsx * const [refs, setRefs] = createSignal<Element[]>([]); * <Refs ref={setRefs}> * {props.children} * </Refs> * ``` */ export function Refs(props) { if (isServer) { return props.children; } const cb = props.ref, resolved = children(() => props.children); let prev = []; createComputed(() => { const els = resolved.toArray().filter(defaultElementPredicate); if (!arrayEquals(prev, els)) untrack(() => cb(els)); prev = els; }, []); onCleanup(() => prev.length && cb([])); return resolved; } /** * Get up-to-date reference to a single child element. * @param ref Getter of current element *(or `undefined` if not mounted)* * @see https://github.com/solidjs-community/solid-primitives/tree/main/packages/refs#Ref * @example * ```tsx * const [ref, setRef] = createSignal<Element | undefined>(); * <Ref ref={setRef}> * {props.children} * </Ref> * ``` */ export function Ref(props) { if (isServer) { return props.children; } const cb = props.ref, resolved = children(() => props.children); let prev; createComputed(() => { const el = resolved.toArray().find(defaultElementPredicate); if (el !== prev) untrack(() => cb(el)); prev = el; }); onCleanup(() => prev && cb(undefined)); return resolved; }