Skip to content

Instantly share code, notes, and snippets.

@developit
Last active December 18, 2023 14:18
Show Gist options
  • Save developit/af2a4488de152a84bff83e035bb8afc1 to your computer and use it in GitHub Desktop.
Save developit/af2a4488de152a84bff83e035bb8afc1 to your computer and use it in GitHub Desktop.

signal.map() / <For each={signal}>

Reactive iteration and conditionals for @preact/signals.

By default, @preact/signals assumes you will use standard Virtual DOM rendering for conditions and loops, and does not provide a reactive primitive for these constructs.

Fortunately, Signal.prototype is just an import away. We can easily add our own reactive primitive and hang it off of Signal, or provide it as a Component. We can also provide implementation's of Solid's <For each={signal}>{item => ...}</For> and <Show when={signal}>...</Show> using the same technique.

import { render } from 'preact';
import { useSignal, signal } from '@preact/signals';
import { For } from './signal-map.js';

const item = (text, done = false) => ({ text: signal(text), done: signal(done) });

const items = signal([item('a'), item('b'), item('c')]);

function App() {
  const text = useSignal('');

  function add(e) {
    items.value = items.value.concat(item(text.value));
    text.value = '';
    e.preventDefault();
  }

  return (
    <form onSubmit={add}>
      <input onInput={e => text.value = e.target.value} />
      <ul>
        <For each={items}>
          {item => (
            <li style={item.done.value ? 'text-decoration:line-through':''}>
              <input type="checkbox" checked={item.done.value} onInput={() => item.done.value = !item.done.value} />
              {' '}
              {item.text}
            </li>
          )}
        </For>
      </ul>
    </form>
  );
}

Longer example, this time using Signal.prototype.map():

let update = 0;

function Counter() {
  const letters = useSignal("ABDEFGHIJKLMNOP");

  const position = useSignal(0);

  const chars = useComputed(() => ([...letters.value]));

  return <>
    <input type="text" value={letters} onInput={e => letters.value = e.currentTarget.value}/>
    <input type="number" value={position} onInput={e => position.value = +e.currentTarget.value}/>
    <ul>
      {chars.map((char, index) => (
        // you can use hooks in the map() callback, since it's actually a component:
        <li>{char} ({update++}) {useComputed(() => index === position.value ? "active" : null)}</li>
      ))}
    </ul>
  </>
}

View Demo on the Preact REPL

import { h } from 'preact';
import { useMemo } from 'preact/hooks';
import { Signal } from '@preact/signals';
Signal.prototype.map = function(fn) { return <For each={this} children={fn} /> };
export function For({ each, children: f, fallback }) {
let c = useMemo(() => new Map, []);
let value = each.value;
//if (!Array.isArray(value)) return <Item v={value} f={f} />;
return value?.map((v, k, x) => c.get(v) || (c.set(v, x = <Item {...{ key: v, v, k, f }} />), x)) ?? fallback;
}
const Item = ({ v, k, f }) => f(v, k);
export function Show({ when, fallback, children: f }) {
let v = when.value;
return v ? typeof f === 'function' ? <Item v={v} f={f} /> : f : fallback;
}
@saolof
Copy link

saolof commented Feb 14, 2023

This was very useful. Should the let value = each.value maybe check if each is a signal and otherwise just accept static values? I often find myself wanting to pass a normal (often static) list to For.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment