Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add MapEntries control flow component. #728

Merged
merged 13 commits into from
Jan 5, 2025
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
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.
AlexErrant marked this conversation as resolved.
Show resolved Hide resolved

`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
thetarnav marked this conversation as resolved.
Show resolved Hide resolved

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
thetarnav marked this conversation as resolved.
Show resolved Hide resolved

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
Loading