Skip to content

Commit

Permalink
Add MapEntries control flow component. (#728)
Browse files Browse the repository at this point in the history
  • Loading branch information
thetarnav authored Jan 5, 2025
2 parents 9c742e7 + 533c821 commit 6026ab2
Show file tree
Hide file tree
Showing 12 changed files with 748 additions and 213 deletions.
5 changes: 5 additions & 0 deletions .changeset/sharp-beers-approve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@solid-primitives/keyed": minor
---

Add `MapEntries` control flow component.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ The goal of Solid Primitives is to wrap client and server side functionality to
|<h4>*Control Flow*</h4>|
|[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)<br />[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)<br />[createToken](https://github.com/solidjs-community/solid-primitives/tree/main/packages/jsx-tokenizer#createtoken)<br />[resolveTokens](https://github.com/solidjs-community/solid-primitives/tree/main/packages/jsx-tokenizer#resolvetokens)<br />[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)<br />[Key](https://github.com/solidjs-community/solid-primitives/tree/main/packages/keyed#key)<br />[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)<br />[Key](https://github.com/solidjs-community/solid-primitives/tree/main/packages/keyed#key)<br />[Entries](https://github.com/solidjs-community/solid-primitives/tree/main/packages/keyed#entries)<br />[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)<br />[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)<br />[mapRange](https://github.com/solidjs-community/solid-primitives/tree/main/packages/range#maprange)<br />[indexRange](https://github.com/solidjs-community/solid-primitives/tree/main/packages/range#indexrange)<br />[Repeat](https://github.com/solidjs-community/solid-primitives/tree/main/packages/range#repeat)<br />[Range](https://github.com/solidjs-community/solid-primitives/tree/main/packages/range#range)<br />[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)<br />[resolveElements](https://github.com/solidjs-community/solid-primitives/tree/main/packages/refs#resolveelements)<br />[resolveFirst](https://github.com/solidjs-community/solid-primitives/tree/main/packages/refs#resolvefirst)<br />[Ref](https://github.com/solidjs-community/solid-primitives/tree/main/packages/refs#ref)<br />[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)|
Expand Down
37 changes: 37 additions & 0 deletions packages/keyed/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<Key>` 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
Expand Down Expand Up @@ -152,6 +153,42 @@ Third argument of the map function is an index signal.
</Entries>
```

## `<MapEntries>`

Creates a list of elements by mapping Map entries. Similar to Solid's `<For>` and `<Index>`, 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());

<MapEntries of={map()} fallback={<div>No items</div>}>
{(key, value) => (
<div>
{key}: {value()}
</div>
)}
</MapEntries>;
```

### 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
<MapEntries of={map()} fallback={<div>No items</div>}>
{(key, value, index) => (
<div data-index={index()}>
{key}: {value()}
</div>
)}
</MapEntries>
```

## `<Rerun>`

Causes the children to rerender when the `on` key changes. Equivalent of `v-key` in vue, and `{#key}` in svelte.
Expand Down
95 changes: 95 additions & 0 deletions packages/keyed/dev/entries.tsx
Original file line number Diff line number Diff line change
@@ -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, string>): string => {
const keys = Object.keys(record);
return keys[randomIndex(keys)]!;
};

export default function App() {
const [store, setStore] = createStore<Record<string, string>>({
[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 (
<>
<div class="wrapper-h">
<button class="btn" onclick={addRandom}>
Add
</button>
<button class="btn" onclick={removeRandom}>
Remove
</button>
<button class="btn" onclick={changeRandomValue}>
Change value
</button>
<button class="btn" onclick={clone}>
Clone
</button>
</div>
<div class="wrapper-h flex-wrap">
<TransitionGroup name="fade">
<Entries of={store} fallback={<p class="bg-yellow-500 p-1 transition-all">No items.</p>}>
{(key, value, index) => {
createEffect(() => {
console.log("Effect:", key, value());
});
return (
<div class="node relative transition-all duration-500">
{index()}. {value()}
<div class="bg-dark-500 text-light-900 absolute -bottom-2 left-2 px-1 text-[9px]">
ID: {key}
</div>
</div>
);
}}
</Entries>
</TransitionGroup>
</div>
</>
);
}
15 changes: 14 additions & 1 deletion packages/keyed/dev/index.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div class="box-border min-h-screen w-full space-y-4 overflow-hidden bg-gray-800 p-24 text-white">
<Key />
<div class="wrapper-v">
<h4>Key</h4>
<Key />
</div>
<div class="wrapper-v">
<h4>Entries</h4>
<Entries />
</div>
<div class="wrapper-v">
<h4>MapEntries</h4>
<MapEntries />
</div>
</div>
);
};
Expand Down
2 changes: 2 additions & 0 deletions packages/keyed/dev/key.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
110 changes: 110 additions & 0 deletions packages/keyed/dev/mapEntries.tsx
Original file line number Diff line number Diff line change
@@ -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, string>): 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 (
<>
<div class="wrapper-h">
<button class="btn" onclick={addRandom}>
Add
</button>
<button class="btn" onclick={removeRandom}>
Remove
</button>
<button class="btn" onclick={changeRandomValue}>
Change value
</button>
<button class="btn" onclick={clone}>
Clone
</button>
</div>
<div class="wrapper-h flex-wrap">
<TransitionGroup name="fade">
<MapEntries
of={map()}
fallback={<p class="bg-yellow-500 p-1 transition-all">No items.</p>}
>
{(key, value, index) => {
createEffect(() => {
console.log("Effect:", key, value());
});
return (
<div class="node relative transition-all duration-500">
{index()}. {value()}
<div class="bg-dark-500 text-light-900 absolute -bottom-2 left-2 px-1 text-[9px]">
ID: {key}
</div>
</div>
);
}}
</MapEntries>
</TransitionGroup>
</div>
</>
);
}
3 changes: 2 additions & 1 deletion packages/keyed/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
"list": [
"keyArray",
"Key",
"Entries"
"Entries",
"MapEntries"
],
"category": "Control Flow"
},
Expand Down
38 changes: 38 additions & 0 deletions packages/keyed/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ export function Entries<K extends string | number, V>(props: {
i: Accessor<number>,
) => 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(
Expand All @@ -201,6 +202,43 @@ export function Entries<K extends string | number, V>(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
* <MapEntries of={map()} fallback={<div>No items</div>}>
* {(key, value, index) => <div data-index={index()}>{key}: {value()}</div>}
* </MapEntries>
* ```
*
* @see https://github.com/solidjs-community/solid-primitives/tree/main/packages/keyed#mapentries
*/
export function MapEntries<K, V>(props: {
of: Map<K, V> | undefined | null | false;
fallback?: JSX.Element;
children: (key: K, v: Accessor<V>, i: Accessor<number>) => 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<V>) => JSX.Element)(
key,
() => (props.of as Map<K, V>).get(key)!,
)
: (key, i) => mapFn(key, () => (props.of as Map<K, V>).get(key)!, i),
"fallback" in props ? { fallback: () => props.fallback } : undefined,
),
) as unknown as JSX.Element;
}

export type RerunChildren<T> = ((input: T, prevInput: T | undefined) => JSX.Element) | JSX.Element;

/**
Expand Down
Loading

0 comments on commit 6026ab2

Please sign in to comment.