Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .changeset/signal-builders-solid2-migration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"@solid-primitives/signal-builders": major
---

Migrate to Solid.js v2.0 (beta.13)

## Breaking Changes

**Peer dependency**: `solid-js@^2.0.0-beta.13` is now required.

- The `on` helper from `solid-js` (used internally by `capitalize`) is removed in Solid 2.0; `capitalize` now uses a plain `createMemo` which is equivalent
- `get` and `merge` now correctly return reactive `Accessor<T>` values via `createMemo` — previously they returned plain (non-reactive) values despite their type signatures claiming otherwise; any code that was working around this bug by calling the result as a plain value will break
107 changes: 51 additions & 56 deletions packages/signal-builders/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,23 @@
[![version](https://img.shields.io/npm/v/@solid-primitives/signal-builders?style=for-the-badge)](https://www.npmjs.com/package/@solid-primitives/signal-builders)
[![stage](https://img.shields.io/endpoint?style=for-the-badge&url=https%3A%2F%2Fraw.githubusercontent.com%2Fsolidjs-community%2Fsolid-primitives%2Fmain%2Fassets%2Fbadges%2Fstage-2.json)](https://github.com/solidjs-community/solid-primitives#contribution-process)

A collection of chainable and composable reactive signal calculations, _AKA_ **Signal Builders**.
A collection of chainable, composable reactive computations — **Signal Builders** — for common array, object, number, string, and type-conversion operations.

## Installation

```bash
npm install @solid-primitives/signal-builders
# or
yarn add @solid-primitives/signal-builders
pnpm add @solid-primitives/signal-builders
```

## How to use it
Requires `solid-js@^2.0.0-beta.13` as a peer dependency.

Signal builders create computations when used, so they need to be used under a reactive root.
## Usage

Note, since all of the signal builders use [`createMemo`](https://www.solidjs.com/docs/latest/api#creatememo) to wrap the calculation, updates will be caused only when the calculated value changes. Also the calculations should stay 'pure' – try to not cause side effects inside them.
Each builder wraps its computation in `createMemo`, so results only update when the computed value actually changes. Builders must be called inside a reactive owner (a component body or `createRoot`), and computations should be kept pure — avoid side effects inside them.

Because each builder returns an `Accessor<T>`, the output of one can be passed directly as input to another:

### Array

Expand Down Expand Up @@ -56,10 +58,10 @@ modifiedUser(); // { name: { first: "John", last: "Solid" }, age: 21 }
import { add, multiply, clamp, int } from "@solid-primitives/signal-builders";

const [input, setInput] = createSignal("123");
const [ing, setIng] = createSignal(-45);
const [offset, setOffset] = createSignal(-45);
const [max, setMax] = createSignal(1000);

const value = clamp(multiply(int(input), add(ing, 54, 9)), 0, max);
const value = clamp(multiply(int(input), add(offset, 54, 9)), 0, max);
```

### String
Expand All @@ -71,74 +73,67 @@ const [greeting, setGreeting] = createSignal("Hello");
const [target, setTarget] = createSignal("World");

const message = template`${greeting}, ${target}!`;
message(); // => Hello, World!
message(); // => "Hello, World!"

const solidMessage = lowercase(add(substring(message, 0, 7), "Solid"));
solidMessage(); // => hello, solid
solidMessage(); // => "hello, solid"
```

## List of builders
## Builder Reference

### Array

- **`push`** - basically `Array.prototype.push()`
- **`drop`** - drop n items from the array start
- **`dropRight`** - drop n items from the end of the array
- **`filter`** - basically `Array.prototype.filter()`
- **`filterOut`** - filter out passed item from an array
- **`remove`** - removes passed item from an array (first one from the start)
- **`removeItems`** - removes multiple items from an array
- **`splice`** - signal-builder `Array.prototype.splice()`
- **`slice`** - signal-builder `Array.prototype.slice()`
- **`map`** - signal-builder `Array.prototype.map()`
- **`sort`** - signal-builder `Array.prototype.sort()`
- **`concat`** - Append multiple arrays together
- **`flatten`** - Flattens a nested array into a one-level array
- **`filterInstance`** - filter list: only leave items that are instances of specified Classes
- **`filterOutInstance`** - filter list: remove items that are instances of specified Classes

### Object/Array

- **`get`** - Get a single property value of an object by specifying a path to it.
- **`update`** - Change single value in an object by key, or series of recursing keys.
- **`push`** — append items to an array
- **`drop`** — remove n items from the start
- **`dropRight`** — remove n items from the end
- **`filter`** — `Array.prototype.filter()`
- **`filterOut`** — remove all occurrences of a specific item
- **`remove`** — remove the first occurrence of a specific item
- **`removeItems`** — remove multiple specific items
- **`splice`** — `Array.prototype.splice()`
- **`slice`** — `Array.prototype.slice()`
- **`map`** — `Array.prototype.map()`
- **`sort`** — `Array.prototype.sort()`
- **`concat`** — concatenate multiple arrays
- **`flatten`** — flatten one level of nesting
- **`filterInstance`** — keep only items that are instances of the specified classes
- **`filterOutInstance`** — remove items that are instances of the specified classes

### Object

- **`omit`** - get an object copy without the provided keys
- **`pick`** - get an object copy with only the provided keys
- **`merge`** - Merges multiple objects into a single one.
- **`omit`** — copy an object without the specified keys
- **`pick`** — copy an object with only the specified keys
- **`get`** — read a value at a key path (up to 6 levels deep)
- **`merge`** — shallow merge of multiple objects
- **`update`** — immutably set a value at a key path; the last argument can be a new value or a setter function `(prev) => next`

### Convert

- **`string`** - turns passed value to a string
- **`float`** - turns passed string to an float number
- **`int`** - turns passed string to an intiger
- **`join`** - join array with a separator to a string
- **`string`** — convert a value to a string
- **`float`** — parse a string as a float (`Number.parseFloat`)
- **`int`** — parse a string as an integer (`Number.parseInt`)
- **`join`** join an array into a string with a separator

### Number

- **`add`** - `a + b + c + ...`
- **`substract`** - `a - b - c - ...`
- **`multiply`** - `a * b * c * ...`
- **`divide`** - `a / b / c / ...`
- **`power`** - `a ** b ** c ** ...`
- **`clamp`** - clamp a number value between two other values
- **`round`** - `Math.round()`
- **`ceil`** - `Math.ceil()`
- **`floor`** - `Math.floor()`
- **`add`** `a + b + c + ...`
- **`substract`** `a - b - c - ...`
- **`multiply`** `a * b * c * ...`
- **`divide`** `a / b / c / ...`
- **`power`** `a ** b ** c ** ...`
- **`clamp`** — constrain a value between min and max
- **`round`** `Math.round()`
- **`ceil`** `Math.ceil()`
- **`floor`** `Math.floor()`

### String

- **`add`** - `a + b + c + ...`
- **`lowercase`** - signal builder `String.prototype.toLowerCase()`
- **`uppercase`** - signal builder `String.prototype.toUpperCase()`
- **`capitalize`** - capitalize a string input e.g. `"solidJS"` -> `"Solidjs"`
- **`substring`** - signal builder `String.prototype.substring()`
- **`template`** - Create reactive string templates

## A call for feedback

`signal-builders` package is now a proof of concept of a fresh and experimental idea. Therefore all feedback/ideas/issues are highly welcome! :)
- **`lowercase`** — `String.prototype.toLowerCase()`
- **`uppercase`** — `String.prototype.toUpperCase()`
- **`capitalize`** — capitalize the first character and lowercase the rest
- **`substring`** — `String.prototype.substring()`
- **`add`** — `a + b + c + ...` (string concatenation)
- **`template`** — reactive tagged template literal

## Changelog

Expand Down
22 changes: 17 additions & 5 deletions packages/signal-builders/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@solid-primitives/signal-builders",
"version": "0.2.3",
"description": "A collection of chainable and composable reactive signal calculations, aka Signal Builders.",
"author": "Your Name <you@youremail.com>",
"author": "Damian Tarnawski <gthetarnav@gmail.com>",
"license": "MIT",
"homepage": "https://primitives.solidjs.community/package/signal-builders",
"repository": {
Expand All @@ -13,14 +13,26 @@
"name": "signal-builders",
"stage": 2,
"list": [
"List of builders"
"push",
"filter",
"sort",
"map",
"get",
"merge",
"update",
"add",
"clamp",
"template"
Comment thread
davedbase marked this conversation as resolved.
],
"category": "Reactivity"
},
"keywords": [
"solid",
"primitives",
"fp"
"signal",
"reactive",
"fp",
"functional"
],
"files": [
"dist"
Expand Down Expand Up @@ -49,10 +61,10 @@
"@solid-primitives/utils": "workspace:^"
},
"peerDependencies": {
"solid-js": "^1.6.12"
"solid-js": "^2.0.0-beta.14"
},
"typesVersions": {},
"devDependencies": {
"solid-js": "^1.9.7"
"solid-js": "2.0.0-beta.14"
}
}
66 changes: 37 additions & 29 deletions packages/signal-builders/src/array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,7 @@ import * as _ from "@solid-primitives/utils/immutable";
import { type Accessor, createMemo } from "solid-js";
import type { MappingFn, Predicate, FlattenArray } from "@solid-primitives/utils/immutable";

/**
* signal-builder `Array.prototype.push()`
Comment thread
davedbase marked this conversation as resolved.
*/
/** Reactively appends items to an array, returning a new array without mutating the original. */
export const push = <
A extends MaybeAccessor<any[]>,
V extends ItemsOf<MaybeAccessorValue<A>>,
Expand All @@ -25,60 +23,60 @@ export const push = <
createMemo(() => _.push(access(list), ...accessArray(items)));

/**
* signal-builder that drops n items from the array start.
* Reactively drops the first `n` elements from an array (default: 1).
* @example
* const [list, setList] = createSignal([1, 2, 3, 4]);
* drop(list, 2)(); // => [3, 4]
*/
export const drop = <T extends any[]>(list: MaybeAccessor<T>, n?: number): Accessor<T> =>
createMemo(() => _.drop(access(list), n) as T);

/**
* signal-builder that drops n items from the end of the array.
* Reactively drops the last `n` elements from an array (default: 1).
* @example
* const [list, setList] = createSignal([1, 2, 3, 4]);
* dropRight(list, 2)(); // => [1, 2]
*/
export const dropRight = <T extends any[]>(list: MaybeAccessor<T>, n?: number): Accessor<T> =>
createMemo(() => _.dropRight(access(list), n) as T);

/**
* signal-builder `Array.prototype.filter()`
*/
/** Reactively filters array items by a predicate function. */
export const filter = <A extends MaybeAccessor<any[]>, V extends ItemsOf<MaybeAccessorValue<A>>>(
list: A,
predicate: Predicate<V>,
): Accessor<V[]> => createMemo(() => _.filter(access(list), predicate));

/**
* signal-builder `Array.prototype.filter()` that filters out passed item
*/
/** Reactively filters out all occurrences of `item` from the array. */
export const filterOut = <A extends MaybeAccessor<any[]>, V extends ItemsOf<MaybeAccessorValue<A>>>(
list: A,
item: MaybeAccessor<V>,
): Accessor<V[]> => createMemo(() => _.filterOut(access(list), access(item)));

/**
* signal-builder `Array.prototype.sort()`
* Reactively sorts an array, returning a new sorted copy without mutating the original.
* Without a `compareFn`, items are sorted lexicographically (coerced to strings).
*/
export const sort = <A extends MaybeAccessor<any[]>, V extends ItemsOf<MaybeAccessorValue<A>>>(
list: A,
compareFn?: (a: V, b: V) => number,
): Accessor<V[]> => createMemo(() => _.sort(access(list), compareFn));

/**
* signal-builder `Array.prototype.map()`
*/
/** Reactively maps array items to a new value via `mapFn`. */
export const map = <A extends MaybeAccessor<any[]>, T>(
list: A,
mapFn: MappingFn<ItemsOf<MaybeAccessorValue<A>>, T>,
): Accessor<T[]> => createMemo(() => _.map(access(list), mapFn));

/**
* signal-builder `Array.prototype.slice()`
*/
/** Reactive `Array.prototype.slice()` — extracts a portion of the array without mutating it. */
export const slice = <T extends any[]>(
list: MaybeAccessor<T>,
start?: number,
end?: number,
): Accessor<T> => createMemo(() => _.slice(access(list), start, end) as T);

/**
* signal-builder `Array.prototype.splice()`
* Reactively removes/replaces elements and optionally inserts new ones, returning a new array.
* Immutable equivalent of `Array.prototype.splice()`.
*/
export const splice = <
A extends MaybeAccessor<any[]>,
Expand All @@ -94,38 +92,45 @@ export const splice = <
_.splice(access(list), access(start), access(deleteCount), ...accessArray(items)),
);

/**
* signal-builder removing passed item from an array (first one from the start)
*/
/** Reactively removes the first occurrence of `item` from the array. */
export const remove = <A extends MaybeAccessor<any[]>, V extends ItemsOf<MaybeAccessorValue<A>>>(
list: A,
item: MaybeAccessor<V>,
): Accessor<V[]> => createMemo(() => _.remove(access(list), access(item)));

/**
* signal-builder removing multiple items from an array
* Reactively removes the first occurrence of each provided item from the array.
* Once an item is matched, subsequent occurrences of that item in the list are kept.
* @example
* const [list, setList] = createSignal([1, 2, 3, 2, 1]);
* removeItems(list, 1, 2)(); // => [3, 2, 1] (first 1 and first 2 removed)
*/
export const removeItems = <T extends any[]>(
list: MaybeAccessor<T>,
...items: MaybeAccessor<ItemsOf<T>>[]
): Accessor<T> => createMemo(() => _.removeItems(access(list), ...accessArray(items)) as T);

/**
* signal-builder appending multiple arrays together
* Reactively concatenates multiple arrays or values into a single flat array.
* @example
* const [a, setA] = createSignal([1, 2]);
* const [b, setB] = createSignal([3, 4]);
* concat(a, b)(); // => [1, 2, 3, 4]
*/
export const concat = <A extends MaybeAccessor<any>[], V extends MaybeAccessorValue<ItemsOf<A>>>(
...a: A
): Accessor<Array<V extends any[] ? ItemsOf<V> : V>> =>
createMemo(() => _.concat(...accessArray(a)));

/**
* Signal builder: Flattens a nested array into a one-level array
*/
/** Reactively deep-flattens a nested array (fully recursive, not just one level). */
export const flatten = <T extends any[]>(list: MaybeAccessor<T>): Accessor<FlattenArray<T>> =>
createMemo(() => _.flatten(access(list)) as FlattenArray<T>);

/**
* Signal builder: filter list: only leave items that are instances of specified Classes
* Reactively keeps only items that are instances of any of the provided classes.
* @example
* const [nodes, setNodes] = createSignal([new Text("hi"), new Comment("x"), new Text("bye")]);
* filterInstance(nodes, Text)(); // => [Text("hi"), Text("bye")]
*/
export const filterInstance = <T, I extends AnyClass[]>(list: MaybeAccessor<T[]>, ...classes: I) =>
(classes.length === 1
Expand All @@ -135,7 +140,10 @@ export const filterInstance = <T, I extends AnyClass[]>(list: MaybeAccessor<T[]>
)) as Accessor<Extract<T, InstanceType<ItemsOf<I>>>[]>;

/**
* Signal builder: filter list: remove items that are instances of specified Classes
* Reactively removes items that are instances of any of the provided classes.
* @example
* const [nodes, setNodes] = createSignal([new Text("hi"), new Comment("x"), new Text("bye")]);
* filterOutInstance(nodes, Comment)(); // => [Text("hi"), Text("bye")]
*/
export const filterOutInstance = <T, I extends AnyClass[]>(
list: MaybeAccessor<T[]>,
Expand Down
Loading