From a52514e11fcc83df97d437b8e524f0f255028b5c Mon Sep 17 00:00:00 2001 From: jdecroock Date: Sat, 4 Oct 2025 11:58:29 +0200 Subject: [PATCH 1/3] Add new devtools demo --- docs/demos/devtools.tsx | 141 ++++++++++++++++++++++++++++++++++++++++ docs/demos/index.tsx | 1 + docs/demos/utils.ts | 71 ++++++++++++++++++++ 3 files changed, 213 insertions(+) create mode 100644 docs/demos/devtools.tsx create mode 100644 docs/demos/utils.ts 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! 🎉

+
+
    + + {todo => } + +
+
+ ); +} + +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; + }, +}; From 0d029de628b6c481c4dca4293bf4b455565a4335 Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Sun, 5 Oct 2025 13:53:46 +0200 Subject: [PATCH 2/3] Address feedback --- docs/demos/devtools.css | 13 ++++++++ docs/demos/devtools.tsx | 21 +++--------- docs/demos/utils.ts | 71 ----------------------------------------- docs/vite.config.ts | 7 +++- 4 files changed, 23 insertions(+), 89 deletions(-) create mode 100644 docs/demos/devtools.css delete mode 100644 docs/demos/utils.ts diff --git a/docs/demos/devtools.css b/docs/demos/devtools.css new file mode 100644 index 000000000..42c3fc926 --- /dev/null +++ b/docs/demos/devtools.css @@ -0,0 +1,13 @@ +.new-todo { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 8px; +} + +.todo-item { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 4px; +} diff --git a/docs/demos/devtools.tsx b/docs/demos/devtools.tsx index b6d894fe2..2ec6529fa 100644 --- a/docs/demos/devtools.tsx +++ b/docs/demos/devtools.tsx @@ -1,6 +1,7 @@ import { useSignal } from "@preact/signals"; -import { Show, For } from "./utils"; +import { Show, For } from "@preact/signals/utils"; import { computed, signal } from "@preact/signals-core"; +import "./devtools.css"; type TodoModel = { id: number; @@ -71,14 +72,7 @@ function TodosList() { return (

    Todos

    -
    +