Extending the editor
Every extension point is data plus callbacks — no subclassing, no injected services. Properties providers add typed panel fields whose commits are undoable engine commands; palette and context-pad entries bring icons, labels, hotkeys and actions; renderers replace a type's shape; element templates pre-configure elements the way Camunda Modeler's do. Icons and shapes are built with the safe bpmnSvg builders and sanitized before they render — there is no raw-markup path.
Properties providers
A provider returns groups of typed entries for the selected element — text, textarea, select, checkbox, expression, list or custom — and each entry's set(value) returns the engine command a commit runs, so every field is one undo step. A custom entry is drawn by an <ng-template ogeBpmnPropertiesEntry>. Select Review order to see the Service level group; the values land as oge:* attributes in the exported XML.
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeBpmnEditor, OgeBpmnPropertiesEntryTemplate, bpmnForeignAttribute, setForeignAttributeCommand } from '@oge-ui/bpmn';
import type { OgeBpmnPropertiesProvider } from '@oge-ui/bpmn';
const NS = { oge: 'https://ogeui.com/schema/bpmn/demo' };
/** A "Service level" group on user tasks, stored as oge:* attributes. */
const slaProvider: OgeBpmnPropertiesProvider = {
id: 'demo-sla',
getGroups: ({ diagram, target }) => {
if (target.kind !== 'node' || target.node.type !== 'userTask') return [];
const id = target.id;
const read = (name: string) => bpmnForeignAttribute(diagram, id, name);
const write = (name: string, value: string) =>
setForeignAttributeCommand(id, name, value, NS);
return [
{
id: 'sla',
label: 'Service level',
entries: [
{
id: 'sla-due',
label: 'Due within',
type: 'select',
value: read('oge:dueWithin'),
options: [
{ value: '', label: 'No deadline' },
{ value: 'P1D', label: '1 day' },
{ value: 'P1W', label: '1 week' },
],
set: (value) => write('oge:dueWithin', String(value)),
},
{
id: 'sla-escalate',
label: 'Escalate when late',
type: 'checkbox',
value: read('oge:escalate') === 'true',
set: (value) => write('oge:escalate', value === true ? 'true' : ''),
},
{
id: 'sla-effort',
label: 'Effort (hours)',
type: 'custom',
value: read('oge:effort') || '4',
set: (value) => write('oge:effort', String(value)),
},
],
},
];
},
};
@Component({
selector: 'demo-root',
imports: [OgeBpmnEditor, OgeBpmnPropertiesEntryTemplate],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<oge-bpmn-editor style="height: 480px" [propertiesProviders]="providers">
<!-- draws the 'custom' entry whose id matches; commit() is one undo step -->
<ng-template
ogeBpmnPropertiesEntry="sla-effort"
let-entry
let-commit="commit"
let-id="inputId"
>
<input
type="range"
min="1"
max="40"
[attr.aria-labelledby]="id"
[value]="entry.value"
(change)="commit($any($event.target).value)"
/>
{{ entry.value }} h
</ng-template>
</oge-bpmn-editor>
`,
})
export class Demo {
protected readonly providers = [slaProvider];
}Palette & context pad
Custom palette entries render after the built-ins and join the palette's roving tabindex; custom context-pad actions render beside the built-in ones for the selected element. Both take a bpmnSvg icon, a label (the accessible name and tooltip) and an optional single-character hotkey advertised with aria-keyshortcuts — keys the canvas already uses are never shadowed. Press M on the canvas to arm the templated Mail task, select an element and press R to mark it reviewed.
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeBpmnEditor, bpmnSvg, setElementColorsCommand } from '@oge-ui/bpmn';
import type { OgeBpmnContextPadProvider, OgeBpmnPaletteProvider } from '@oge-ui/bpmn';
@Component({
selector: 'demo-root',
imports: [OgeBpmnEditor],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<oge-bpmn-editor
style="height: 460px"
[paletteProvider]="palette"
[contextPadProvider]="pad"
/>
`,
})
export class Demo {
// icons are bpmnSvg trees — sanitized, never markup strings
protected readonly palette: OgeBpmnPaletteProvider = () => [
{
id: 'mail-task',
label: 'Mail task',
icon: [
bpmnSvg.rect(4, 6, 16, 12, { rx: 2 }),
bpmnSvg.polyline('4,7 12,13 20,7'),
],
hotkey: 'm', // canvas shortcut; built-in keys are never shadowed
action: (api) => api.armPlace('serviceTask'),
},
];
protected readonly pad: OgeBpmnContextPadProvider = ({ diagram, elementId }) =>
diagram.nodes[elementId] === undefined
? []
: [
{
id: 'mark-reviewed',
label: 'Mark reviewed',
icon: [bpmnSvg.polyline('3,8 7,12 13,4')],
hotkey: 'r',
action: (api, id) => {
// execute() keeps every change undoable
api.execute(setElementColorsCommand([id], { fill: '#dcfce7' }));
api.announce('Marked reviewed');
},
},
];
}Renderers
[renderers] overrides a node type's shape: the renderer gets the node, its size and DI colors and returns a bpmnSvg tree in shape-local coordinates (or null for the built-in glyph). The tree is sanitized — unknown tags, on* handlers, href, style and external url() are dropped — and the editor still draws labels, markers, selection and badges. Exports use the same renderer.
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeBpmnEditor, bpmnSvg } from '@oge-ui/bpmn';
import type { OgeBpmnRenderers } from '@oge-ui/bpmn';
@Component({
selector: 'demo-root',
imports: [OgeBpmnEditor],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<oge-bpmn-editor style="height: 420px" [renderers]="renderers" />
`,
})
export class Demo {
// draws in shape-local coordinates; null falls back to the built-in glyph.
// Labels, markers, selection and badges are still drawn by the editor, and
// exportSvg() / exportPng() use the same renderer.
protected readonly renderers: OgeBpmnRenderers = {
serviceTask: ({ width, height, fill }) => [
bpmnSvg.rect(0, 0, width, height, {
rx: 18,
fill: fill ?? '#eef2ff',
stroke: '#4f46e5',
'stroke-width': 2,
}),
bpmnSvg.circle(16, 16, 6, { fill: 'none', stroke: '#4f46e5' }),
bpmnSvg.path('M16 7v3M16 22v3M7 16h3M22 16h3', { stroke: '#4f46e5' }),
],
};
}Element templates
Templates follow Camunda Modeler's JSON shape: appliesTo, an optional elementType to morph to, and properties bound to an attribute, the documentation or a Zeebe / Camunda extension value. bpmnElementTemplatesProvider(templates) adds a Template select to every applicable element; applying writes every default in one undo step and records zeebe:modelerTemplate, after which the panel shows the template's fields. Select Ask for documents and pick Send mail.
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeBpmnEditor, bpmnElementTemplatesProvider } from '@oge-ui/bpmn';
import type { OgeBpmnElementTemplate } from '@oge-ui/bpmn';
/** Modeled on Camunda Modeler element templates (Zeebe bindings). */
const mailTemplate: OgeBpmnElementTemplate = {
id: 'io.ogeui.mail',
name: 'Send mail',
version: 1,
appliesTo: ['task', 'serviceTask'],
elementType: 'serviceTask',
properties: [
{
type: 'Hidden',
value: 'io.ogeui:mail:1',
binding: { type: 'zeebe:taskDefinition', property: 'type' },
},
{
label: 'Recipient',
type: 'String',
value: '=customer.email',
binding: { type: 'zeebe:input', name: 'to' },
},
{
label: 'Priority',
type: 'Dropdown',
value: 'normal',
choices: [
{ name: 'Normal', value: 'normal' },
{ name: 'High', value: 'high' },
],
binding: { type: 'zeebe:taskHeader', key: 'priority' },
},
],
};
@Component({
selector: 'demo-root',
imports: [OgeBpmnEditor],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- select a task: the "Template" select applies "Send mail" -->
<oge-bpmn-editor style="height: 460px" [propertiesProviders]="providers" />
`,
})
export class Demo {
protected readonly providers = [bpmnElementTemplatesProvider([mailTemplate])];
}