Use the ER kit for a visual schema editor: render table cards with typed columns and column-level relationships, enable inline edits, cap a long card so its body scrolls, and highlight candidate joins while the reader drags a connection.
erDiagram turns entities and relationships into ordinary specs plus a wiring step. Mount the complete kit spec, rather than passing its nodes and edges separately, so the host also installs row selection and editing.
1. Declare the schema
Install the packages for your framework in your own project.
JavaScript:
bashnpm install @grafloria/element @grafloria/engine @grafloria/renderer
Angular:
bashnpm install @grafloria/angular @angular/common @angular/core @angular/forms @angular/platform-browser @grafloria/element @grafloria/engine @grafloria/renderer rxjs
Qwik:
bashnpm install @grafloria/qwik @builder.io/qwik @grafloria/element @grafloria/engine @grafloria/renderer
React:
bashnpm install @grafloria/react react react-dom @grafloria/element @grafloria/engine @grafloria/renderer
Vue:
bashnpm install @grafloria/vue vue @grafloria/element @grafloria/engine @grafloria/renderer
Create this shared file beside the framework entry below. The data is typed with ErDiagramOptions. CUSTOMERS.id and ORDERS.customer_id attach the relationship to columns, not table centers. The one-to-many markers run from the customer primary key to the order foreign key.
tsimport type { ErDiagramOptions } from '@grafloria/element';
export function schema(): ErDiagramOptions {
return {
editable: true,
entities: [
{
id: 'CUSTOMERS', name: 'Customers',
position: { x: 50, y: 60 }, width: 260, height: 155,
columns: [
{ name: 'id', type: 'int', pk: true },
{ name: 'name', type: 'varchar' },
{ name: 'email', type: 'varchar' },
{ name: 'country', type: 'varchar' },
{ name: 'city', type: 'varchar' },
{ name: 'postal_code', type: 'varchar' },
],
},
{
id: 'ORDERS', name: 'Orders',
position: { x: 410, y: 100 }, width: 280,
columns: [
{ name: 'id', type: 'int', pk: true },
{ name: 'status', type: 'varchar' },
{ name: 'customer_id', type: 'int', fk: true },
{ name: 'total', type: 'decimal' },
],
},
],
relationships: [
{
id: 'customer-orders',
from: 'CUSTOMERS.id', to: 'ORDERS.customer_id',
cardinality: 'one-to-many', label: 'places',
},
],
};
}
The Customers card has more columns than its fixed height can display. Scroll inside its column list; the header and “+ add column” affordance stay outside the scrolling body. Orders uses automatic height.
2. Mount an editable diagram
render returns a live DiagramInstance. Framework kit hosts hand you that same instance through their ready callback or event.
These entries render the same two cards and the places relationship. They also add a toolbar button that uses erTable to rename Orders to Sales orders. The returned ErTable reads the live entity; its spec getter returns a deep copy, not an editable reference. rename() and resize() return Promise<boolean> and route their edits through the history stack.
JavaScript
Mount in a browser. Call the returned cleanup function when your application removes this view.
tsimport { render, erDiagram, erTable } from '@grafloria/element';
import { schema } from './schema';
export function mountDatabaseEditor(parent: HTMLElement): () => void {
const button = document.createElement('button');
button.textContent = 'Rename Orders';
const host = document.createElement('div');
host.style.height = '400px';
parent.append(button, host);
const api = render(erDiagram(schema()), host);
api.fitView(40);
button.onclick = () => { void erTable(api, 'ORDERS').rename('Sales orders'); };
return () => {
api.dispose();
button.remove();
host.remove();
};
}
const parent = document.getElementById('app')!;
export const unmountDatabaseEditor = mountDatabaseEditor(parent);
html<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
Framework hosts
Use GrafloriaDiagramComponent in Angular. In Qwik, use GrafloriaDiagram with spec$ to build the function-bearing kit spec in the browser. React's GrafloriaDiagram accepts spec and onReady; Vue's GrafloriaDiagram accepts :spec and emits ready.
tsimport { Component } from '@angular/core';
import { GrafloriaDiagramComponent } from '@grafloria/angular';
import { erDiagram, erTable } from '@grafloria/element';
import type { DiagramInstance } from '@grafloria/renderer';
import { schema } from './schema';
@Component({
standalone: true,
selector: 'app-database-editor',
imports: [GrafloriaDiagramComponent],
template: `
<button (click)="rename()">Rename Orders</button>
<grafloria-diagram [spec]="spec" (ready)="ready($event)"
style="display:block; height:400px" />
`,
})
export class DatabaseEditorComponent {
readonly spec = erDiagram(schema());
private instance?: DiagramInstance;
ready(api: DiagramInstance): void {
this.instance = api;
api.fitView(40);
}
rename(): void {
if (this.instance) void erTable(this.instance, 'ORDERS').rename('Sales orders');
}
}
tsximport { component$, noSerialize, useSignal, type NoSerialize } from '@builder.io/qwik';
import { GrafloriaDiagram } from '@grafloria/qwik';
import { erDiagram, erTable } from '@grafloria/element';
import type { DiagramInstance } from '@grafloria/renderer';
import { schema } from './schema';
export default component$(() => {
const instance = useSignal<NoSerialize<DiagramInstance>>();
return (
<>
<button onClick$={() => {
if (instance.value) void erTable(instance.value, 'ORDERS').rename('Sales orders');
}}>Rename Orders</button>
<div style={{ height: '400px' }}>
<GrafloriaDiagram
spec$={() => erDiagram(schema())}
onReady$={(api: DiagramInstance) => {
instance.value = noSerialize(api);
api.fitView(40);
}}
/>
</div>
</>
);
});
tsximport { useRef } from 'react';
import { GrafloriaDiagram } from '@grafloria/react';
import { erDiagram, erTable } from '@grafloria/element';
import type { DiagramInstance } from '@grafloria/renderer';
import { schema } from './schema';
const spec = erDiagram(schema());
export default function DatabaseEditor() {
const instance = useRef<DiagramInstance | null>(null);
return (
<>
<button onClick={() => {
if (instance.current) void erTable(instance.current, 'ORDERS').rename('Sales orders');
}}>Rename Orders</button>
<div style={{ height: '400px' }}>
<GrafloriaDiagram spec={spec} onReady={(api) => {
instance.current = api;
api.fitView(40);
}} />
</div>
</>
);
}
vue<script setup lang="ts"> import { shallowRef } from 'vue'; import { GrafloriaDiagram } from '@grafloria/vue'; import { erDiagram, erTable } from '@grafloria/element'; import type { DiagramInstance } from '@grafloria/renderer'; import { schema } from './schema'; const spec = erDiagram(schema()); const instance = shallowRef<DiagramInstance>(); function ready(api: DiagramInstance): void { instance.value = api; api.fitView(40); } function rename(): void { if (instance.value) void erTable(instance.value, 'ORDERS').rename('Sales orders'); } </script> <template> <button @click="rename">Rename Orders</button> <div style="height:400px"> <GrafloriaDiagram :spec="spec" @ready="ready" /> </div> </template>
The framework hosts dispose their instance on unmount. Keep user-facing edits on the live handle instead of replacing the kit spec; changed spec values replace the mounted diagram.
3. Edit and scroll the cards
With editable: true, use the kit's own controls:
- Double-click a header or column name to open the inline editor. Enter commits; Escape cancels.
- Click “+ add column” to insert a column and open its name editor.
- Hover a column and click × to delete it. Deleting an attached column also removes its field port and relationship.
- Click a column once to select the row. The container emits
axk:row-select; use that DOM event for a properties panel, not a framework event the kit host does not declare.
Each edit becomes one undoable step. Surviving field ports move to their columns' new row positions when columns are inserted, removed or reordered. For example, deleting Orders.status moves the customer_id relationship up with that row. See Commands and history for a history toolbar.
To resize a card from your own toolbar, call erTable(instance, 'CUSTOMERS').resize({ height: 180 }). A fixed height caps the column body; omitting the initial entity height uses automatic sizing. Use the handle rather than mutating its copied spec.
Guide candidate joins
bindJoinGuidance listens to the mounted instance's connection lifecycle. Bind it in onReady, ready, or onReady$, or after JavaScript's render() call. Keep its returned handle and call dispose() when that view unmounts.
The ER kit's field ports are hidden relationship attachment points, not visible query-builder grips. A query editor needs visible per-column connection ports; the visual SQL demo shows ports on both sides of each column and binds the shipped guidance function. Its source also implements the SQL pane and join-type inspector.
During a column connection drag, guidance leaves the source table unchanged and tints other tables' rows:
| Tier | What the reader sees | Matching rule |
|---|---|---|
top | Gold row and “★ BEST” chip | The first highest-scoring candidate, if its score is at least 2 |
good | Green row | Other PK/FK flag matches or stronger naming matches |
ok | Blue row | Equal column names, or both names ending in id |
none | Dimmed row | No matching reason |
The strongest naming match is a singular table-name foreign key such as ORDERS.customer_id against CUSTOMERS.id, or equal _id names with an FK flag. Guidance clears its tints and chip when the drag completes or cancels. It ranks candidates; it does not infer SQL or enforce your database's join rules.
Known issue: After a column rename, ER field ports keep their old ids, but the default guidance resolver matches ids against current column names. Until this is fixed, supply
resolvePortthat resolves a port's row position against the live columns.
The intended call is bindJoinGuidance(instance). This complete browser helper supplies the resolver for renamed kit field ports and returns an unbind function. Call it from the host's ready handler when adding join guidance to a view with connection grips.
tsimport { bindJoinGuidance, erRowCenterY, erTable } from '@grafloria/element';
import type { DiagramInstance } from '@grafloria/renderer';
export function attachJoinGuidance(instance: DiagramInstance): () => void {
const guidance = bindJoinGuidance(instance, {
resolvePort(portId, nodeId) {
const node = nodeId
? instance.getModel().getNode(nodeId)
: instance.getModel().getNodeByPortId(portId);
if (!node || !node.getMetadata('kitEntity')) return null;
const port = node.getPort(portId);
const y = port?.layout?.args?.y;
if (port?.layout?.strategy !== 'absolute' || typeof y !== 'number') return null;
const columns = erTable(instance, node.id).spec.columns;
const index = columns.findIndex((_, i) => erRowCenterY(i) === y);
const column = columns[index];
return column ? { nodeId: node.id, column: column.name } : null;
},
});
return () => guidance.dispose();
}
erRowCenterY supplies the kit's row-center coordinate. This resolver uses the updated row position instead of the preserved port id.
Options that matter
| Option | Type | Default | What it does |
|---|---|---|---|
editable | boolean | false | Adds inline rename, add and delete controls |
rowSelection | boolean | Enabled | Adds row selection and container CustomEvents; false opts out |
Entity height | number | Computed from columns and editing chrome | Caps the card and enables body scrolling |
Relationship cardinality | Named cardinality or { tail: string; head: string } | one-to-many | Chooses endpoint markers |
Relationship fromSide / toSide | left, right, top, bottom | right / left | Chooses relationship attachment sides |
Guidance chipText | string | ★ BEST | Changes the best candidate's chip text |
Named cardinalities are one-to-many, one-to-one, many-to-many, one-to-zero-or-many and one-to-one-or-many.
Pitfalls and next steps
- Spell entity ids and column endpoints exactly.
erDiagram()throws for an unknown entity or column while building the spec. - Pass a known kit-node id to
erTable(); it throws for a missing node or a non-ER node. - For Qwik kit construction and resumable state, see Documents and kits.
- Save the live document rather than reconstructing it from framework specs: Save and restore documents.
Try the in-canvas ERD editor for rename/add/delete, or the advanced ER demo for shared PK endpoints, self-references and junction tables.
Was this page helpful?