UNPKG

@solid-primitives/range

Version:

Control Flow Primitives for displaying given number or a number range of elements.

146 lines (145 loc) 5.83 kB
import { createRoot, onCleanup, untrack, DEV, createMemo } from "solid-js"; import { isServer } from "solid-js/web"; import { abs, accessor, ceil, floor, min, toFunction } from "./common.js"; /** * Reactively maps a number range of specified `stop`, `to` and `step`, with a callback function - underlying helper for the `<Range>` control flow. * @param getStart number accessor of the start of the range * @param getTo number accessor of the end of the range *(not included in the range)* * @param getStep number accessor of the difference between two points in the range *(negative step value depends on the `to` being greater/smaller than `start`, not this argument)* * @param mapFn reactive function used to create mapped output item array * @param options a fallback for when the input list is empty or missing * @returns mapped input array signal * @see https://github.com/solidjs-community/solid-primitives/tree/main/packages/range#mapRange * @example * ```tsx * const [to, setTo] = createSignal(5) * const mapped = mapRange(() => 0, to, () => 0.5, number => { * const [value, setValue] = createSignal(number); * createEffect(() => {...}) * return value * }) * mapped() // => [0, 0.5, 1, 1.5, 2...] * setTo(3) // changes the output array, mapping only added numbers * ``` */ export function mapRange(getStart, getTo, getStep, mapFn, options = {}) { let disposers = [], items = [], prevStart = 0, prevTo = 0, prevStep = getStep(), fallback = false; onCleanup(() => disposers.forEach(f => f())); const mapper = (i, n, itemsList, disposersList) => createRoot(dispose => { disposersList[i] = dispose; itemsList[i] = mapFn(n); }); const mapNewRange = (start, to, step) => { // new range is empty if (start === to) { disposers.forEach(f => f()); if (options.fallback) { fallback = true; return createRoot(dispose => { disposers = [dispose]; return (items = [options.fallback()]); }); } disposers = []; return (items = []); } let n = start, i = 0; if (fallback) { fallback = false; disposers[0](); items = []; } // previous range was empty if (!items.length) { for (; n < to; i++, n += step) mapper(i, n, items, disposers); return items; } const newLength = ceil((to - start) / step); const newItems = new Array(newLength); const newDisposers = new Array(newLength); const oldDisposers = disposers; let end; // front - add if (start < prevStart) { end = min(ceil((prevStart - start) / step), newLength); for (n = start; i < end; i++, n += step) mapper(i, n, newItems, newDisposers); } // body - keep/add end = ceil((min(prevTo, to) - start) / step); for (; i < end; n += step, i++) { let index = (n - prevStart) / prevStep; if (Number.isInteger(index) || (index < 1 && index + Number.EPSILON > 1)) { index = ceil(index); newItems[i] = items[index]; newDisposers[i] = disposers[index]; oldDisposers[index] = undefined; } else { mapper(i, n, newItems, newDisposers); } } // back - add if (to > prevTo) { for (; i < newLength; i++, n += step) mapper(i, n, newItems, newDisposers); } oldDisposers.forEach(f => f?.()); disposers = newDisposers; return (items = newItems); }; return () => { let _step = getStep(); if (_step === 0) { // eslint-disable-next-line no-console if (!isServer && DEV) console.warn("Range cannot have a step of 0"); return items; } let _start = getStart(); let _to = getTo(); _step = abs(_step); const positive = _start <= _to; if (!positive) { const temp = _start; const x = (_start - _to) / _step; _start = _start - (Number.isInteger(x) ? x - 1 : floor(x)) * _step; _to = temp + _step; } const list = untrack(mapNewRange.bind(void 0, _start, _to, _step)); prevStart = _start; prevTo = _to; prevStep = _step; return positive ? list : [...list].reverse(); }; } /** * Creates a list of elements by mapping a number range of specified `start`, `to`, and `step`. * @param start beginning of the number range *(defaults to 0)* * @param to end *(not included)* of the number range * @param step difference between two points in the range *(negative step value depends on the `to` being greater/smaller then `start`, not this argument)* *(defaults to 1)* * @param fallback element returned when the range size is 0 * @param children render function, recives plain number value * @see https://github.com/solidjs-community/solid-primitives/tree/main/packages/range#Range * @example * ```tsx * <Range start={2} to={14} step={0.5}> * {n => <div>{n}</div>} * </Range> * ``` */ export function Range(props) { let start, to, step; if ("to" in props) { start = () => props.start ?? 0; to = () => props.to; step = () => props.step ?? 1; } else { start = accessor(() => props[0]); to = accessor(() => props[1]); step = accessor(() => props[2] ?? 1); } return createMemo(mapRange(start, to, step, toFunction(() => props.children), "fallback" in props ? { fallback: () => props.fallback } : undefined)); }