151 lines
5.2 KiB
JavaScript
151 lines
5.2 KiB
JavaScript
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;
|
|
}
|