diff --git a/docs/demos/devtools.tsx b/docs/demos/devtools.tsx new file mode 100644 index 000000000..b6d894fe2 --- /dev/null +++ b/docs/demos/devtools.tsx @@ -0,0 +1,141 @@ +import { useSignal } from "@preact/signals"; +import { Show, For } from "./utils"; +import { computed, signal } from "@preact/signals-core"; + +type TodoModel = { + id: number; + get text(): string; + get done(): boolean; + toggle(): void; + updateText(newText: string): void; +}; + +const createTodoModel = (id: number, input: string): TodoModel => { + const text = signal(input, { name: `todo-${id}-text` }); + const done = signal(false, { name: `todo-${id}-done` }); + + return { + id, + get text() { + return text.value; + }, + get done() { + return done.value; + }, + toggle() { + done.value = !done.value; + }, + updateText(newText: string) { + text.value = newText; + }, + }; +}; + +const todosModel = (() => { + const todos = signal( + [ + createTodoModel(1, "Learn Preact Signals"), + createTodoModel(2, "Build something fun"), + ], + { name: "todos-list" } + ); + + const allDone = computed( + () => todos.value.length > 0 && todos.value.every(t => t.done), + { name: "all-done" } + ); + + return { + todos, + allDone, + add(text: string) { + todos.value = [...todos.value, createTodoModel(Date.now(), text)]; + }, + }; +})(); + +export default function DevToolsDemo() { + return ( +
+

DevTools Demo

+
+ +
+
+ ); +} + +function TodosList() { + const newTodoText = useSignal("", { name: "new-todo-text" }); + + return ( +
+

Todos

+
+ + +
+ +

All todos are done! 🎉

+
+ +
+ ); +} + +function TodoItem({ todo }: { todo: TodoModel }) { + const isEditing = useSignal(false, { name: `todo-${todo.id}-isEditing` }); + return ( +
  • + todo.toggle()} + /> + {todo.text}

    }> + todo.updateText(e.currentTarget.value)} + /> +
    + +
  • + ); +} diff --git a/docs/demos/index.tsx b/docs/demos/index.tsx index 7332f81e5..63417c3e9 100644 --- a/docs/demos/index.tsx +++ b/docs/demos/index.tsx @@ -13,6 +13,7 @@ const demos = { Sum, GlobalCounter, DuelingCounters, + Devtools: lazy(() => import("./devtools")), Nesting: lazy(() => import("./nesting")), Animation: lazy(() => import("./animation")), Bench: lazy(() => import("./bench")), diff --git a/docs/demos/utils.ts b/docs/demos/utils.ts new file mode 100644 index 000000000..785d447b9 --- /dev/null +++ b/docs/demos/utils.ts @@ -0,0 +1,71 @@ +import { ReadonlySignal, Signal } from "@preact/signals-core"; +import { useSignal } from "@preact/signals"; +import { Fragment, createElement, JSX } from "preact"; +import { useMemo } from "preact/hooks"; + +interface ShowProps { + when: Signal | ReadonlySignal; + fallback?: JSX.Element; + children: JSX.Element | ((value: NonNullable) => JSX.Element); +} + +export function Show(props: ShowProps): JSX.Element | null { + const value = props.when.value; + if (!value) return props.fallback || null; + return typeof props.children === "function" + ? props.children(value) + : props.children; +} + +interface ForProps { + each: + | Signal> + | ReadonlySignal> + | (() => Signal> | ReadonlySignal>); + fallback?: JSX.Element; + children: (value: T, index: number) => JSX.Element; +} + +export function For(props: ForProps): JSX.Element | null { + const cache = useMemo(() => new Map(), []); + let list = ( + (typeof props.each === "function" ? props.each() : props.each) as Signal< + Array + > + ).value; + + if (!list.length) return props.fallback || null; + + const items = list.map((value, key) => { + if (!cache.has(value)) { + cache.set(value, props.children(value, key)); + } + return cache.get(value); + }); + + return createElement(Fragment, null, items); +} + +export function useLiveSignal( + value: Signal | ReadonlySignal +): Signal | ReadonlySignal> { + const s = useSignal(value); + if (s.peek() !== value) s.value = value; + return s; +} + +export function useSignalRef(value: T): Signal & { current: T } { + const ref = useSignal(value) as Signal & { current: T }; + if (!("current" in ref)) + Object.defineProperty(ref, "current", refSignalProto); + return ref; +} +const refSignalProto = { + configurable: true, + get(this: Signal) { + return this.value; + }, + set(this: Signal, v: any) { + this.value = v; + }, +};