Skip to content
Open
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
11 changes: 11 additions & 0 deletions .changeset/live-state-editor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@solidjs/start-devtools': minor
---

Add live state and store editing to the dev toolbar.

Signals and stores can now be inspected and edited live on-the-fly directly from the devtools:

- In the Reactivity Graph detail pane, primitive values and nested properties of selected signals and store slots can be edited inline.
- In the Ownership Tree detail pane, component signals feature inline editing with immediate updates to dependent memos, effects, and DOM elements.
- Boolean values support 1-click toggling, while strings, numbers, null, undefined, and JSON objects/arrays are parsed and written through Solid's internal reactivity scheduler.
20 changes: 20 additions & 0 deletions src/dev-toolbar/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -760,3 +760,23 @@ export function InspectIcon(props: JSX.IntrinsicElements['svg'] & { title: strin
</svg>
);
}

export function EditIcon(props: JSX.IntrinsicElements['svg'] & { title: string }): JSX.Element {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
{...props}
>
<title>{props.title}</title>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.6"
d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125"
/>
</svg>
);
}
185 changes: 156 additions & 29 deletions src/dev-toolbar/ownership/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,17 @@ import { Badge } from '../../ui/Badge.js';
import IconButton from '../../ui/IconButton.js';
import Placeholder from '../../ui/Placeholder.js';
import { Text } from '../../ui/Text.js';
import { CollapseIcon, ExpandIcon, EyeIcon, PauseIcon, PlayIcon } from '../icons.js';
import { CollapseIcon, EditIcon, ExpandIcon, EyeIcon, PauseIcon, PlayIcon } from '../icons.js';
import { openInEditor } from '../open-in-editor.js';
import { CopyReportButton } from '../report/CopyReportButton.js';
import { pageRoute } from '../report/markdown.js';
import { ownerReport } from '../report/owner-report.js';
import {
parseValueInput,
writeReactiveValue,
formatDraftValue,
} from '../reactivity/value-editor.js';
import { ValueInspector } from '../reactivity/ValueInspector.js';
import { previewValue, typeName } from './format.js';
import {
excludeOwner,
Expand Down Expand Up @@ -54,6 +60,151 @@ export interface OwnershipViewerProps {
focus?: { owner: object };
}

interface OwnershipSignalRowProps {
signal: { name: string; value: unknown; node: object };
onViewInGraph?: (node: object) => void;
}

function OwnershipSignalRow(props: OwnershipSignalRowProps): JSX.Element {
const [editing, setEditing] = createSignal(false);
const [expanded, setExpanded] = createSignal(false);
const [draft, setDraft] = createSignal(formatDraftValue(props.signal.value));

const isComplex = createMemo(
() => typeof props.signal.value === 'object' && props.signal.value !== null,
);

function startEditing(): void {
setDraft(formatDraftValue(props.signal.value));
setEditing(true);
}

function commit(): void {
if (!editing()) return;
setEditing(false);
const parsed = parseValueInput(draft(), props.signal.value);
if (parsed !== props.signal.value) {
writeReactiveValue(props.signal.node, [], parsed);
}
}

return (
<div data-solid-ownership-signal-block>
<div data-solid-ownership-signal>
<span data-solid-ownership-signal-name>
<Show when={isComplex()}>
<button
type="button"
data-solid-ownership-signal-chevron
data-expanded={expanded() ? 'true' : undefined}
onClick={() => setExpanded((v) => !v)}
aria-label={expanded() ? 'Collapse value' : 'Expand value'}
/>
</Show>
<Text
options={{
size: 'xs',
weight: 'semibold',
font: 'mono',
wrap: 'nowrap',
}}
>
{props.signal.name}
</Text>
</span>
<Show
when={editing()}
fallback={
<>
<Show
when={typeof props.signal.value === 'boolean'}
fallback={
<Text
data-solid-ownership-signal-value
options={{ size: 'xs', font: 'mono' }}
onClick={() => {
if (isComplex()) setExpanded((v) => !v);
}}
onDblClick={startEditing}
title={
isComplex()
? 'Click to expand / double-click to edit JSON'
: 'Double-click to edit'
}
>
{previewValue(props.signal.value)}
</Text>
}
>
<button
type="button"
data-solid-ownership-toggle-btn
data-value={props.signal.value ? 'true' : 'false'}
onClick={() => writeReactiveValue(props.signal.node, [], !props.signal.value)}
title="Click to toggle boolean"
>
<Text options={{ size: 'xs', font: 'mono' }}>{`${props.signal.value}`}</Text>
</button>
</Show>
<div data-solid-ownership-signal-actions>
<button
type="button"
data-solid-ownership-edit-signal
aria-label={`Edit ${props.signal.name}`}
title={`Edit ${props.signal.name}`}
onClick={startEditing}
>
<EditIcon title={`Edit ${props.signal.name}`} />
</button>
<Show when={props.onViewInGraph}>
<button
type="button"
data-solid-ownership-view-graph
aria-label="View in graph"
onClick={() => props.onViewInGraph?.(props.signal.node)}
>
<EyeIcon title="View in graph" />
</button>
</Show>
</div>
</>
}
>
<input
type="text"
data-solid-ownership-signal-input
value={draft()}
onInput={(e) => setDraft(e.currentTarget.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') commit();
if (e.key === 'Escape') setEditing(false);
}}
onBlur={commit}
ref={(el) => {
queueMicrotask(() => {
el.focus();
el.select();
});
}}
/>
</Show>
</div>
<Show when={isComplex() && expanded()}>
<div data-solid-ownership-signal-tree>
<ValueInspector
value={props.signal.value}
open
editable
onEdit={(path, nextValue) => {
writeReactiveValue(props.signal.node, path, nextValue);
}}
/>
</div>
</Show>
</div>
);
}

export default function OwnershipViewer(props: OwnershipViewerProps): JSX.Element {
// The panel renders inside the app it inspects, so its own scope is marked.
excludeOwner(getOwner());
Expand Down Expand Up @@ -433,34 +584,10 @@ export default function OwnershipViewer(props: OwnershipViewerProps): JSX.Elemen
}
>
{(signal) => (
<div data-solid-ownership-signal>
<Text
options={{
size: 'xs',
weight: 'semibold',
font: 'mono',
wrap: 'nowrap',
}}
>
{signal.name}
</Text>
<Text
data-solid-ownership-signal-value
options={{ size: 'xs', font: 'mono' }}
>
{previewValue(signal.value)}
</Text>
<Show when={props.onViewInGraph}>
<button
type="button"
data-solid-ownership-view-graph
aria-label="View in graph"
onClick={() => props.onViewInGraph?.(signal.node)}
>
<EyeIcon title="View in graph" />
</button>
</Show>
</div>
<OwnershipSignalRow
signal={signal}
onViewInGraph={props.onViewInGraph}
/>
)}
</For>
</div>
Expand Down
Loading
Loading