Skip to content

A form from the table ​

Task: a form for a table without writing each field and each rule again.

yaml
# Recipe: the table's schema gives the action its rules and the form its fields.
paths:
  components: ./components
  migrations: ./migrations

datasources:
  db:
    driver: sqlite
    database: ./data/library.db

The schema already says what a book needs:

sql
CREATE TABLE authors (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL
);

CREATE TABLE books (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    title VARCHAR(120) NOT NULL,
    author_id INTEGER NOT NULL REFERENCES authors(id),
    genre TEXT NOT NULL CHECK (genre IN ('novel', 'poetry', 'essay')),
    pages INTEGER,
    lent BOOLEAN NOT NULL DEFAULT 0
);

INSERT INTO authors (name) VALUES ('Clarice Lispector'), ('Machado de Assis');

<q:action table="books" datasource="db"> reads it and makes a q:param of each column except the key, in the table's order:

ColumnBecomes
title VARCHAR(120) NOT NULLrequired, at most 120 characters
author_id ... REFERENCES authors(id)an integer that must name an existing author
genre ... CHECK (genre IN (...))one of the three
pages INTEGER (accepts NULL)an integer; left blank, it reaches the action as None
lent BOOLEANa box

A ui:form with no fields of its own draws one per param, labelled from the name (author_id becomes "Author"). The author field is a list of the authors' names. null="true" on the query's pages param stores a blank as NULL.

xml
<q:component name="Books">
  <!-- No q:param: each column but the key becomes one, with the rules the
       schema gives it (UI-10). NOT NULL is required, VARCHAR(120) a
       maxlength, the CHECK a list, the REFERENCES a row that must exist. -->
  <q:action name="add" method="POST" table="books" datasource="db">
    <q:query name="added" datasource="db">
      INSERT INTO books (title, author_id, genre, pages, lent)
      VALUES (:title, :author_id, :genre, :pages, :lent)
      <q:param name="title" value="{title}" type="string" />
      <q:param name="author_id" value="{author_id}" type="integer" />
      <q:param name="genre" value="{genre}" type="string" />
      <q:param name="pages" value="{pages}" type="integer" null="true" />
      <q:param name="lent" value="{lent}" type="boolean" />
    </q:query>
    <q:redirect url="/" flash="Added: {title}" />
  </q:action>

  <q:query name="books" datasource="db">
    SELECT b.title, a.name AS author, b.genre FROM books b JOIN authors a ON a.id = b.author_id
    ORDER BY b.id
  </q:query>

  <ui:window title="Library">
    <q:if condition="flash">
      <ui:alert variant="{flashType == 'error' and 'danger' or 'success'}">{flash}</ui:alert>
    </q:if>
    <!-- A form with no fields draws one per param: a list of authors for
         author_id, a list for genre, a box for lent, and the button. -->
    <ui:form on-submit="add" submit="Add book" />
    <ui:table source="{books}" />
  </ui:window>
</q:component>
xml
<q:test name="the form draws a field per column, labelled from its name" page="/">
  <test:visit />
  <test:expect text="Title" />
  <test:expect text="Author" />
  <test:expect text="Machado de Assis" />
  <test:expect text="Add book" />
</q:test>

<q:test name="a book is added" page="/">
  <test:submit action="add" title="Dom Casmurro" author_id="2" genre="novel" pages="256" />
  <test:expect redirect="/" flash="Added: Dom Casmurro" />
  <test:expect table="books" count="1" where="title = 'Dom Casmurro' AND pages = 256 AND lent = 0" />
</q:test>

<q:test name="the rules come from the schema" page="/">
  <test:submit action="add" title="" author_id="9" genre="comic" pages="many" />
  <test:expect error="title" message="Required" />
  <test:expect error="author_id" message="Must name an existing row of authors (no id = 9)" />
  <test:expect error="genre" message="Must be one of: novel, poetry, essay" />
  <test:expect error="pages" message="Must be an integer, got 'many'" />
  <test:expect table="books" count="0" />
</q:test>

<q:test name="a blank optional column is stored as NULL" page="/">
  <test:submit action="add" title="A hora da estrela" author_id="1" genre="novel" pages="" />
  <test:expect table="books" count="1" where="pages IS NULL" />
</q:test>
text
tests/books.test.q
  PASS  the form draws a field per column, labelled from its name
  PASS  a book is added
  PASS  the rules come from the schema
  PASS  a blank optional column is stored as NULL
4 passed, 0 failed

A q:param written in the action wins over the schema's, and columns="a,b" keeps only those columns. See UI-10.

MIT Licensed · Built with VitePress