Skip to content
D
Documentation

Edit database models

how-to
5 min readUpdated

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:

bash
npm install @grafloria/element @grafloria/engine @grafloria/renderer

Angular:

bash
npm install @grafloria/angular @angular/common @angular/core @angular/forms @angular/platform-browser @grafloria/element @grafloria/engine @grafloria/renderer rxjs

Qwik:

bash
npm install @grafloria/qwik @builder.io/qwik @grafloria/element @grafloria/engine @grafloria/renderer

React:

bash
npm install @grafloria/react react react-dom @grafloria/element @grafloria/engine @grafloria/renderer

Vue:

bash
npm 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.

ts
import 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.

ts
import { 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);
JavaScript: Customers and Orders cards, the places relationship and the Rename Orders button.

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.

ts
import { 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');
  }
}

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:

TierWhat the reader seesMatching rule
topGold row and “★ BEST” chipThe first highest-scoring candidate, if its score is at least 2
goodGreen rowOther PK/FK flag matches or stronger naming matches
okBlue rowEqual column names, or both names ending in id
noneDimmed rowNo 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 resolvePort that 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.

ts
import { 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

OptionTypeDefaultWhat it does
editablebooleanfalseAdds inline rename, add and delete controls
rowSelectionbooleanEnabledAdds row selection and container CustomEvents; false opts out
Entity heightnumberComputed from columns and editing chromeCaps the card and enables body scrolling
Relationship cardinalityNamed cardinality or { tail: string; head: string }one-to-manyChooses endpoint markers
Relationship fromSide / toSideleft, right, top, bottomright / leftChooses relationship attachment sides
Guidance chipTextstring★ BESTChanges 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?

Edit database models — Grafloria