@solid-primitives/range
Version:
Control Flow Primitives for displaying given number or a number range of elements.
146 lines (145 loc) • 5.83 kB
JavaScript
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));
}