Form Editor Implementation¶
The Form Editor wraps the @bpmn-io/form-js library in a two-panel React component. This page covers the component structure, storage layer, and the integration points with the BPMN Modeler.
Component structure¶
packages/frontend/src/components/FormEditor/
├── FormEditor.tsx main orchestrator — state, seeding, CRUD callbacks
├── FormCanvas.tsx @bpmn-io/form-js editor canvas wrapper
└── FormList.tsx left-panel list with create / rename / delete actions
packages/frontend/src/
├── services/
│ └── formService.ts localStorage CRUD for FormSchema records
└── types/
└── index.ts FormSchema interface
FormSchema type¶
interface FormSchema {
id: string; // internal UUID used as list key
name: string; // display name
description?: string;
schema: Record<string, unknown>; // the @bpmn-io/form-js JSON schema
createdAt: string; // ISO 8601
updatedAt: string;
readonly?: boolean; // true for EXAMPLE seed forms
status?: 'example' | 'wip';
}
The schema.id field inside the JSON schema object is the form identifier used in BPMN XML (camunda:formRef) and in the Operaton deployment. It is distinct from the outer FormSchema.id used as the localStorage record key.
FormService¶
FormService is a static class providing synchronous localStorage CRUD:
import { FormService } from '../../services/formService';
const STORAGE_KEY = 'linkedDataExplorer_formSchemas';
FormService.getForms(): FormSchema[]
FormService.getForm(formId: string): FormSchema | null // by FormSchema.id
FormService.saveForm(form: FormSchema): void // upsert by .id
FormService.deleteForm(formId: string): void
The methods read/write the entire array on each call. There is no batching or indexedDB fallback — the storage size is small (three seed schemas are ≈ 3 KB total).
FormEditor.tsx — seeding logic¶
On mount, FormEditor.tsx runs a useEffect that checks for the presence of each seed form by FormSchema.id:
useEffect(() => {
const existing = FormService.getForms();
const existingIds = new Set(existing.map((f) => f.id));
for (const seed of SEED_FORMS) {
if (!existingIds.has(seed.id)) {
FormService.saveForm(seed);
}
}
setForms(FormService.getForms());
}, []);
The three seed IDs are example_kapvergunning_start, example_tree_felling_review, and example_awb_notify_applicant. Their readonly: true flag causes FormList to suppress rename and delete controls.
The schema.id values embedded in the JSON schemas are kapvergunning-start, tree-felling-review, and awb-notify-applicant respectively — these are the values written into camunda:formRef.
FormCanvas.tsx — editor lifecycle¶
FormCanvas.tsx mounts a @bpmn-io/form-js FormEditor instance:
import { FormEditor as FormJsEditor } from '@bpmn-io/form-js';
useEffect(() => {
const editor = new FormJsEditor({ container: containerRef.current });
editorRef.current = editor;
editor.importSchema(schema);
editor.on('changed', () => setHasChanges(true));
return () => {
editor.destroy();
editorRef.current = null;
};
}, [schema]); // re-mounts on schema identity change (new form selected)
The schema prop is the FormSchema.schema object for the currently active form. Because schema is a new object reference each time a different form is selected, the useEffect re-runs and the editor instance is replaced cleanly.
Save flow¶
const handleSave = async () => {
const { schema: savedSchema } = await editorRef.current.saveSchema();
onSave(savedSchema); // parent writes to FormService
setHasChanges(false);
};
Export flow¶
const handleExport = async () => {
const { schema: exportSchema } = await editorRef.current.saveSchema();
const formId = exportSchema.id ?? 'form';
// ... download as `{formId}.form`
};
Integration with FormTemplateSelector¶
FormTemplateSelector (in BpmnModeler/) reads forms directly from FormService.getForms() on mount. Because both FormEditor and FormTemplateSelector use the same FormService singleton over shared localStorage, no pub/sub or context is needed — a form saved in the Form Editor is immediately visible the next time the properties panel mounts for a UserTask or StartEvent.
Form schema requirements for Operaton¶
Every form deployed to Operaton must satisfy:
schemaVersion: 16— the version Operaton resolves at runtime.- A
schema.idthat matches thecamunda:formRefvalue in the BPMN XML. - A
Buttoncomponent withaction: "submit"at the end of thecomponentsarray.@bpmn-io/form-jsdoes not inject one automatically; omitting it causes the form to render but never emit thesubmitevent. - Deployed in the same Operaton deployment as the BPMN that references it. The deploy modal in the BPMN Modeler handles this automatically — see BPMN Modeler Implementation — Deploy modal.
CSS¶
@bpmn-io/form-js requires two stylesheet imports:
import '@bpmn-io/form-js/dist/assets/form-js.css';
import '@bpmn-io/form-js/dist/assets/form-js-editor.css';
Both are imported at the top of FormCanvas.tsx. They are bundled by Vite and apply globally to the form editor canvas.