diff --git a/.changeset/sharp-beers-approve.md b/.changeset/sharp-beers-approve.md new file mode 100644 index 000000000..d7d21b1ab --- /dev/null +++ b/.changeset/sharp-beers-approve.md @@ -0,0 +1,5 @@ +--- +"@solid-primitives/keyed": minor +--- + +Add `MapEntries` control flow component. diff --git a/README.md b/README.md index 311855f70..4cb006a29 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ The goal of Solid Primitives is to wrap client and server side functionality to |

*Control Flow*

| |[context](https://github.com/solidjs-community/solid-primitives/tree/main/packages/context#readme)|[![STAGE](https://img.shields.io/endpoint?style=for-the-badge&label=&url=https%3A%2F%2Fraw.githubusercontent.com%2Fsolidjs-community%2Fsolid-primitives%2Fmain%2Fassets%2Fbadges%2Fstage-2.json)](https://github.com/solidjs-community/solid-primitives/blob/main/CONTRIBUTING.md#contribution-process)|[createContextProvider](https://github.com/solidjs-community/solid-primitives/tree/main/packages/context#createcontextprovider)
[MultiProvider](https://github.com/solidjs-community/solid-primitives/tree/main/packages/context#multiprovider)|[![SIZE](https://img.shields.io/bundlephobia/minzip/@solid-primitives/context?style=for-the-badge&label=)](https://bundlephobia.com/package/@solid-primitives/context)|[![VERSION](https://img.shields.io/npm/v/@solid-primitives/context?style=for-the-badge&label=)](https://www.npmjs.com/package/@solid-primitives/context)| |[jsx-tokenizer](https://github.com/solidjs-community/solid-primitives/tree/main/packages/jsx-tokenizer#readme)|[![STAGE](https://img.shields.io/endpoint?style=for-the-badge&label=&url=https%3A%2F%2Fraw.githubusercontent.com%2Fsolidjs-community%2Fsolid-primitives%2Fmain%2Fassets%2Fbadges%2Fstage-2.json)](https://github.com/solidjs-community/solid-primitives/blob/main/CONTRIBUTING.md#contribution-process)|[createTokenizer](https://github.com/solidjs-community/solid-primitives/tree/main/packages/jsx-tokenizer#createtokenizer)
[createToken](https://github.com/solidjs-community/solid-primitives/tree/main/packages/jsx-tokenizer#createtoken)
[resolveTokens](https://github.com/solidjs-community/solid-primitives/tree/main/packages/jsx-tokenizer#resolvetokens)
[isToken](https://github.com/solidjs-community/solid-primitives/tree/main/packages/jsx-tokenizer#istoken)|[![SIZE](https://img.shields.io/bundlephobia/minzip/@solid-primitives/jsx-tokenizer?style=for-the-badge&label=)](https://bundlephobia.com/package/@solid-primitives/jsx-tokenizer)|[![VERSION](https://img.shields.io/npm/v/@solid-primitives/jsx-tokenizer?style=for-the-badge&label=)](https://www.npmjs.com/package/@solid-primitives/jsx-tokenizer)| -|[keyed](https://github.com/solidjs-community/solid-primitives/tree/main/packages/keyed#readme)|[![STAGE](https://img.shields.io/endpoint?style=for-the-badge&label=&url=https%3A%2F%2Fraw.githubusercontent.com%2Fsolidjs-community%2Fsolid-primitives%2Fmain%2Fassets%2Fbadges%2Fstage-3.json)](https://github.com/solidjs-community/solid-primitives/blob/main/CONTRIBUTING.md#contribution-process)|[keyArray](https://github.com/solidjs-community/solid-primitives/tree/main/packages/keyed#keyarray)
[Key](https://github.com/solidjs-community/solid-primitives/tree/main/packages/keyed#key)
[Entries](https://github.com/solidjs-community/solid-primitives/tree/main/packages/keyed#entries)|[![SIZE](https://img.shields.io/bundlephobia/minzip/@solid-primitives/keyed?style=for-the-badge&label=)](https://bundlephobia.com/package/@solid-primitives/keyed)|[![VERSION](https://img.shields.io/npm/v/@solid-primitives/keyed?style=for-the-badge&label=)](https://www.npmjs.com/package/@solid-primitives/keyed)| +|[keyed](https://github.com/solidjs-community/solid-primitives/tree/main/packages/keyed#readme)|[![STAGE](https://img.shields.io/endpoint?style=for-the-badge&label=&url=https%3A%2F%2Fraw.githubusercontent.com%2Fsolidjs-community%2Fsolid-primitives%2Fmain%2Fassets%2Fbadges%2Fstage-3.json)](https://github.com/solidjs-community/solid-primitives/blob/main/CONTRIBUTING.md#contribution-process)|[keyArray](https://github.com/solidjs-community/solid-primitives/tree/main/packages/keyed#keyarray)
[Key](https://github.com/solidjs-community/solid-primitives/tree/main/packages/keyed#key)
[Entries](https://github.com/solidjs-community/solid-primitives/tree/main/packages/keyed#entries)
[MapEntries](https://github.com/solidjs-community/solid-primitives/tree/main/packages/keyed#mapentries)|[![SIZE](https://img.shields.io/bundlephobia/minzip/@solid-primitives/keyed?style=for-the-badge&label=)](https://bundlephobia.com/package/@solid-primitives/keyed)|[![VERSION](https://img.shields.io/npm/v/@solid-primitives/keyed?style=for-the-badge&label=)](https://www.npmjs.com/package/@solid-primitives/keyed)| |[list](https://github.com/solidjs-community/solid-primitives/tree/main/packages/list#readme)|[![STAGE](https://img.shields.io/endpoint?style=for-the-badge&label=&url=https%3A%2F%2Fraw.githubusercontent.com%2Fsolidjs-community%2Fsolid-primitives%2Fmain%2Fassets%2Fbadges%2Fstage-0.json)](https://github.com/solidjs-community/solid-primitives/blob/main/CONTRIBUTING.md#contribution-process)|[listArray](https://github.com/solidjs-community/solid-primitives/tree/main/packages/list#listarray)
[List](https://github.com/solidjs-community/solid-primitives/tree/main/packages/list#list)|[![SIZE](https://img.shields.io/bundlephobia/minzip/@solid-primitives/list?style=for-the-badge&label=)](https://bundlephobia.com/package/@solid-primitives/list)|[![VERSION](https://img.shields.io/npm/v/@solid-primitives/list?style=for-the-badge&label=)](https://www.npmjs.com/package/@solid-primitives/list)| |[range](https://github.com/solidjs-community/solid-primitives/tree/main/packages/range#readme)|[![STAGE](https://img.shields.io/endpoint?style=for-the-badge&label=&url=https%3A%2F%2Fraw.githubusercontent.com%2Fsolidjs-community%2Fsolid-primitives%2Fmain%2Fassets%2Fbadges%2Fstage-1.json)](https://github.com/solidjs-community/solid-primitives/blob/main/CONTRIBUTING.md#contribution-process)|[repeat](https://github.com/solidjs-community/solid-primitives/tree/main/packages/range#repeat)
[mapRange](https://github.com/solidjs-community/solid-primitives/tree/main/packages/range#maprange)
[indexRange](https://github.com/solidjs-community/solid-primitives/tree/main/packages/range#indexrange)
[Repeat](https://github.com/solidjs-community/solid-primitives/tree/main/packages/range#repeat)
[Range](https://github.com/solidjs-community/solid-primitives/tree/main/packages/range#range)
[IndexRange](https://github.com/solidjs-community/solid-primitives/tree/main/packages/range#indexrange)|[![SIZE](https://img.shields.io/bundlephobia/minzip/@solid-primitives/range?style=for-the-badge&label=)](https://bundlephobia.com/package/@solid-primitives/range)|[![VERSION](https://img.shields.io/npm/v/@solid-primitives/range?style=for-the-badge&label=)](https://www.npmjs.com/package/@solid-primitives/range)| |[refs](https://github.com/solidjs-community/solid-primitives/tree/main/packages/refs#readme)|[![STAGE](https://img.shields.io/endpoint?style=for-the-badge&label=&url=https%3A%2F%2Fraw.githubusercontent.com%2Fsolidjs-community%2Fsolid-primitives%2Fmain%2Fassets%2Fbadges%2Fstage-2.json)](https://github.com/solidjs-community/solid-primitives/blob/main/CONTRIBUTING.md#contribution-process)|[mergeRefs](https://github.com/solidjs-community/solid-primitives/tree/main/packages/refs#mergerefs)
[resolveElements](https://github.com/solidjs-community/solid-primitives/tree/main/packages/refs#resolveelements)
[resolveFirst](https://github.com/solidjs-community/solid-primitives/tree/main/packages/refs#resolvefirst)
[Ref](https://github.com/solidjs-community/solid-primitives/tree/main/packages/refs#ref)
[Refs](https://github.com/solidjs-community/solid-primitives/tree/main/packages/refs#refs)|[![SIZE](https://img.shields.io/bundlephobia/minzip/@solid-primitives/refs?style=for-the-badge&label=)](https://bundlephobia.com/package/@solid-primitives/refs)|[![VERSION](https://img.shields.io/npm/v/@solid-primitives/refs?style=for-the-badge&label=)](https://www.npmjs.com/package/@solid-primitives/refs)| diff --git a/packages/keyed/README.md b/packages/keyed/README.md index a6ed35ff7..a08cd82ac 100644 --- a/packages/keyed/README.md +++ b/packages/keyed/README.md @@ -14,6 +14,7 @@ Control Flow primitives and components that require specifying explicit keys to - [`keyArray`](#keyarray) - Reactively maps an array by specified key with a callback function - underlying helper for the `` control flow. - [`Key`](#key) - Creates a list of elements by mapping items by provided key. - [`Entries`](#entries) - Creates a list of elements by mapping object entries. +- [`MapEntries`](#mapentries) - Creates a list of elements by mapping Map entries. - [`Rerun`](#rerun) - Causes the children to rerender when the `on` changes. ## Installation @@ -152,6 +153,42 @@ Third argument of the map function is an index signal. ``` +## `` + +Creates a list of elements by mapping Map entries. Similar to Solid's `` and ``, but here, render function takes three arguments, and both value and index arguments are signals. + +### How to use it + +```tsx +import { MapEntries } from "@solid-primitives/keyed"; + +const [map, setMap] = createSignal(new Map()); + +No items}> + {(key, value) => ( +
+ {key}: {value()} +
+ )} +
; +``` + +### Index argument + +Third argument of the map function is an index signal. + +`MapEntries` is using [`Map#key()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/keys) so the index and resulting JSX will follow the insertion order. + +```tsx +No items}> + {(key, value, index) => ( +
+ {key}: {value()} +
+ )} +
+``` + ## `` Causes the children to rerender when the `on` key changes. Equivalent of `v-key` in vue, and `{#key}` in svelte. diff --git a/packages/keyed/dev/entries.tsx b/packages/keyed/dev/entries.tsx new file mode 100644 index 000000000..48b1c7e9f --- /dev/null +++ b/packages/keyed/dev/entries.tsx @@ -0,0 +1,95 @@ +// changes to this file might be applicable to similar files - grep 95DB7339-BB2A-4F06-A34A-25DDF8BF7AF7 + +import { createStore, produce } from "solid-js/store"; +import { createEffect } from "solid-js"; +import { Entries } from "../src/index.js"; +import { TransitionGroup } from "solid-transition-group"; + +const foods = [ + "oatmeal", + "plantains", + "cranberries", + "chickpeas", + "tofu", + "Parmesan cheese", + "amaretto", + "sunflower seeds", + "grapes", + "vegemite", + "pasta", + "cider", + "chicken", + "pinto beans", + "bok choy", + "sweet peppers", + "Cappuccino Latte", + "corn", + "broccoli", + "brussels sprouts", + "bread", + "milk", + "honey", + "chips", + "cookie", +]; +const randomIndex = (list: readonly any[]): number => Math.floor(Math.random() * list.length); +const getRandomFood = () => foods[randomIndex(foods)]!; +const randomKey = (record: Record): string => { + const keys = Object.keys(record); + return keys[randomIndex(keys)]!; +}; + +export default function App() { + const [store, setStore] = createStore>({ + [Math.random()]: "bread", + [Math.random()]: "milk", + [Math.random()]: "honey", + [Math.random()]: "chips", + [Math.random()]: "cookie", + }); + + const addRandom = () => { + setStore(Math.random().toString(), getRandomFood()); + }; + const removeRandom = () => setStore(p => ({ [randomKey(p)]: undefined })); + const clone = () => setStore("entries", p => JSON.parse(JSON.stringify(p))); + const changeRandomValue = () => setStore(p => ({ [randomKey(p)]: getRandomFood() })); + + return ( + <> +
+ + + + +
+
+ + No items.

}> + {(key, value, index) => { + createEffect(() => { + console.log("Effect:", key, value()); + }); + return ( +
+ {index()}. {value()} +
+ ID: {key} +
+
+ ); + }} +
+
+
+ + ); +} diff --git a/packages/keyed/dev/index.tsx b/packages/keyed/dev/index.tsx index 4c9b40c71..1a176d5ba 100644 --- a/packages/keyed/dev/index.tsx +++ b/packages/keyed/dev/index.tsx @@ -1,11 +1,24 @@ import { Component } from "solid-js"; import Key from "./key.js"; +import Entries from "./entries.js"; +import MapEntries from "./mapEntries.js"; const App: Component = () => { return (
- +
+

Key

+ +
+
+

Entries

+ +
+
+

MapEntries

+ +
); }; diff --git a/packages/keyed/dev/key.tsx b/packages/keyed/dev/key.tsx index 593a4c7eb..b14e93318 100644 --- a/packages/keyed/dev/key.tsx +++ b/packages/keyed/dev/key.tsx @@ -1,3 +1,5 @@ +// changes to this file might be applicable to similar files - grep 95DB7339-BB2A-4F06-A34A-25DDF8BF7AF7 + import { splice, update } from "@solid-primitives/utils/immutable"; import { createEffect, createSignal } from "solid-js"; import { Key } from "../src/index.js"; diff --git a/packages/keyed/dev/mapEntries.tsx b/packages/keyed/dev/mapEntries.tsx new file mode 100644 index 000000000..341488b7b --- /dev/null +++ b/packages/keyed/dev/mapEntries.tsx @@ -0,0 +1,110 @@ +// changes to this file might be applicable to similar files - grep 95DB7339-BB2A-4F06-A34A-25DDF8BF7AF7 + +import { createEffect, createSignal } from "solid-js"; +import { MapEntries } from "../src/index.js"; +import { TransitionGroup } from "solid-transition-group"; + +const foods = [ + "oatmeal", + "plantains", + "cranberries", + "chickpeas", + "tofu", + "Parmesan cheese", + "amaretto", + "sunflower seeds", + "grapes", + "vegemite", + "pasta", + "cider", + "chicken", + "pinto beans", + "bok choy", + "sweet peppers", + "Cappuccino Latte", + "corn", + "broccoli", + "brussels sprouts", + "bread", + "milk", + "honey", + "chips", + "cookie", +]; +const randomIndex = (list: readonly any[]): number => Math.floor(Math.random() * list.length); +const getRandomFood = () => foods[randomIndex(foods)]!; +const randomKey = (map: Map): string => { + const keys = Array.from(map.keys()); + return keys[randomIndex(keys)]!; +}; + +export default function App() { + const [map, setMap] = createSignal( + new Map([ + [Math.random().toString(), "bread"], + [Math.random().toString(), "milk"], + [Math.random().toString(), "honey"], + [Math.random().toString(), "chips"], + [Math.random().toString(), "cookie"], + ]), + ); + + const addRandom = () => { + setMap(p => { + p.set(Math.random().toString(), getRandomFood()); + return new Map(p); + }); + }; + const removeRandom = () => + setMap(p => { + p.delete(randomKey(p)); + return new Map(p); + }); + const clone = () => setMap(p => new Map(p)); + const changeRandomValue = () => + setMap(p => { + p.set(randomKey(p), getRandomFood()); + return new Map(p); + }); + + return ( + <> +
+ + + + +
+
+ + No items.

} + > + {(key, value, index) => { + createEffect(() => { + console.log("Effect:", key, value()); + }); + return ( +
+ {index()}. {value()} +
+ ID: {key} +
+
+ ); + }} +
+
+
+ + ); +} diff --git a/packages/keyed/package.json b/packages/keyed/package.json index ac7f33615..1b8c0dc28 100644 --- a/packages/keyed/package.json +++ b/packages/keyed/package.json @@ -15,7 +15,8 @@ "list": [ "keyArray", "Key", - "Entries" + "Entries", + "MapEntries" ], "category": "Control Flow" }, diff --git a/packages/keyed/src/index.ts b/packages/keyed/src/index.ts index 342a86795..8be5e01a7 100644 --- a/packages/keyed/src/index.ts +++ b/packages/keyed/src/index.ts @@ -185,6 +185,7 @@ export function Entries(props: { i: Accessor, ) => JSX.Element; }): JSX.Element { + // changes to this function may be applicable to similar functions - grep 4A29BECD-767A-4CC0-AEBB-3543D7B444C6 const mapFn = props.children; return createMemo( mapArray( @@ -201,6 +202,43 @@ export function Entries(props: { ) as unknown as JSX.Element; } +/** + * Creates a list of elements from the entries of provided Map + * + * @param props + * @param props.of map to iterate entries of (`myMap.entries()`) + * @param props.children + * a map render function that receives a Map key, **value signal** and **index signal** and returns a JSX-Element; if the list is empty, an optional fallback is returned: + * ```tsx + * No items}> + * {(key, value, index) =>
{key}: {value()}
} + *
+ * ``` + * + * @see https://github.com/solidjs-community/solid-primitives/tree/main/packages/keyed#mapentries + */ +export function MapEntries(props: { + of: Map | undefined | null | false; + fallback?: JSX.Element; + children: (key: K, v: Accessor, i: Accessor) => JSX.Element; +}): JSX.Element { + // changes to this function may be applicable to similar functions - grep 4A29BECD-767A-4CC0-AEBB-3543D7B444C6 + const mapFn = props.children; + return createMemo( + mapArray( + () => props.of && Array.from(props.of.keys()), + mapFn.length < 3 + ? key => + (mapFn as (key: K, v: Accessor) => JSX.Element)( + key, + () => (props.of as Map).get(key)!, + ) + : (key, i) => mapFn(key, () => (props.of as Map).get(key)!, i), + "fallback" in props ? { fallback: () => props.fallback } : undefined, + ), + ) as unknown as JSX.Element; +} + export type RerunChildren = ((input: T, prevInput: T | undefined) => JSX.Element) | JSX.Element; /** diff --git a/packages/keyed/test/index.test.ts b/packages/keyed/test/index.test.ts deleted file mode 100644 index 435d770bd..000000000 --- a/packages/keyed/test/index.test.ts +++ /dev/null @@ -1,209 +0,0 @@ -import { createComputed, createMemo, createRoot, createSignal } from "solid-js"; -import { createStore } from "solid-js/store"; -import { update } from "@solid-primitives/utils/immutable"; -import { describe, expect, test } from "vitest"; -import { keyArray } from "../src/index.js"; - -const el1 = { id: 1, value: "bread" }; -const el2 = { id: 2, value: "milk" }; -const el3 = { id: 3, value: "honey" }; -const el4 = { id: 4, value: "chips" }; - -describe("keyArray", () => { - test("maps and returns all initial items", () => - createRoot(dispose => { - const mapped = keyArray( - () => [el1, el2, el3], - v => v.id, - v => ({ ...v(), key: v().id }), - ); - expect(mapped().length).toBe(3); - expect(mapped()[0].key).toBe(1); - expect(mapped()[1].key).toBe(2); - expect(mapped()[2].key).toBe(3); - - dispose(); - })); - - test("cloning list should have no effect", () => - createRoot(dispose => { - const [list, setList] = createSignal([el1, el2, el3]); - let changes = 0; - const mapped = keyArray( - list, - v => v.id, - v => v().id, - ); - createComputed(() => mapped(), changes++); - expect(mapped()).toEqual([1, 2, 3]); - expect(changes).toBe(1); - - setList(p => p.slice()); - expect(mapped()).toEqual([1, 2, 3]); - expect(changes).toBe(1); - - dispose(); - })); - - test("mapFn is reactive", () => - createRoot(dispose => { - const [list, setList] = createSignal([el1, el2, el3]); - let changes = 0; - const mapped = keyArray( - list, - v => v.id, - v => { - const item = { value: v().value }; - createComputed(() => (item.value = v().value)); - return item; - }, - ); - createComputed(() => mapped(), changes++); - - expect(mapped()).toEqual([{ value: "bread" }, { value: "milk" }, { value: "honey" }]); - expect(changes).toBe(1); - - setList(p => update(p, 0, "value", "bananas")); - expect(mapped()).toEqual([{ value: "bananas" }, { value: "milk" }, { value: "honey" }]); - expect(changes).toBe(1); - - dispose(); - - setList(p => update(p, 1, "value", "orange juice")); - expect(mapped()).toEqual([{ value: "bananas" }, { value: "milk" }, { value: "honey" }]); - expect(changes).toBe(1); - expect(changes).toBe(1); - })); - - test("index is reactive", () => - createRoot(dispose => { - const [list, setList] = createSignal([el1, el2, el3]); - let changes = 0; - let maprun = 0; - const mapped = keyArray( - list, - v => v.id, - (v, i) => { - maprun++; - const item = { i: i(), v: v().value }; - createComputed(() => (item.i = i()), (item.v = v().value)); - return item; - }, - ); - createComputed(() => { - mapped(); - changes++; - }); - - expect(mapped()).toEqual([ - { i: 0, v: "bread" }, - { i: 1, v: "milk" }, - { i: 2, v: "honey" }, - ]); - expect(changes).toBe(1); - expect(maprun).toBe(3); - - setList([el1, el3, el2]); - expect(mapped()).toEqual([ - { i: 0, v: "bread" }, - { i: 1, v: "honey" }, - { i: 2, v: "milk" }, - ]); - expect(changes).toBe(2); - expect(maprun).toBe(3); - - setList([el1, el4, el3, el2]); - expect(mapped()).toEqual([ - { i: 0, v: "bread" }, - { i: 1, v: "chips" }, - { i: 2, v: "honey" }, - { i: 3, v: "milk" }, - ]); - expect(changes).toBe(3); - expect(maprun).toBe(4); - - dispose(); - })); - - test("supports top-level store arrays", () => - createRoot(dispose => { - const [list, setList] = createStore([ - { i: 0, v: "foo" }, - { i: 1, v: "bar" }, - { i: 2, v: "baz" }, - ]); - - const mapped = keyArray( - () => list, - e => e.i, - (item, index) => [item, index] as const, - ); - - const getUnwrapped = (): [number, string, number][] => - mapped().map(([e, index]) => { - const { i, v } = e(); - return [i, v, index()]; - }); - - expect(mapped().length).toBe(3); - expect(getUnwrapped()).toEqual([ - [0, "foo", 0], - [1, "bar", 1], - [2, "baz", 2], - ]); - - const [a0, a1, a2] = mapped(); - - setList([ - { i: 2, v: "foo" }, - { i: 0, v: "bar" }, - { i: 1, v: "baz" }, - ]); - - expect(mapped().length).toBe(3); - expect(getUnwrapped()).toEqual([ - [2, "foo", 0], - [0, "bar", 1], - [1, "baz", 2], - ]); - - const [b0, b1, b2] = mapped(); - expect(a0).toBe(b1); - expect(a1).toBe(b2); - expect(a2).toBe(b0); - - dispose(); - })); - - test("key entries by prop name", () => - createRoot(dispose => { - const entriesFrom: [string, {}][] = [ - ["0", 0], - ["1", 1], - ["2", 2], - ["3", 3], - ]; - const entriesTo: [string, {}][] = [ - ["0", 0], - ["1", 1], - ["2", 2], - ]; - - const [list, setList] = createSignal<[string, {}][]>(entriesFrom); - const mapped = createMemo( - keyArray( - list, - v => v[0], - v => v()[1], - ), - ); - expect(mapped().length).toBe(4); - expect(mapped()).toEqual([0, 1, 2, 3]); - - setList(entriesTo); - expect(mapped().length).toBe(3); - expect(mapped()).toEqual([0, 1, 2]); - - dispose(); - })); -}); diff --git a/packages/keyed/test/index.test.tsx b/packages/keyed/test/index.test.tsx new file mode 100644 index 000000000..012ac2456 --- /dev/null +++ b/packages/keyed/test/index.test.tsx @@ -0,0 +1,443 @@ +import { createComputed, createMemo, createRoot, createSignal } from "solid-js"; +import { createStore } from "solid-js/store"; +import { update } from "@solid-primitives/utils/immutable"; +import { describe, expect, test } from "vitest"; +import { keyArray, MapEntries } from "../src/index.js"; +import { render } from "solid-js/web"; + +const el1 = { id: 1, value: "bread" }; +const el2 = { id: 2, value: "milk" }; +const el3 = { id: 3, value: "honey" }; +const el4 = { id: 4, value: "chips" }; + +describe("keyArray", () => { + test("maps and returns all initial items", () => + createRoot(dispose => { + const mapped = keyArray( + () => [el1, el2, el3], + v => v.id, + v => ({ ...v(), key: v().id }), + ); + expect(mapped().length).toBe(3); + expect(mapped()[0]!.key).toBe(1); + expect(mapped()[1]!.key).toBe(2); + expect(mapped()[2]!.key).toBe(3); + + dispose(); + })); + + test("cloning list should have no effect", () => + createRoot(dispose => { + const [list, setList] = createSignal([el1, el2, el3]); + let changes = 0; + const mapped = keyArray( + list, + v => v.id, + v => v().id, + ); + createComputed(() => mapped(), changes++); + expect(mapped()).toEqual([1, 2, 3]); + expect(changes).toBe(1); + + setList(p => p.slice()); + expect(mapped()).toEqual([1, 2, 3]); + expect(changes).toBe(1); + + dispose(); + })); + + test("mapFn is reactive", () => + createRoot(dispose => { + const [list, setList] = createSignal([el1, el2, el3]); + let changes = 0; + const mapped = keyArray( + list, + v => v.id, + v => { + const item = { value: v().value }; + createComputed(() => (item.value = v().value)); + return item; + }, + ); + createComputed(() => mapped(), changes++); + + expect(mapped()).toEqual([{ value: "bread" }, { value: "milk" }, { value: "honey" }]); + expect(changes).toBe(1); + + setList(p => update(p, 0, "value", "bananas")); + expect(mapped()).toEqual([{ value: "bananas" }, { value: "milk" }, { value: "honey" }]); + expect(changes).toBe(1); + + dispose(); + + setList(p => update(p, 1, "value", "orange juice")); + expect(mapped()).toEqual([{ value: "bananas" }, { value: "milk" }, { value: "honey" }]); + expect(changes).toBe(1); + expect(changes).toBe(1); + })); + + test("index is reactive", () => + createRoot(dispose => { + const [list, setList] = createSignal([el1, el2, el3]); + let changes = 0; + let maprun = 0; + const mapped = keyArray( + list, + v => v.id, + (v, i) => { + maprun++; + const item = { i: i(), v: v().value }; + createComputed(() => (item.i = i()), (item.v = v().value)); + return item; + }, + ); + createComputed(() => { + mapped(); + changes++; + }); + + expect(mapped()).toEqual([ + { i: 0, v: "bread" }, + { i: 1, v: "milk" }, + { i: 2, v: "honey" }, + ]); + expect(changes).toBe(1); + expect(maprun).toBe(3); + + setList([el1, el3, el2]); + expect(mapped()).toEqual([ + { i: 0, v: "bread" }, + { i: 1, v: "honey" }, + { i: 2, v: "milk" }, + ]); + expect(changes).toBe(2); + expect(maprun).toBe(3); + + setList([el1, el4, el3, el2]); + expect(mapped()).toEqual([ + { i: 0, v: "bread" }, + { i: 1, v: "chips" }, + { i: 2, v: "honey" }, + { i: 3, v: "milk" }, + ]); + expect(changes).toBe(3); + expect(maprun).toBe(4); + + dispose(); + })); + + test("supports top-level store arrays", () => + createRoot(dispose => { + const [list, setList] = createStore([ + { i: 0, v: "foo" }, + { i: 1, v: "bar" }, + { i: 2, v: "baz" }, + ]); + + const mapped = keyArray( + () => list, + e => e.i, + (item, index) => [item, index] as const, + ); + + const getUnwrapped = (): [number, string, number][] => + mapped().map(([e, index]) => { + const { i, v } = e(); + return [i, v, index()]; + }); + + expect(mapped().length).toBe(3); + expect(getUnwrapped()).toEqual([ + [0, "foo", 0], + [1, "bar", 1], + [2, "baz", 2], + ]); + + const [a0, a1, a2] = mapped(); + + setList([ + { i: 2, v: "foo" }, + { i: 0, v: "bar" }, + { i: 1, v: "baz" }, + ]); + + expect(mapped().length).toBe(3); + expect(getUnwrapped()).toEqual([ + [2, "foo", 0], + [0, "bar", 1], + [1, "baz", 2], + ]); + + const [b0, b1, b2] = mapped(); + expect(a0).toBe(b1); + expect(a1).toBe(b2); + expect(a2).toBe(b0); + + dispose(); + })); + + test("key entries by prop name", () => + createRoot(dispose => { + const entriesFrom: [string, {}][] = [ + ["0", 0], + ["1", 1], + ["2", 2], + ["3", 3], + ]; + const entriesTo: [string, {}][] = [ + ["0", 0], + ["1", 1], + ["2", 2], + ]; + + const [list, setList] = createSignal<[string, {}][]>(entriesFrom); + const mapped = createMemo( + keyArray( + list, + v => v[0], + v => v()[1], + ), + ); + expect(mapped().length).toBe(4); + expect(mapped()).toEqual([0, 1, 2, 3]); + + setList(entriesTo); + expect(mapped().length).toBe(3); + expect(mapped()).toEqual([0, 1, 2]); + + dispose(); + })); +}); + +describe("MapEntries", () => { + test("simple", () => { + const container = document.createElement("div"); + document.body.appendChild(container); + + const startingMap = new Map([ + [1, "1"], + [2, "2"], + [3, "3"], + ]); + const [s] = createSignal(startingMap); + const unmount = render( + () => ( + + {(k, v, i) => ( +
+ {i()}. {k}: {v()} +
+ )} +
+ ), + container, + ); + + container.childNodes.forEach((v, i) => { + const k = Array.from(startingMap.keys())[i]!; + expect(v.textContent).toEqual(`${i}. ${k}: ${startingMap.get(k)}`); + }); + + unmount(); + document.body.removeChild(container); + }); + + test("doesn't change for same values", () => { + const container = document.createElement("div"); + document.body.appendChild(container); + + const startingMap = new Map([ + [1, "1"], + [2, "2"], + [3, "3"], + ]); + const [s, set] = createSignal(startingMap); + const unmount = render( + () => ( + + {(k, v, i) => ( +
+ {i()}. {k}: {v()} +
+ )} +
+ ), + container, + ); + + const oldMapped: ChildNode[] = new Array(container.childNodes.length); + container.childNodes.forEach((v, i) => { + const k = Array.from(startingMap.keys())[i]!; + expect(v.textContent).toEqual(`${i}. ${k}: ${startingMap.get(k)}`); + oldMapped[k] = v; + }); + + set(new Map(startingMap)); + + container.childNodes.forEach((v, i) => { + const k = Array.from(startingMap.keys())[i]!; + expect(v.textContent).toEqual(`${i}. ${k}: ${startingMap.get(k)}`); + expect(oldMapped[k]).toBe(v); + }); + + unmount(); + document.body.removeChild(container); + }); + + test("changes value of elements", () => { + const container = document.createElement("div"); + document.body.appendChild(container); + + const startingMap = new Map([ + [1, "1"], + [2, "2"], + [3, "3"], + ]); + const [s, set] = createSignal(startingMap); + const unmount = render( + () => ( + + {(k, v, i) => ( +
+ {i()}. {k}: {v()} +
+ )} +
+ ), + container, + ); + + const oldMapped: ChildNode[] = new Array(container.childNodes.length); + container.childNodes.forEach((v, i) => { + const k = Array.from(startingMap.keys())[i]!; + expect(v.textContent).toEqual(`${i}. ${k}: ${startingMap.get(k)}`); + oldMapped[k] = v; + }); + + const nextMap = new Map([ + [1, "1"], + [2, "2?!"], + [3, "3"], + ]); + set(nextMap); + + container.childNodes.forEach((v, i) => { + const k = Array.from(nextMap.keys())[i]!; + expect(v.textContent).toEqual(`${i}. ${k}: ${nextMap.get(k)}`); + expect(oldMapped[k]).toBe(v); + }); + + unmount(); + document.body.removeChild(container); + }); + + test("creates new elements", () => { + const container = document.createElement("div"); + document.body.appendChild(container); + + const startingMap = new Map([ + [1, "1"], + [2, "2"], + [3, "3"], + ]); + const [s, set] = createSignal(startingMap); + const unmount = render( + () => ( + + {(k, v, i) => ( +
+ {i()}. {k}: {v()} +
+ )} +
+ ), + container, + ); + + const oldMapped: ChildNode[] = new Array(container.childNodes.length); + container.childNodes.forEach((v, i) => { + const k = Array.from(startingMap.keys())[i]!; + expect(v.textContent).toEqual(`${i}. ${k}: ${startingMap.get(k)}`); + oldMapped[k] = v; + }); + + const nextMap = new Map([ + [1, "1"], + [2, "2"], + [3, "3"], + [4, "4"], + [5, "5"], + ]); + set(nextMap); + + const newMapped: ChildNode[] = new Array(container.childNodes.length); + container.childNodes.forEach((v, i) => { + const k = Array.from(nextMap.keys())[i]!; + expect(v.textContent).toEqual(`${i}. ${k}: ${nextMap.get(k)}`); + newMapped[k] = v; + }); + + expect(oldMapped[0]).toBe(newMapped[0]); + expect(oldMapped[1]).toBe(newMapped[1]); + expect(oldMapped[2]).toBe(newMapped[2]); + expect(oldMapped[3]).toBe(newMapped[3]); + expect(oldMapped.includes(newMapped[4]!)).toEqual(false); + expect(oldMapped.includes(newMapped[5]!)).toEqual(false); + + unmount(); + document.body.removeChild(container); + }); + + test("deletes unused elements", () => { + const container = document.createElement("div"); + document.body.appendChild(container); + + const startingMap = new Map([ + [0, "0"], + [1, "1"], + [2, "2"], + [3, "3"], + ]); + const [s, set] = createSignal(startingMap); + const unmount = render( + () => ( + + {(k, v, i) => ( +
+ {i()}. {k}: {v()} +
+ )} +
+ ), + container, + ); + + const oldMapped: ChildNode[] = new Array(container.childNodes.length); + container.childNodes.forEach((v, i) => { + const k = Array.from(startingMap.keys())[i]!; + expect(v.textContent).toEqual(`${i}. ${k}: ${startingMap.get(k)}`); + oldMapped[k] = v; + }); + + const nextMap = new Map([ + [0, "0"], + [3, "3"], + ]); + set(nextMap); + + const newMapped: ChildNode[] = new Array(container.childNodes.length); + container.childNodes.forEach((v, i) => { + const k = Array.from(nextMap.keys())[i]!; + expect(v.textContent).toEqual(`${i}. ${k}: ${nextMap.get(k)}`); + newMapped[k] = v; + }); + + expect(oldMapped[0]).toBe(newMapped[0]); + expect(oldMapped[3]).toBe(newMapped[3]); + expect(newMapped.includes(oldMapped[1]!)).toEqual(false); + expect(newMapped.includes(oldMapped[2]!)).toEqual(false); + + unmount(); + document.body.removeChild(container); + }); +}); diff --git a/packages/map/README.md b/packages/map/README.md index 0f36a3557..42e2cdf3c 100644 --- a/packages/map/README.md +++ b/packages/map/README.md @@ -7,7 +7,7 @@ [![turborepo](https://img.shields.io/badge/built%20with-turborepo-cc00ff.svg?style=for-the-badge&logo=turborepo)](https://turborepo.org/) [![size](https://img.shields.io/bundlephobia/minzip/@solid-primitives/map?style=for-the-badge&label=size)](https://bundlephobia.com/package/@solid-primitives/map) [![version](https://img.shields.io/npm/v/@solid-primitives/map?style=for-the-badge)](https://www.npmjs.com/package/@solid-primitives/map) -[![stage](https://img.shields.io/endpoint?style=for-the-badge&url=https%3A%2F%2Fraw.githubusercontent.com%2Fsolidjs-community%2Fsolid-primitives%2Fmain%2Fasmaps%2Fbadges%2Fstage-2.json)](https://github.com/solidjs-community/solid-primitives#contribution-process) +[![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) The reactive versions of `Map` & `WeakMap` built-in data structures.