# Edit nodes

Use an external inspector when your application needs to edit a node without putting every control inside the canvas. The examples below render a fixed-size node and a content-sized node, then let you edit a target by `nodeId`, add and delete nodes, and undo committed payload changes.

Specs describe intent; live models hold data. Use the mounted [`DiagramInstance`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-instance-diagraminstance#diagraminstance) to get the model and engine. Loading and editing are different intents: tracked model setters update live data, while commands put user-facing changes on the history stack.

## 1. Share the data and inspector

Create `inspector.ts` in your browser application's source directory. The framework examples in the next step import this file.

The data uses [`NodeSpec`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-instance-nodespec#nodespec) and [`EdgeSpec`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-instance-edgespec#edgespec). `fixed` starts at 200 × 80; `auto` starts at 60 × 36 and fits its longer label through `metadata.sizing.auto`.

The inspector uses the mounted [`DiagramEngine`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-engine-engine#diagramengine). `addNode()` returns the live [`NodeModel`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-engine-models-nodemodel#nodemodel) and executes the shipped [`AddNodeCommand`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-engine-commands-classes-a-r#addnodecommand); `removeNode()` executes [`RemoveNodeCommand`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-engine-commands-classes-a-r#removenodecommand). You do not need to construct those commands yourself.

For payload commits, execute the shipped [`SetNodeDataCommand`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-engine-commands-classes-r-u#setnodedatacommand). This is the command surface for an inspector edit: it changes only the supplied data keys and restores those keys on undo.

```ts title="inspector.ts"
import { NodeModel, SetNodeDataCommand, type DiagramEngine } from '@grafloria/engine';
import type { NodeSpec, EdgeSpec } from '@grafloria/renderer';

export const initialNodes: NodeSpec[] = [
  {
    id: 'fixed', position: { x: 80, y: 80 },
    size: { width: 200, height: 80 }, label: 'Draft',
    data: { note: 'Review pending' },
  },
  {
    id: 'auto', position: { x: 80, y: 230 },
    size: { width: 60, height: 36 },
    label: 'This node fits its longer label',
    metadata: { sizing: { auto: true, padding: 10 } },
    data: { note: 'Content-sized' },
  },
];
export const initialEdges: EdgeSpec[] = [
  { id: 'connection', source: 'fixed', target: 'auto' },
];

let nextNodeId = 0;

export function mountInspector(
  host: HTMLElement,
  engine: DiagramEngine,
  repaint: () => void,
): () => void {
  engine.setInteractionConfig({ enableInPlaceTextEdit: true });
  const abort = new AbortController();
  const panel = document.createElement('div');
  panel.style.cssText = 'display:flex;flex-wrap:wrap;gap:12px;padding:12px;font:14px sans-serif';
  host.append(panel);

  function field(caption: string, value: string, type = 'text'): HTMLInputElement {
    const label = document.createElement('label');
    label.append(`${caption} `);
    const input = document.createElement('input');
    input.type = type;
    input.value = value;
    input.style.width = type === 'number' ? '70px' : '160px';
    label.append(input);
    panel.append(label);
    return input;
  }

  const target = field('nodeId', 'fixed');
  const label = field('Label (live)', 'Draft');
  const note = field('Note', 'Review pending');
  const width = field('Width', '200', 'number');
  const height = field('Height', '80', 'number');
  width.min = height.min = '1';
  const status = document.createElement('output');
  panel.append(status);

  function node(): NodeModel | undefined {
    return engine.getDiagram()?.getNode(target.value);
  }

  function refresh(): void {
    const current = node();
    if (!current) {
      status.textContent = 'Target not found';
      return;
    }
    label.value = current.getLabel() ?? '';
    note.value = typeof current.data.note === 'string' ? current.data.note : '';
    width.value = String(current.size.width);
    height.value = String(current.size.height);
    status.textContent = `Current note: ${note.value}`;
  }

  function button(caption: string, action: () => Promise<void>): void {
    const control = document.createElement('button');
    control.type = 'button';
    control.textContent = caption;
    control.addEventListener('click', () => {
      void action().then(() => {
        repaint();
        refresh();
      }).catch((error: Error) => { status.textContent = error.message; });
    }, { signal: abort.signal });
    panel.append(control);
  }

  target.addEventListener('input', refresh, { signal: abort.signal });
  label.addEventListener('input', () => {
    node()?.setMetadata('label', label.value);
    repaint();
  }, { signal: abort.signal });

  const resize = () => {
    const w = Number(width.value);
    const h = Number(height.value);
    if (Number.isFinite(w) && Number.isFinite(h) && w > 0 && h > 0) {
      node()?.setSize(w, h);
      repaint();
    }
  };
  width.addEventListener('input', resize, { signal: abort.signal });
  height.addEventListener('input', resize, { signal: abort.signal });

  button('Apply note', async () => {
    if (!node()) throw new Error('Choose an existing nodeId');
    await engine.commandManager.execute(
      new SetNodeDataCommand(target.value, { note: note.value }),
    );
  });
  button('Add', async () => {
    let id: string;
    do { id = `inspector-node-${++nextNodeId}`; }
    while (engine.getDiagram()?.getNode(id));
    const added = new NodeModel({
      id, type: 'rect',
      position: { x: 420, y: 80 }, size: { width: 180, height: 80 },
    });
    added.setMetadata('label', 'New node');
    const live = await engine.addNode(added);
    target.value = live.id;
  });
  button('Delete target', async () => {
    if (!node()) throw new Error('Choose an existing nodeId');
    await engine.removeNode(target.value);
  });
  button('Undo', async () => { await engine.undo(); });
  button('Redo', async () => { await engine.redo(); });
  refresh();

  return () => {
    abort.abort();
    panel.remove();
  };
}
```

The label and dimension fields are **live, non-history updates**. The Note field is a draft until you press **Apply note**; that button records one undoable payload edit. Add and delete also enter history. For undoable label editing, use the canvas's in-place editor described below rather than the live label field.

## 2. Mount the canvas in your framework

Install the packages for your framework in your own project.

JavaScript:

```bash
npm install @grafloria/element @grafloria/renderer @grafloria/engine
```

Angular:

```bash
npm install @grafloria/angular @grafloria/engine @grafloria/renderer @grafloria/element @angular/common @angular/core @angular/forms @angular/platform-browser rxjs
```

Qwik:

```bash
npm install @grafloria/qwik @grafloria/engine @grafloria/renderer @grafloria/element @builder.io/qwik
```

React:

```bash
npm install @grafloria/react @grafloria/engine @grafloria/renderer @grafloria/element react react-dom
```

Vue:

```bash
npm install @grafloria/vue @grafloria/engine @grafloria/renderer @grafloria/element vue
```

JavaScript mounts with [`render`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-element-core#render). Angular uses [`DiagramCanvasComponent`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-angular-components-diagramcanvascomponent#diagramcanvascomponent) and its `activeEngine()`. React's [`GrafloriaFlow`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-react#grafloriaflow), Vue's [`GrafloriaFlow`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-vue#grafloriaflow), and Qwik's [`GrafloriaFlow`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-qwik#grafloriaflow) hand you the instance through their initialization callback or event.

Each sample mounts the same inspector above a 400-pixel canvas. The React, Vue and Qwik samples use uncontrolled defaults so the live instance owns edits. Angular uses two-way node and edge bindings so model changes return to application state.

:::code-group
```ts title="JavaScript"
// main.ts — run in the browser; call the returned cleanup when removing this view.
import { render } from '@grafloria/element';
import { initialNodes, initialEdges, mountInspector } from './inspector';

export function mountNodeEditor(parent: HTMLElement): () => void {
  const inspector = document.createElement('div');
  const canvas = document.createElement('div');
  canvas.style.height = '400px';
  parent.append(inspector, canvas);
  const instance = render({ nodes: initialNodes, edges: initialEdges }, canvas);
  const removeInspector = mountInspector(
    inspector, instance.getEngine(), () => instance.renderNow(),
  );
  return () => {
    removeInspector();
    instance.dispose();
    inspector.remove();
    canvas.remove();
  };
}

const root = document.createElement('div');
document.body.append(root);
export const unmountNodeEditor = mountNodeEditor(root);
```
```ts title="Angular"
// node-editor.component.ts
import { AfterViewInit, Component, ElementRef, OnDestroy, viewChild } from '@angular/core';
import { DiagramCanvasComponent } from '@grafloria/angular';
import { initialNodes, initialEdges, mountInspector } from './inspector';

@Component({
  selector: 'app-node-editor',
  standalone: true,
  imports: [DiagramCanvasComponent],
  template: `
    <div #inspector></div>
    <grafloria-diagram-canvas
      [(nodes)]="nodes" [(edges)]="edges"
      style="display:block;height:400px" />
  `,
})
export class NodeEditorComponent implements AfterViewInit, OnDestroy {
  nodes: ReturnType<DiagramCanvasComponent['nodes']> = initialNodes;
  edges: ReturnType<DiagramCanvasComponent['edges']> = initialEdges;
  canvas = viewChild.required(DiagramCanvasComponent);
  inspector = viewChild.required<ElementRef<HTMLDivElement>>('inspector');
  private removeInspector?: () => void;

  ngAfterViewInit(): void {
    const canvas = this.canvas();
    const engine = canvas.activeEngine();
    if (!engine) return;
    this.removeInspector = mountInspector(
      this.inspector().nativeElement, engine, () => canvas.scheduleRender(),
    );
  }
  ngOnDestroy(): void { this.removeInspector?.(); }
}
```
```tsx title="Qwik"
// node-editor.tsx
import { component$, $, noSerialize, useSignal, useVisibleTask$, type NoSerialize } from '@builder.io/qwik';
import { GrafloriaFlow } from '@grafloria/qwik';
import type { DiagramInstance } from '@grafloria/renderer';
import { initialNodes, initialEdges, mountInspector } from './inspector';

export default component$(() => {
  const instance = useSignal<NoSerialize<DiagramInstance>>();
  const inspector = useSignal<HTMLDivElement>();

  useVisibleTask$(({ track, cleanup }) => {
    const api = track(() => instance.value);
    const host = inspector.value;
    if (!api || !host) return;
    cleanup(mountInspector(host, api.getEngine(), () => api.renderNow()));
  });

  return <>
    <div ref={inspector} />
    <div style={{ height: '400px' }}>
      <GrafloriaFlow defaultNodes={initialNodes} defaultEdges={initialEdges}
        onInit$={$((api: DiagramInstance) => { instance.value = noSerialize(api); })} />
    </div>
  </>;
});
```
```tsx title="React"
// NodeEditor.tsx
import { useEffect, useRef } from 'react';
import { GrafloriaFlow } from '@grafloria/react';
import type { DiagramInstance } from '@grafloria/renderer';
import { initialNodes, initialEdges, mountInspector } from './inspector';

export default function NodeEditor() {
  const inspector = useRef<HTMLDivElement>(null);
  const instance = useRef<DiagramInstance | null>(null);
  const removeInspector = useRef<(() => void) | null>(null);

  function onInit(api: DiagramInstance): void {
    instance.current = api;
    removeInspector.current?.();
    if (inspector.current) {
      removeInspector.current = mountInspector(
        inspector.current, api.getEngine(), () => api.renderNow(),
      );
    }
  }
  useEffect(() => () => { removeInspector.current?.(); }, []);

  return <>
    <div ref={inspector} />
    <div style={{ height: 400 }}>
      <GrafloriaFlow defaultNodes={initialNodes} defaultEdges={initialEdges} onInit={onInit} />
    </div>
  </>;
}
```
```vue title="Vue"
<!-- NodeEditor.vue -->
<script setup lang="ts">
import { shallowRef, onUnmounted } from 'vue';
import { GrafloriaFlow } from '@grafloria/vue';
import type { DiagramInstance } from '@grafloria/renderer';
import { initialNodes, initialEdges, mountInspector } from './inspector';

const inspector = shallowRef<HTMLDivElement>();
const instance = shallowRef<DiagramInstance>();
let removeInspector: (() => void) | undefined;
function onInit(api: DiagramInstance): void {
  instance.value = api;
  removeInspector?.();
  if (inspector.value) {
    removeInspector = mountInspector(
      inspector.value, api.getEngine(), () => api.renderNow(),
    );
  }
}
onUnmounted(() => { removeInspector?.(); });
</script>

<template>
  <div ref="inspector"></div>
  <div style="height:400px">
    <GrafloriaFlow :default-nodes="initialNodes" :default-edges="initialEdges" @init="onInit" />
  </div>
</template>
```
:::

## 3. Edit the target and test history

The JavaScript view starts with the inspector targeting Draft and an edge leading to the wider content-sized node.

![JavaScript inspector above Draft and the connected content-sized node.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/c85f76f3cbe887b58b082a4e7e2d97dc.png)

Angular, Qwik, React and Vue render the same initial nodes and the inspector's Apply note, Add, Delete target, Undo and Redo controls.

![Angular inspector targeting fixed, with the two connected nodes below.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/5439724aabab52b5f7ed588b0ab2f928.png)

1. Keep `nodeId` set to `fixed`. Type in **Label (live)**: the canvas text follows the input. Change Width or Height: the box changes size through `setSize()`.
2. Change Note and press **Apply note**. The Current note output shows the committed payload. Press Undo to restore the previous note, then Redo to reapply it. Payload data is separate from the display label; changing `note` does not replace the node's label.
3. Press Add. A new node appears to the right, and the inspector targets its generated id. Press Delete target to remove it. Undo restores it. Look up the node again by id after undo rather than retaining a model reference: deletion undo reconstructs models.
4. Set `nodeId` to `auto`. Lengthen its label: content-aware sizing grows it during rendering when the text needs more room. With this unconstrained node, manually enlarging the box survives subsequent renders; shortening the label does not shrink it.

Removing a node also removes its connected links and descendants; undo restores them. `removeNode()` rejects if the target does not exist, which the inspector reports instead of silently ignoring it.

### Commit labels in place

The shared inspector setup enables `enableInPlaceTextEdit` through `setInteractionConfig()`. Double-click a node to open the shipped label editor. Enter or blur commits the rename through a command; Escape cancels it. Undo and Redo then act on that rename. Angular also enables its in-place editing input by default.

For an external Rename action on an instance, call `beginLabelEdit({ type: 'node', nodeId: 'fixed' })`. It returns whether an editor opens; a missing, non-editable or read-only target returns `false`. An optional `{ seed: 'R' }` second argument starts with replacement text instead of selecting the existing label.

See the live [in-place label editing demo](https://grafloria.com/demos/nodes/edit-label.html) to try Enter, blur and Escape.

## Options that matter

| Option | Type | Default | What it does |
|---|---|---|---|
| `NodeSpec.id` | `string` | Optional | Gives your inspector a stable target for model lookup and commands. |
| `NodeSpec.label` | `string` | Optional | Supplies `metadata.label`, the display label. |
| `NodeSpec.data` | Payload record | Optional | Carries application data separately from the label. |
| `NodeSpec.size` | `{ width: number; height: number }` | Optional | Declares the box dimensions. |
| `metadata.sizing.auto` | `boolean` | Off unless `true` | Enables content-aware fitting during rendering. |
| `metadata.sizing.padding` | `number` | `8` | Adds padding around measured label content. |
| `metadata.sizing.minWidth`, `minHeight` | `number` | No per-node constraint | Sets lower bounds for content sizing and interactive resizing. |
| `metadata.sizing.maxWidth`, `maxHeight` | `number` | No per-node constraint | Sets upper bounds; `maxWidth` also supplies the label's wrap width during measurement. |

## Pitfalls

- Use tracked setters such as `setMetadata()`, `setData()` and `setSize()`, not raw assignments to live fields. Setters notify the model's change tracking. They do not themselves create history entries; use a command for a committed inspector action.
- `metadata.label` takes precedence over a legacy `data.label`. A payload command that writes `data.label` does not rename a node with a canonical label; use the label editor for that task.
- With auto-sizing enabled, the renderer grows the current dimensions to accommodate content, subject to sizing constraints. A manual enlargement of the unconstrained `auto` node remains; a manual reduction can grow again if the label needs more room.
- For controlled inputs, keep the framework's change-event return path. See the [React quick start](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/react-quick-start) and [Vue quick start](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/vue-quick-start).
- For engine history, explicit layout after edits, and repainting custom HTML content, see [commands and history](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/commands-and-history), [lay out a diagram](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/lay-out-a-diagram), and [JavaScript elements and content](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/javascript-elements-and-content).

## Live demos and related guides

- [Updating nodes](https://grafloria.com/demos/nodes/updating-nodes.html): live label, background and width controls. [Source](https://github.com/grafloria/grafloria/blob/cccb505e76c7af26c8bafecaba8146db7d8df928/demos/nodes/updating-nodes.html).
- [Auto-sizing](https://grafloria.com/demos/nodes/auto-sizing.html): compare a content-sized node with a fixed-size control.
- [Save and restore documents](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/save-and-restore-documents): persist the live document rather than a framework projection.
