Skip to content

Example: a form that reads and writes

This page ties the SDK together into one working flow: load data on start, render it, and write new rows back on submit. It is a distilled version of the simple form template — a form that lists products from the base, lets the user pick quantities, and stores the result as an order.

The data flow is always the same four steps:

flowchart LR
    A[init SDK] --> B[listRows: load data]
    B --> C[render UI]
    C --> D[on submit: batchAddRows + addRow]

It assumes the three tables described in Developer setup: Products, OrderItems (link to Products), and Orders (link to OrderItems).

Just need a single file?

If your page is one index.html loading the SDK from a CDN — no build step — start from the low-code quickstart instead. This example covers the modular developer structure: SDK imported from npm, logic split into modules.

One file or modules — a maintainability choice, not a capability one

Modules do not unlock anything a single file cannot do — the low-code quickstart reads and writes linked tables from one index.html too. Splitting into modules pays off as a page grows: easier to navigate, test and reuse. Reach for them when the file gets unwieldy, not when the logic gets ambitious.

Wrap base access in a Context class

As a page grows, split the logic into modules and import the SDK from the npm package. The template's src/esm directory does exactly this. A common pattern is to wrap all base access in a single Context class, keeping SDK calls out of your UI code:

import { HTMLPageSDK } from "seatable-html-page-sdk";

export default class Context {
  async init(options) {
    this.sdk = new HTMLPageSDK(options);
    await this.sdk.init();
  }

  async loadProducts() {
    const res = await this.sdk.listRows({ tableName: "Products" });
    return {
      columns: res.data.metadata,
      rows: res.data.results,
    };
  }

  async submitOrder(orderItemsData) {
    const itemsRes = await this.sdk.batchAddRows({
      tableName: "OrderItems",
      rowsData: orderItemsData,
    });
    const orderItemIds = itemsRes.data.rows.map((row) => row._id);

    await this.sdk.addRow({
      tableName: "Orders",
      rowData: { OrderItems: orderItemIds },
    });
  }
}

The entry point reads the injected dev config and starts the app:

// index.js
import Context from "./context";

document.addEventListener("DOMContentLoaded", async () => {
  const context = new Context();
  await context.init(window.__HTML_PAGE_DEV_CONFIG__ || null);
  const { rows } = await context.loadProducts();
  // ... render rows, wire up submit -> context.submitOrder(...)
});

Linking rows

Notice the two-step write in submitOrder: first create the OrderItems rows with batchAddRows, then read their _id values from res.data.rows and pass them as an array to the Orders row's link column. Link columns always take an array of linked row IDs — see addRow.

Next steps