Guide: backend integration¶
The editor talks to a backend only through two optional ports —
BpmnObjectProvider and BpmnPersistence (both plain TypeScript
interfaces, see the API reference for their
full shape). Nothing in the editor's own code knows about HTTP, CSRF,
routes, or any particular backend framework; all of that lives in your
implementation of these two interfaces.
This guide is framework-agnostic — the fetch calls below are plain
fetch(), adapt them to whatever your backend actually expects. Laravel
is not assumed anywhere in this package; it just happens to be the
backend Mercator (the reference integration below) uses.
BpmnObjectProvider¶
Powers the "insert cartography object" search action inside the contextual menu that appears when you select an element. Implement it against whatever "catalogue of things a diagram element can link to" means in your app:
import type { BpmnObjectProvider } from '@sourcentis/bpmn-editor';
const provider: BpmnObjectProvider = {
getGraphObjects: () =>
fetch('/api/applications', { credentials: 'same-origin' })
.then((res) => res.json()),
getInformationObjects: () =>
fetch('/api/information', { credentials: 'same-origin' })
.then((res) => res.json()),
getActorObjects: () =>
fetch('/api/actors', { credentials: 'same-origin' })
.then((res) => res.json()),
getProcessObjects: () =>
fetch('/api/processes', { credentials: 'same-origin' })
.then((res) => res.json()),
};
createBpmnEditor(container, { provider });
Each endpoint should resolve to an array of { id, name, glyph, url? }.
Leave the provider option out entirely and this feature is simply hidden
— nothing else about the editor changes.
BpmnPersistence¶
Powers the built-in ui: 'default' toolbar's Save button:
import type { BpmnPersistence } from '@sourcentis/bpmn-editor';
const persistence: BpmnPersistence = {
save: ({ id, name, type, content }) =>
fetch(`/api/diagrams/${id}`, {
method: 'PUT',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, type, content }),
}).then((res) => {
if (!res.ok) throw new Error(`Save failed (${res.status})`);
return res.json(); // must resolve to { id }
}),
};
createBpmnEditor(container, { persistence });
Leave persistence out and Save downloads a local .maxgraph file instead —
useful for prototyping before a save endpoint exists.
If you're using ui: 'none'¶
The built-in Save button doesn't exist in ui: 'none' mode (there's no
toolbar at all), so options.persistence has no effect there. Call your
BpmnPersistence implementation directly from your own Save button
instead:
const editor = createBpmnEditor(container, { ui: 'none' });
document.getElementById('my-save-button').addEventListener('click', async () => {
const content = editor.getXml();
const { id } = await persistence.save({ id: currentId, name, type, content });
currentId = id;
});
This is exactly the pattern Mercator's own adapter uses — see below.
Mercator: a worked example¶
Mercator (a Laravel app) is the
project this package was extracted from, and its current BPMN editor is
built on it — mounted with ui: 'none' to reuse Mercator's own Bootstrap
sidebar/toolbar. Its two adapter files
(resources/BPMN/mercator-provider.ts and mercator-persistence.ts in the
Mercator repo — not part of this package) implement exactly the two ports
above against Laravel routes, with the framework-specific details fully
contained there:
// mercator-provider.ts (excerpt)
async function fetchBpmnObjects(endpoint: string): Promise<BpmnElementDef[]> {
const res = await fetch(endpoint, {
method: 'GET',
headers: { Accept: 'application/json', 'X-Requested-With': 'XMLHttpRequest' },
credentials: 'same-origin',
});
const data = await res.json();
return data.map((o) => ({ id: o.id, name: o.name, glyph: o.id[0], url: o.url }));
}
export const MercatorBpmnProvider: BpmnObjectProvider = {
getGraphObjects: () => fetchBpmnObjects('/admin/bpmn/objects'),
getInformationObjects: () => fetchBpmnObjects('/admin/bpmn/information'),
getActorObjects: () => fetchBpmnObjects('/admin/bpmn/actors'),
getProcessObjects: () => fetchBpmnObjects('/admin/bpmn/process'),
};
// mercator-persistence.ts (excerpt)
export const MercatorBpmnPersistence: BpmnPersistence = {
async save({ id, name, type, content }) {
if (!name.trim()) throw new Error('Le nom du graphe est obligatoire.');
const response = await fetch(`/admin/bpmn/${id}`, {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') ?? '',
'X-Requested-With': 'XMLHttpRequest',
},
// Laravel's PUT-via-POST convention — nothing the editor itself needs to know about.
body: JSON.stringify({ _method: 'PUT', id, name, type, content }),
});
if (response.status !== 200) throw new Error('Erreur lors de la sauvegarde du graphe.');
const { graph_id: graphId } = await response.json();
// Reflect the newly-assigned id back into the page — Mercator uses a
// fixed #id input and the URL to track "which diagram is this".
const idInput = document.getElementById('id');
if (idInput && graphId) idInput.value = String(graphId);
if (id === -1 && graphId) window.history.replaceState({}, '', `/admin/bpmn/${graphId}`);
return { id: graphId };
},
};
And its bootstrap (resources/BPMN/bpmn.ts in the Mercator repo) ties it
together, mounting with ui: 'none' and driving Mercator's own toolbar
buttons through the editor's instance API:
const editor = createBpmnEditor(document.getElementById('graph-container'), {
ui: 'none',
paletteRoot: document.getElementById('sidebar'), // Mercator's own palette, tagged with data-node-type
provider: MercatorBpmnProvider,
persistence: MercatorBpmnPersistence,
onNavigate: (url) => { window.location.href = url; },
});
document.getElementById('zoom-in-btn').addEventListener('click', () => editor.zoomIn());
document.getElementById('save-btn').addEventListener('click', async () => {
const content = editor.getXml();
await MercatorBpmnPersistence.save({ id: currentId(), name: nameField.value, type: typeField.value, content });
});
No CSRF handling, no route strings, no _method: 'PUT' convention appears
anywhere in the editor package itself — all of it lives in these three
adapter files, on the host side of the port boundary.