Diagram - Data Binding

Map business records into a stable Diagram store and connect application-owned loaders and writers.

#Usage

import { Diagram } from '@primeui/vue-diagram';
<Diagram.Root :store="store" page-id="page">
    <Diagram.Viewport label="Workflow editor" style="height: 24rem" />
</Diagram.Root>
import { DiagramDataController, type DiagramDataMapping } from '@primeui/vue-diagram';

type Account = { id: string | number; name: string; x: number; y: number };
const mapping: DiagramDataMapping<Account> = {
    namespace: 'accounts',
    pageId: 'page',
    layerId: 'layer',
    id: (row) => row.id,
    node: (row) => ({
        type: 'card',
        bounds: { x: row.x, y: row.y, width: 180, height: 90 },
        rotation: 0,
        ports: [],
        labels: [],
        data: { name: row.name }
    })
};
const binding = new DiagramDataController(store, mapping);
binding.refresh({ nodes: accounts });

Dispose the binding before its store. Keep both alive across refreshes.

Create a DiagramDataController for the store using the mapping above. Refresh records through that controller; stable business IDs preserve identity. The application owns provider cancellation and controller disposal.

This skeleton assumes an initialized store with page page. Use First Diagram for model creation, style imports and store disposal.

Use data binding when business records own the content of your diagram. A DiagramDataController maps those records into an existing store. Keep the store, root and controller stable while refreshing; a refresh updates records without creating another editor session.

In the store owner's setup, import DiagramDataController and DiagramDataMapping from @primeui/vue-diagram. This example maps a complete accounts array into an existing page/layer:

#Map flat records with stable business IDs

Select Ada, drag her card, then refresh names. Her Diagram ID and local position stay stable. The local note is outside the binding and stays unchanged.

Loading Demo...

The mapping's id callback returns a business ID, not a Diagram record ID. Numeric 1 and string '1' are different identities, as the example shows. Use a stable key from your application instead of an array index or display name.

Read the allocated ID with binding.recordId(1); it returns undefined before allocation. The optional second argument is node (default), edge or hierarchy. IDs remain reserved across removal and re-addition during this controller's lifetime. Existing extensions.primeuiData tags seed a new controller; deleted-record reservations do not survive disposal. Read a tag with diagramDataIdentity(record) rather than guessing from the generated ID.

#Choose source-owned fields

Every refresh({ nodes, edges? }) is a complete authoritative snapshot for that namespace. This is also required of every loader and writer result. Missing owned records are removed. Send unchanged rows too; a changed-row array would remove the rows you omitted.

ValueOwnership on refresh
data[namespace]Replaced by mapped source data; other top-level data survives.
extensions.primeuiDataBinding identity; preserve it and use one controller per namespace per store.
Node parent and relational endpointsAlways source-owned.
nodeFieldsComplete opted-in fields: type, bounds, rotation, ports, labels, container. Default [].
edgeFieldsComplete opted-in fields: type, waypoints, controls, labels, sourceDecorator, targetDecorator. Default [].
Other fieldsInitialized on creation, then retain local edits. Other extensions survive.

Opted-in fields are replaced in full; an omitted optional field is removed. For example, nodeFields: ['bounds'] restores source geometry on each refresh. Without it, users keep their authored geometry. Mapping data lives at node.data['accounts'] in this example, rather than node.data['name'].

A refresh validates all changes together. Removing a node still referenced by an unrelated child or connector rejects the transaction. Moving a bound record outside its mapped page/layer is an ownership conflict. Equal snapshots return noop; unchanged records retain their references. One committed refresh is one Undo unit. Surviving selection, focus and view remain in the same session. Undo changes the local model and does not invoke a writer; a later refresh can restore the authoritative source values.

#Map containment, trees and relationships

Reparent Ada between two containers. Her local coordinates remain unchanged; her world placement changes with the parent. This source refresh differs from an editor reparent operation that preserves world placement.

