BPMN Editor
A from-scratch, Angular-native BPMN 2.0 modeler — not a wrapper. The package carries its own dependency-free XML + diagram-interchange engine covering the full working element set: all event kinds with the nine standard event definitions, boundary events, sub-processes / event sub-processes / transactions, pools with lanes and message flows, data objects and stores, groups, call activities and activity markers. importXml() / exportXml() round-trip real BPMN 2.0 documents — Camunda extension elements and unknown attributes preserved verbatim, bpmn.io bioc element colors interoperable both ways — and the canvas is composed accessibility: no APG canvas-editor pattern exists, so the editor combines role="application", aria-activedescendant element tracking and a polite live region that narrates every action.
@oge-ui/bpmn is a commercial package — free for evaluation and non-commercial use, with no watermark and no runtime license checks. See licensing for the terms.
Getting started
One element, a working modeler — properties panel and minimap included by default (showPropertiesPanel / showMinimap). Pick a shape from the palette and click the canvas — or drag it onto the canvas — to place it; the context pad on a selected element connects, appends and deletes, and grows an align/distribute flyout on multi-selections. The tool strip under the palette switches hand (H), lasso (L), space (S) and global-connect tools; Ctrl+F opens element search. Keyboard: Tab cycles elements, arrows move (Shift for 1px), C connects, A appends, Ctrl+C/X/V/A clipboard, F zooms to fit, F2 edits the label, Ctrl+Z / Ctrl+Y undo and redo — Escape cancels any tool or drag.
Zoom: 100% — mouse wheel zooms at the cursor, middle-drag or Space-drag pans, the minimap click/drag jumps.
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeBpmnEditor } from '@oge-ui/bpmn';
import type { OgeBpmnElementsChangedEvent } from '@oge-ui/bpmn';
@Component({
selector: 'demo-root',
imports: [OgeBpmnEditor],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<oge-bpmn-editor
style="height: 480px"
[(zoom)]="zoom"
(elementsChanged)="onChanged($event)"
/>
`,
})
export class Demo {
// two-way zoom model: wheel zooming writes it back
protected readonly zoom = signal(1);
// fires on every command, undo/redo, import and newDiagram()
protected onChanged(event: OgeBpmnElementsChangedEvent): void {
console.log(event.source, event.label);
}
}Import & export
The engine reads prefix-agnostic BPMN 2.0 (bpmn:, bpmn2: or no prefix) and writes byte-deterministic XML with normalized prefixes — camunda-flavored files round-trip byte-identically, and bpmn.io bioc element colors are read and written both ways. The few remaining unsupported constructs (nested lane sets, extra event definitions on one event, timer/error payload children) are dropped with an explicit warning in importCompleted — never silently. Edit the XML below, import it, move things around, then export it back.
import { ChangeDetectionStrategy, Component, signal, viewChild } from '@angular/core';
import { OgeBpmnEditor } from '@oge-ui/bpmn';
import type { BpmnImportWarning, OgeBpmnImportEvent } from '@oge-ui/bpmn';
const SAMPLE_BPMN_XML = `<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" ...>
<bpmn:process id="Process_order">
<bpmn:startEvent id="Start_1" name="Order received" />
<bpmn:userTask id="Task_review" name="Review order" />
<bpmn:exclusiveGateway id="Gateway_ok" name="Approved?" default="Flow_no" />
<!-- ... end events, sequence flows and BPMN DI shapes/edges ... -->
</bpmn:process>
</bpmn:definitions>`;
@Component({
selector: 'demo-root',
imports: [OgeBpmnEditor],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<oge-bpmn-editor #editor style="height: 420px" (importCompleted)="onImport($event)" />
<textarea [value]="xml()" (input)="xml.set($any($event.target).value)"></textarea>
<button type="button" (click)="importXml()">Import</button>
<button type="button" (click)="exportXml()">Export</button>
`,
})
export class Demo {
private readonly editor = viewChild.required(OgeBpmnEditor);
protected readonly xml = signal(SAMPLE_BPMN_XML);
protected readonly warnings = signal<readonly BpmnImportWarning[]>([]);
protected importXml(): void {
// resolves with { model, warnings, error? }; a fatal parse
// error leaves the current diagram untouched
void this.editor().importXml(this.xml());
}
protected exportXml(): void {
// deterministic BPMN 2.0 XML — same model, same bytes
this.xml.set(this.editor().exportXml());
}
protected onImport(event: OgeBpmnImportEvent): void {
this.warnings.set(event.warnings);
}
}Autosave & persistence
The debounced diagramChanged stream is the autosave hook: after edits settle for autoSaveDebounceMs (default 500ms, configurable via provideOgeBpmnConfig()) it emits the diagram already serialized to both the versioned JSON envelope and BPMN XML — never mid-drag. importJson() restores an envelope with full structural validation, so a corrupted payload returns an error instead of clobbering the canvas. Draw below, watch the status line, reload the page, then restore.
Nothing saved yet — place an element to trigger the stream.
import { ChangeDetectionStrategy, Component, viewChild } from '@angular/core';
import { OgeBpmnEditor } from '@oge-ui/bpmn';
import type { OgeBpmnDiagramChangedEvent } from '@oge-ui/bpmn';
@Component({
selector: 'demo-root',
imports: [OgeBpmnEditor],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<oge-bpmn-editor
#editor
style="height: 420px"
(diagramChanged)="onDiagramChanged($event)"
/>
<button type="button" (click)="restore()">Restore last save</button>
`,
})
export class Demo {
private readonly editor = viewChild.required(OgeBpmnEditor);
// fires once per settled change (autoSaveDebounceMs, default 500ms),
// with the diagram already serialized to both JSON and XML — no
// exportJson() call needed and never mid-drag
protected onDiagramChanged(event: OgeBpmnDiagramChangedEvent): void {
if (event.source === 'import' || event.source === 'new') return;
localStorage.setItem('diagram', JSON.stringify(event.json));
this.editor().markSaved();
}
protected restore(): void {
const raw = localStorage.getItem('diagram');
if (raw === null) return;
// structural validation — a broken envelope never clobbers the canvas
const { error } = this.editor().importJson(JSON.parse(raw));
if (error !== undefined) console.warn(error);
}
}Overlays & monitoring
addOverlay() attaches an HTML badge to any element — the process-monitoring primitive for token counts, incident markers or heatmaps. Badges anchor to a corner (or the center) of the element's bounds with an optional diagram-unit offset, track the element through pan, zoom and model changes, and hide (without losing their registration) while the element is gone. The markup renders through Angular's sanitizing [innerHTML]. Select an element below and add a count bubble to it.
import { ChangeDetectionStrategy, Component, viewChild } from '@angular/core';
import { OgeBpmnEditor } from '@oge-ui/bpmn';
@Component({
selector: 'demo-root',
imports: [OgeBpmnEditor],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<oge-bpmn-editor #editor style="height: 420px" />
<button type="button" (click)="addBadge()">Add badge</button>
<button type="button" (click)="clearBadges()">Clear</button>
`,
})
export class Demo {
private readonly editor = viewChild.required(OgeBpmnEditor);
private count = 0;
// attach a count bubble to the selected element; the badge tracks it
// through pan, zoom and model changes, hides while the element is
// gone and returns the handle for removeOverlay()
protected addBadge(): void {
const [id] = this.editor().getSelection();
if (id === undefined) return;
this.editor().addOverlay({
elementId: id,
// rendered through Angular's sanitizing [innerHTML]
html: '<span class="badge">' + ++this.count + '</span>',
position: 'top-right',
offset: { x: 4, y: -4 },
});
}
protected clearBadges(): void {
this.editor().clearOverlays(); // or clearOverlays(elementId)
}
}Read-only viewer
[readOnly]="true" turns the editor into a diagram viewer: palette, context pad, properties panel, keyboard editing and drags are all disabled, while selection, pan, zoom-to-fit, element search (Ctrl+F) and the accessible reading order (Tab through elements, live-region announcements) keep working.
import { ChangeDetectionStrategy, Component, afterNextRender, viewChild } from '@angular/core';
import { OgeBpmnEditor } from '@oge-ui/bpmn';
declare const PROCESS_XML: string; // e.g. fetched from your API
@Component({
selector: 'demo-root',
imports: [OgeBpmnEditor],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<oge-bpmn-editor #viewer style="height: 360px" [readOnly]="true" />
`,
})
export class Demo {
private readonly viewer = viewChild.required(OgeBpmnEditor);
constructor() {
// load the diagram once the view exists; readOnly blocks every
// mutation (palette, context pad, keyboard editing, drags) but
// keeps selection, pan, zoom and the accessible reading order
afterNextRender(() => {
void this.viewer().importXml(PROCESS_XML);
});
}
}Configuration & i18n
Every user-facing string — palette labels, tool strip, align flyout, search overlay, properties panel, context-pad actions, live-region announcement templates, the canvas name and hint — lives in OgeBpmnMessages. Override per instance with [messages] (Turkish below) or app-wide with provideOgeBpmnConfig(), which also sets gridSize, snapThreshold, the zoom bounds, autoSaveDebounceMs and the panel's fill colorPresets.
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeBpmnEditor, provideOgeBpmnConfig } from '@oge-ui/bpmn';
import type { OgeBpmnMessages } from '@oge-ui/bpmn';
@Component({
selector: 'demo-root',
imports: [OgeBpmnEditor],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- per-instance override via the [messages] input -->
<oge-bpmn-editor style="height: 420px" [messages]="turkish" />
`,
})
export class Demo {
// paletteLabels is a full record — every palette entry gets its label
protected readonly turkish: Partial<OgeBpmnMessages> = {
canvasLabel: 'BPMN diyagram editörü',
canvasHint: 'Diyagramdan çıkmak için Escape sonra Tab',
emptyText: 'Boş diyagram — paletten bir öğe seçin',
paletteLabel: 'Öğe paleti',
paletteLabels: {
startEvent: 'Başlangıç olayı',
endEvent: 'Bitiş olayı',
intermediateThrowEvent: 'Ara fırlatma olayı',
intermediateCatchEvent: 'Ara yakalama olayı',
boundaryEvent: 'Sınır olayı',
task: 'Görev',
userTask: 'Kullanıcı görevi',
serviceTask: 'Servis görevi',
scriptTask: 'Betik görevi',
callActivity: 'Çağrı aktivitesi',
subProcess: 'Alt süreç',
eventSubProcess: 'Olay alt süreci',
transaction: 'İşlem',
exclusiveGateway: 'Dışlayıcı geçit',
parallelGateway: 'Paralel geçit',
dataObject: 'Veri nesnesi',
dataStore: 'Veri deposu',
group: 'Grup',
pool: 'Havuz',
textAnnotation: 'Metin notu',
},
};
}
// or app-scoped defaults (merged over the built-ins):
export const appConfig = {
providers: [
provideOgeBpmnConfig({
gridSize: 20, // placement + arrow-key step
snapThreshold: 8, // neighbor-alignment snapping
zoomMin: 0.5,
zoomMax: 2,
autoSaveDebounceMs: 1000, // diagramChanged settle time (0 = sync)
colorPresets: ['#fee2e2', '#dcfce7', '#dbeafe'], // panel fill swatches
messages: { emptyText: 'Boş diyagram — paletten bir öğe seçin' },
}),
],
};Notes
- The engine (XML reader/writer, JSON envelope, SVG renderer, geometry, routing, alignment math, command stack) is pure TypeScript in the framework-free
@oge-ui/bpmn-enginepackage — shared by the Angular and the React editor, re-exported by both and importable directly asreadBpmnXml/writeBpmnXml/toBpmnJson/fromBpmnJson/renderDiagramSvgfor server-side or test pipelines. - Undo is snapshot-based: every command, including each arrow-key step, is exactly one undo entry, and
markSaved()pins the save point thatisDirty()anddirtyChangedreport against. exportSvg()renders a self-contained static SVG (fittedviewBox, neutral colors, custom element colors honored) — no DOM cloning, no external CSS.- Element coverage spans the full working set — events with all nine definition kinds, boundary events, sub-processes / event sub-processes / transactions, pools, lanes and message flows, data objects/stores, groups, call activities and activity markers. The remaining honest cuts (re-parenting by drag, vertical pool creation, multiple event definitions per event) are tracked on the roadmap; dropped content always surfaces as import warnings.