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.
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.
| Value | Ownership on refresh |
|---|---|
data[namespace] | Replaced by mapped source data; other top-level data survives. |
extensions.primeuiData | Binding identity; preserve it and use one controller per namespace per store. |
| Node parent and relational endpoints | Always source-owned. |
nodeFields | Complete opted-in fields: type, bounds, rotation, ports, labels, container. Default []. |
edgeFields | Complete opted-in fields: type, waypoints, controls, labels, sourceDecorator, targetDecorator. Default []. |
| Other fields | Initialized 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.
| Mapping member | Contract |
|---|---|
namespace | Required string matching [A-Za-z][A-Za-z0-9_-]{0,39}; isolates ownership. |
pageId, layerId | Required 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. |
hierarchy | Required with either hierarchy accessor: containment writes actual parents; connectors creates parent-to-child edges between root-level nodes. Omitted for flat records. |
edges | Required 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.
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.
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.
DiagramDataState | Meaning |
|---|---|
idle, loading, mutating | No operation yet, waiting for loader, or waiting for writer. |
committed, noop, pending | Full snapshot applied, no model change, or host decision required. |
error, cancelled, stale, disposed | Invalid/provider failure, locally cancelled, superseded ownership, or unavailable controller. |
requestId, message, transaction | Optional attempt ID, readable diagnosis and underlying store result. |
remoteAcknowledged | Optional 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.