Loading Demo...
Mapping memberContract
namespaceRequired string matching [A-Za-z][A-Za-z0-9_-]{0,39}; isolates ownership.
pageId, layerIdRequired existing Diagram IDs; the layer belongs to the page.
id(row)Required callback returning a nonempty string up to 1,024 characters or a safe integer.
node(row, context)Required node template without id, pageId, layerId, parentId. Context has allocated id, businessId, depth.
parentId(row)Optional flat parent business ID or null. Choose this or children.
children(row)Optional nested row array. Choose this or parentId.
hierarchyRequired with either hierarchy accessor: containment writes actual parents; connectors creates parent-to-child edges between root-level nodes. Omitted for flat records.
edgesRequired when the source has explicit edges. Callbacks id, source, target return business IDs; connector(row, context) returns the connector template without structural IDs/endpoints.

Containment requires container-capable parent templates and parent-local bounds. Hierarchy parents must exist, have no cycles and stay within 32 levels. The example's bottom graph uses flat parent IDs for hierarchy connectors and adds an independent return relationship through source.edges. Relational cycles are allowed. Duplicate identities and missing endpoints reject the whole refresh.

Mapping callbacks receive detached frozen rows. Keep callbacks pure and return inert JSON. Getters, functions, cycles, non-finite values and invalid geometry fail atomically. Limits per binding are 1,000 nodes, 4,000 explicit edges and 10,000 retained identities, including deleted-record reservations.

#Load asynchronously, retry and cancel

This deliberate local loader waits 1.4 seconds. It supplies no server. Open Failure and stale scenarios to fail a load and retry it; start another load and edit Ada or cancel while it waits. The status names the result without a transaction log.

Loading Demo...

load(loader) supplies { requestId, revision, signal } and expects a Promise of { nodes, edges? }. retryLoad() calls the last loader again; with none, it returns an error. A newer request supersedes the previous one. A model commit, editing preview or host proposal invalidates a waiting result. cancel() settles local work immediately, even when the provider ignores cancellation.

The portable signal has aborted, addEventListener('abort', listener) and removeEventListener; it is not a DOM AbortSignal. The example removes its listener and timer when it settles. For an application transport, forward abort to your own transport controller and remove the listener in finally. Providers own response validation, authentication, cancellation and service errors.

#Write through an application-owned provider

Create, update or remove the new account. The local writer returns all rows, then the controlled host makes its own decision. Reject a successful write to see different service and committed-canvas counts.

Loading Demo...

mutate({ id, kind, payload }, writer) receives an immutable mutation and the same request context as a loader. kind is create, update or remove; payload is application-defined inert JSON. Resolution acknowledges the service operation and returns the complete binding snapshot. There are no optimistic local edits, automatic retry writes or rollback writes.

Mutation id is an application idempotency key. Repeating an identical mutation returns the original promise without another writer call. Reusing the ID with a different payload fails. The controller retains 1,000 mutation keys; reconcile before replacing it. Your service must enforce idempotency across reloads and controller lifetimes. After a failed or uncertain write, reconcile service state before deciding whether to submit a new mutation.

remoteAcknowledged reports writer success separately from controlled host acceptance. A result can be pending with service acknowledgement. Cancelling or disposing a controller does not withdraw an already submitted host proposal. Cancellation is advisory and cannot roll back a service write. A late response to a cancelled or stale request is ignored; reload or reconcile service state using your application's protocol. Store observers do not invoke the writer.

#Read outcomes and dispose ownership

state is a snapshot; subscribe(listener) returns an unsubscribe function. Bind subscriber updates to a Vue shallow ref, as the examples do.

DiagramDataStateMeaning
idle, loading, mutatingNo operation yet, waiting for loader, or waiting for writer.
committed, noop, pendingFull snapshot applied, no model change, or host decision required.
error, cancelled, stale, disposedInvalid/provider failure, locally cancelled, superseded ownership, or unavailable controller.
requestId, message, transactionOptional attempt ID, readable diagnosis and underlying store result.
remoteAcknowledgedOptional writer-success fact, independent of local acceptance.

Refresh/load are blocked during previews and pending host proposals. Keep the same store and root when refreshing rows. For teardown, unsubscribe observers, dispose the controller, then dispose your store; see SSR and Lifecycle.

Restart example restores the local fixture and clears its history. It does not reset an external service.

Controlled State ยท History ยท Save and Load

Organization Chart combines directory refresh with independently owned placement and deliberate tree layout.