Rules and changes
Admit edits with the controller's DiagramRules, and observe committed document changes on diagram.changes. The diagram applies its rules consistently, so a canvas, headless controller, custom tool and verb use the same boundary, whichever controller submits the edit.
On this page
| Category | What you can do |
|---|---|
| Rules | Admit changes, creations and connections. |
| Domain invariants | Validate all changed documents consistently. |
| Proposed changes | Inspect an edit before accepting it. |
| Results | Allow or reject an operation with a reason. |
| Committed changes | Observe accepted content updates. |
| Interactions | Shape regions, connections and paste on a mounted canvas. |
DiagramRules
dart
const DiagramRules({
DiagramChangeRule? beforeChange,
DiagramCreationRule? canCreate,
DiagramConnectionRule? canConnect,
})| Rule | Signature | Decides |
|---|---|---|
beforeChange | EditResult Function(DiagramChangeProposal) | Whether a proposed document change may be published: every verb, gesture, load, undo and redo, from any controller. |
canCreate | EditResult Function(FramedElement) | Whether a prepared shape or text element may be created. The element carries its identity and materialized defaults. |
canConnect | EditResult Function(DiagramConnectionIntent) | Whether a connection may be made, from hovering a port to committing the connector. |
Each rule returns const EditResult.allow() or EditResult.denied(reason: ...); a denied edit publishes nothing and the verb returns the denial. Pass the rules as DiagramController(configuration: DiagramConfiguration(rules: ...)), read them from controller.diagram.configuration.rules and replace them for every later edit with controller.diagram.setRules(DiagramRules(...)).
Example
dart
final controller = DiagramController(
document: document,
configuration: DiagramConfiguration(
rules: DiagramRules(
beforeChange: (proposal) {
if (readOnly) {
return const EditResult.denied(
reason: 'This drawing is read-only.',
);
}
return const EditResult.allow();
},
canConnect: (intent) =>
intent.source?.elementId == intent.target.elementId
? const EditResult.denied(reason: 'No loops')
: const EditResult.allow(),
),
),
);
final changes = controller.diagram.changes;
void onChange() => saveQueue.add(changes.value!.after);
changes.addListener(onChange);
// On host disposal:
// changes.removeListener(onChange);Fixed domain validation
A document validator is a beforeChange rule over the proposed document:
dart
DiagramController(
document: document,
configuration: DiagramConfiguration(
rules: DiagramRules(beforeChange: (proposal) => validate(proposal.after)),
),
);beforeChange checks changed candidates, including previews, imports and undo/redo. It does not run on the initial document; validate content before constructing the controller. Lock checks run before layout, so an edit denied by a lock is never resolved. Return allow() for DiagramChangePhase.preview to check only commits; history, imports and verbs performed outside a lease always commit.
Rules must not edit the drawing. Asynchronous validation requires a separate preparation contract; returning a Future from a rule is unsupported.
DiagramChangeProposal
The argument to beforeChange. Read this immutable event in your callback; elements is calculated only when requested.
| Property | Type | Meaning |
|---|---|---|
before | DiagramDocument | Operation baseline, not the previous pointer or IME preview. |
after | DiagramDocument | Proposed content after the edit. |
scene | ResolvedScene | Geometry matching the proposed document. |
currentDocument | DiagramDocument | Current content, including the previous preview when applicable. |
currentScene | ResolvedScene | Geometry matching the current document. |
phase | DiagramChangePhase | preview or commit. |
origin | DiagramDocumentChangeOrigin | Entry path listed below. |
operationId | int | Diagram-local identity retained across previews and final commit. |
historyOperationId | int? | Original accepted operation being undone or redone; null for other edits. |
participant | DiagramParticipant | Initiating participant captured with the operation: the submitting controller's participant, defaulting to DiagramParticipant.local. Attribution, not authentication. |
label | String | Operation label. |
elements | DiagramChangeSet | Baseline-to-candidate delta carried from the transaction. |
EditResult
| Member | Arguments | Result |
|---|---|---|
const EditResult.allow() | None | allowed == true, reason == null. |
const EditResult.created(String id) | Created identity | allowed == true; id names the created element or part. |
const EditResult.denied({DiagramDenialCode code = DiagramDenialCode.policy, String? reason}) | Optional category and explanation | allowed == false; a categorized denial for host handling. The code defaults to policy. |
bool allowed | Read-only property | Whether admission succeeded. |
String? reason | Read-only property | Optional denial explanation. |
DiagramDenialCode? code | Read-only property | Null for success; stable category for denial. |
String? id | Read-only property | The identity a creating verb reported; null otherwise. |
void requireAllowed() | None | Returns normally when allowed; otherwise throws DiagramChangeDenied. |
The denial categories are policy, locked, unavailableTarget, invalidSelection, unsupportedOperation, and conflict. Branch on code, not the human-readable reason.
DiagramChangeDenied.result contains the rejected EditResult. Its string representation uses the reason when supplied.
DiagramDocumentChange
The value of diagram.changes. Read this immutable event in your listener; elements is calculated only when requested.
| Property | Type | Meaning |
|---|---|---|
before | DiagramDocument | Baseline before the completed edit. |
after | DiagramDocument | Accepted content after the edit. |
scene | ResolvedScene | Geometry matching the accepted document. |
label | String | Completed operation label. |
origin | DiagramDocumentChangeOrigin | transaction (a verb), import, interaction (a gesture lease), batch (a cumulative lease or batch), undo, or redo. |
operationId | int | Matches the admission proposal for this operation. |
historyOperationId | int? | Original accepted operation being undone or redone; null for other edits. |
participantId | String | The id of the participant captured by the admission proposal; resolve it with diagram.participantById. |
history | bool | Whether the edit recorded a history entry; false for batch(history: false). Hosts can skip autosave or dirty-marking for these. |
elements | DiagramChangeSet | Baseline-to-final delta carried from the transaction. |
DiagramChangeSet
| Property | Type | Meaning |
|---|---|---|
insertedIds | Set<String> | Identities added to the document. |
removedIds | Set<String> | Identities removed from the document. |
updatedIds | Set<String> | Existing identities whose elements changed. |
reorderedIds | Set<String> | Surviving identities whose canonical order changed. |
reparentedIds | Set<String> | Updated identities whose owner changed. |
orderChanged | bool | Element order changed, including insertion or removal. |
Sets are unmodifiable. DiagramElementChangeSet remains an alias. The diagram derives the delta once from the transaction's proven edits, visiting only changed elements; DiagramChangeSet(before, after) compares independent snapshots completely. Replacing the document identity treats all old elements as removed and all new elements as inserted, even when local element IDs match.
Document admission
beforeChange receives a DiagramChangeProposal containing the operation's before document, validated candidate publication, currently published snapshot, label, origin, phase and operation ID. Its after document and lazy elements delta describe the actual candidate, including indirectly removed elements.
Return EditResult.allow() or EditResult.denied(reason: reason). The rule cannot rewrite the candidate or synchronously mutate the diagram. Transformations belong in explicit transactions. An exception aborts admission and propagates to the caller; it never becomes an implicit allow.
| Entry path | Admission behavior |
|---|---|
perform and typed verbs | Checks locks and expectedRevision, then the proposed edit, before history or publication changes. Invalid arguments are denied with unsupportedOperation before admission. |
setValues | Ordinary verb admission. Invalid value fields/types/ranges reject before publication. |
beginInteraction / shape widget beginValueEdit | Returns a lease; each preview and the final commit use the gesture rules below. |
| Gesture preview | Checks baseline-to-candidate with preview phase. Denial restores the baseline while retaining gesture ownership for recovery. |
| Gesture commit | Rechecks with commit phase. Denial restores the baseline and closes the gesture without a history entry. |
Cumulative lease (text composition, batch) | Checks provisional changes, then checks the complete operation when the lease commits. Final denial restores the lease baseline and prior redo branch. |
load | Checks the complete replacement and expected revision through the same pipeline. |
| Undo and redo | Checks the actual restoration before advancing history. |
| Another controller's preview | An edit touching an element another controller is previewing is denied with DiagramDenialCode.conflict before rules run. An edit without history is admitted and the preview is rebased onto it. |
| Cancellation | Always restores the baseline, without consulting admission. |
| Camera, selection, options and layout refresh | Plain controller methods such as setCamera, select and setOptions; not undoable, not admitted, and available without document permission. |
Admission owns lock and revision checks: a locked target is denied with DiagramDenialCode.locked, and a stale expectedRevision, which every verb accepts, is denied with DiagramDenialCode.conflict. Direct methods throw DiagramChangeDenied on denial; calls through controller.edits report it in the result. The result-oriented methods on controller.edits, including batch, setProperty, undo and redo, return an EditResult; a creation verb reports the new identity in id only when the content was inserted. dispatch is the low-level hook that verb implementations submit through; applications perform verbs.
Operation IDs are local to the diagram. A lease, such as a gesture or composition, retains its ID across previews and final admission. The element delta is the one the operation produced; reading it does not scan the document.
Completed changes and live state
Read the controller's observable getters, such as document and scene, inside an Observer for that controller's live state, including its interaction previews and provisional text composition. Use diagram.changes for completed verbs, committed leases, undo and redo. Cancellation does not emit a completed change.
diagram.changes is a ValueListenable<DiagramDocumentChange?> notified once per completed change from every controller on the diagram. Any number of independently owned consumers can add and remove listeners; it excludes camera and selection updates, and its listeners are released when the diagram is disposed. change.participantId names the participant that made the edit (diagram.participantById resolves it). Changing rules, options or the canvas bounds does not replace these subscriptions.
change.operationId matches the admission proposal for that operation. Lease previews keep the same ID through their final commit. Undo and redo each receive a new ID. IDs are local to the diagram's lifetime, not persistent document IDs or globally unique audit identifiers; a denied proposal has no corresponding completed change.
A cumulative lease commits when composition finishes or when an unrelated edit, undo or new interaction takes over. Its event contains the complete baseline-to-final delta. If final admission is denied, the unrelated operation is also left unapplied; submit a fresh intent against the restored state.
Observers cannot synchronously edit the drawing. Queue later work as a separate verb. All observers receive the publication before callback failures are forwarded to the current error zone.
Embedded control edits
Widget controls use the same rules as headless verbs. There is no separate widget persistence or authorization callback.
| Control action | Live state | Completed diagram.changes |
|---|---|---|
shape.setValues(patch) | Performs setValues as one admitted edit. | One event for a nonempty accepted edit. |
shape.beginValueEdit().preview(patch) | Previews setValues on a lease, relative to the gesture baseline. | None until commit. |
edit.commit() | Rechecks commit admission. | One baseline-to-final event if accepted and changed. |
edit.cancel() or denied commit | Restores the baseline. | None. |
Here shape is the ShapeWidgetContext supplied to the native control builder. The corresponding controller verb is controller.edits.setValues(elementId, patch); for a continuous value edit outside a shape widget, perform setValues on a beginInteraction lease. A continuous slider should use a value edit so persistence sees one completed operation instead of every pointer sample. Observe the controller's live document in an Observer when a host preview needs to follow each sample.
Compare ShapeElement.values in before and after for IDs in elements.updatedIds; this is not a field-level diff. Check permissions in admission, not the widget’s enabled flag. A denied commit restores the gesture baseline without adding history.
Interactions on a canvas
Canvas input goes through verbs and rules; the canvas has no separate callbacks.
- Shape regions. A
ShapeSurfacewhoseonInteractnames a verb of its shape performs that verb, without arguments, when a tap ends inside the region. See shape regions. - Connections. Dragging a connector asks
canConnectfrom port hover to commit. Observe the result through the controller's observabledocument,selectionandisInteracting, ordiagram.changesfor the committed connector. - Paste. A
DiagramFlutterExtensionwith apastehandler receives text pasted when no copied diagram content is available, such as Mermaid source. Browser gestures supply their clipboard text; native keyboard requests passnullso the handler reads the clipboard. Returntruewhen handled.
dart
import 'package:vyuh_diagram_extension_editor/vyuh_diagram_extension_editor.dart';
import 'package:vyuh_diagram_flutter/extension_api.dart';
import 'package:vyuh_diagram_flutter/vyuh_diagram_flutter.dart';
final controller = DiagramController(
configuration: DiagramConfiguration(
extensions: [
EditorExtension(),
DiagramFlutterExtension(
id: 'app.paste',
paste: (controller, text) async => importText(controller, text),
),
],
),
);canCreate and canConnect filter their respective intents. They are not universal permission gates: edits that are not creation or connection intents skip them but always pass through beforeChange. Put application-wide mutation rules in beforeChange.
Rules are read-only. Nested verbs, ID allocation, disposal and rule evaluation throw StateError; perform later verbs after the rule returns. Failure leaves the controller usable. Verbs reject disposed controllers. External host side effects cannot be rolled back.
Ownership and lifetime
Replace rules with diagram.setRules. Add listeners to diagram.changes and remove them when their host is disposed. For persistence, use diagram.changes so document edits made without a mounted surface, or by another controller, are also observed. Never mutate during admission or observer delivery.
Advanced ownership adapters are documented in API reference.
Initial document loading does not run rules or replay creation events. Authorizing access to a snapshot belongs to the host. Rules are synchronous client-side controls; asynchronous approval and server authorization remain host responsibilities. See persistence and imports for revision-checked replacement.
Verbs versus rules
Use a verb for an edit initiated by a button, key or painted region. Use beforeChange to admit or deny the resulting edit and diagram.changes to observe publication. A verb cannot bypass the rules.