# Edit database models

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`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-element-diagram-kit-functions#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`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-element-diagram-kit-interfaces#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 title="schema.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`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-element-core#render) returns a live [`DiagramInstance`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-instance-diagraminstance#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`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-element-diagram-kit-functions#ertable) to rename Orders to Sales orders. The returned [`ErTable`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-element-diagram-kit-classes#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.

:::code-group
```ts title="JavaScript"
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);
```
```html title="index.html"
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
```
:::

![JavaScript: Customers and Orders cards, the places relationship and the Rename Orders button.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/8637dabae8821610d969002a2d11d328.png)

### Framework hosts

Use [`GrafloriaDiagramComponent`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-angular-components-classes#grafloriadiagramcomponent) in Angular. In Qwik, use [`GrafloriaDiagram`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-qwik#grafloriadiagram) with `spec$` to build the function-bearing kit spec in the browser. React's [`GrafloriaDiagram`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-react#grafloriadiagram) accepts `spec` and `onReady`; Vue's [`GrafloriaDiagram`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-vue#grafloriadiagram) accepts `:spec` and emits `ready`.

:::code-group
```ts title="Angular"
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');
  }
}
```
```tsx title="Qwik"
import { 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>
    </>
  );
});
```
```tsx title="React"
import { 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 title="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](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/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`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-element-diagram-kit-functions#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](https://grafloria.com/demos/diagrams/query-builder.html) shows ports on both sides of each column and binds the shipped guidance function. Its [source](https://github.com/grafloria/grafloria/blob/cccb505e76c7af26c8bafecaba8146db7d8df928/demos/diagrams/query-builder.html) 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 `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 title="join-guidance.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`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-element-diagram-kit-functions#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](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/documents-and-kits).
- Save the live document rather than reconstructing it from framework specs: [Save and restore documents](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/save-and-restore-documents).

Try the [in-canvas ERD editor](https://grafloria.com/demos/diagrams/erd-editor.html) for rename/add/delete, or the [advanced ER demo](https://grafloria.com/demos/diagrams/er-advanced.html) for shared PK endpoints, self-references and junction tables.
