CRUDUI

Form development and verification

한국어. The contract is form runtime.

Setup

Run commands from the repository root. Install Node.js and npm, then run:

make install-tools TOOLS=npm
make install-packages
make install-tools
export PATH="$PWD/var/tools/bin:$PATH"
npm ci --strict-allow-scripts
npm run build

Dependency updates resolve the version ranges in package manifests and update the lock file with npm. Review the resulting graph and run the checks below. Review install-script changes and use npm install-scripts approve <package> to update the independent graph's root allowScripts field with exact versions. Run npm rebuild to execute newly approved scripts in an existing installation.

PHP with Composer dependencies, Go and Rust are needed for four-language checks. Install PHP dependencies with composer install in packages/validator/php. No container is required when these toolchains are available locally.

Build before loading data

import { compileForm, createForm, sequenceRowKey } from '@polyspec/crudui-generator-core';
import { Form } from '@polyspec/crudui-generator-react';

const template = compileForm({
  type: 'group',
  properties: {
    stores: {
      type: 'group', multiple: { copy: true, sortable: true },
      properties: { name: { type: 'text', validate: { required: true } } },
    },
  },
}, { keyPrefix: 'form' });
const cached = JSON.stringify(template);
const form = createForm(JSON.parse(cached));

function StoreForm() {
  return <Form form={form} />;
}

form.setData({ stores: { [sequenceRowKey(42)]: { name: 'Store' } } });
const copied = form.copyRow('stores', sequenceRowKey(42));
form.rekeyRow('stores', copied, sequenceRowKey(43));
const submission = form.getData();

Keep the template in the shared cache and create an independent form instance for each rendered form. Call setData when a record load finishes. Vue and Svelte also accept the form prop. Framework packages export the same core functions. React exports renderForm(form) from @polyspec/crudui-generator-react/server; Vue and Svelte export their rendering functions from their package entries. Vue returns a promise. Compile $ref files before rendering.

A server sends the complete form: the form element, hidden fields, the entered values, the errors and the buttons come from one call, and the rest of the page writes no part of the form itself. Pass the validation result's errors unchanged or with replacement texts:

import { validate } from '@polyspec/crudui-validator';
import { renderForm } from '@polyspec/crudui-generator-html';

const result = validate(spec, submitted);
const form = createForm(template, submitted, { language: 'en' });
const html = renderForm(form, {
  action: { method: 'post', url: '/members' },
  hidden: { _csrf: token },
  formErrors: result.valid ? [] : ['Check the marked fields.'],
  errors: result.errors,
});

PHP calls Generator::renderForm($form, $options) with the same members, Go RenderForm(form, options) and Rust render_form(&form, Some(&options)) with an ordered JSON object. Connect a browser binding to the crudui-form element, not to the form element: connectForm synchronizes every named control inside its element with the instance data.

For framework-independent HTML, use the peer renderer package:

import { connectForm, patchContent } from '@polyspec/crudui-generator-core';
import { renderForm, renderList } from '@polyspec/crudui-generator-html';

patchContent(host, renderForm(form));
const connection = connectForm(host, form);
form.subscribe(() => {
  patchContent(host, renderForm(form));
  connection.sync();
});
const listHtml = renderList(listSpec, rows, { layout: 'table' });

patchContent replaces the host's content with the new markup and keeps every node the markup still contains, so a re-render keeps the focused control, its selection and an input method composition; connection.sync() then sets the live control values. A script in new markup runs once, after that markup is in place; a kept script never runs again (script rule). Render the first client markup with patchContent as well: markup set through innerHTML never runs its scripts. The HTML renderer returns fragments and does not create the outer form element, bind browser events, validate data or load records.

Validate submission with validate(spec, submission) from @polyspec/crudui-validator and the original spec. The server assigns saved sequences; apply each returned key to its specific collection path. Never replace a token across the entire data object.

Checks

npm run test:forms
npm test -w @polyspec/crudui-validator
make docs-check

Run the server validation cases from the repository root:

COMPOSER_VENDOR_DIR=../../../vendor node node_modules/@polyspec/kit/dist/run-tests.mjs phpunit --cwd packages/validator/php -- --filter ValidateConformanceTest
node node_modules/@polyspec/kit/dist/run-tests.mjs go --cwd packages/validator/go -- ./validator/validate
node node_modules/@polyspec/kit/dist/run-tests.mjs cargo -- --locked --manifest-path packages/validator/rust/Cargo.toml --test validate_conformance

test:forms builds current packages and runs core, SSR and mounted DOM tests. The shared DOM scenario covers late data injection, edits, nested row operations, saved keys, checkboxes, dates, language fields and conditional display. DOM tests use jsdom; they do not establish external editor or browser file-picker behavior. tests/form-styles.test.mts checks the layout of crudui.css in Chromium, Firefox and WebKit: sticky header stacking, the level label and its data-crudui-stuck fallback, one-border seams, row card edges and focus scrolling, each in a page, a scrolling box and a frame. Every engine runs the same scenarios; Chromium and Firefox are driven by Puppeteer, WebKit by Playwright, each at the build that the locked package pins; node scripts/install-browsers.mts chrome firefox webkit installs them (on Linux with --with-deps for WebKit). A missing browser fails the run with the command that installs it; no engine is skipped. The browser validation check of packages/form-binding/tests/browser.test.ts runs in the same three engines. tests/viewport.test.mts places the expected HTML of the 432 shared render cases at 360 and 1280 CSS pixels and requires no horizontal overflow of the document, and tests/tailwind-styles.test.mts compiles the import of crudui.css into the layer components with Tailwind CSS and requires the computed styles of crudui.css, both in the same three engines. No file is generated from crudui.css, so a change of crudui.css needs no other file to be committed. tests/widget-script-runs.test.mts runs the script rule in the same three engines for the HTML, React, Vue and Svelte renderers, each rendered in the browser and rendered on the server and hydrated: a form row script and an html list cell script run once on the first render and once for each added row, and never on typing, a copied or moved row, a reload or a list re-render; the script of a list script action runs on each click of its button. Record current results in features and changelog.

The Svelte package build emits JavaScript, preprocessed Svelte components and TypeScript declarations into dist. Public export entries reference these packaged files. The package build of a project compiles components for its browser or SSR target.

Run npm run test:packages to build and pack all JavaScript packages, install them into an isolated install project, check exported files and declarations, and compile a production application using all three form components. The install project installs with npm ci --offline from a lock that scripts/install-lock.mts derives from the root package-lock.json: the packed packages as file: tarballs and the root lock entries of every registry package that npm resolves for the install project, so it installs the releases of the root lock from the npm cache of make install and resolves no range against a registry. A passing check removes the temporary install project. A failing check keeps it with failure.log and prints its path.

Package declaration checks

The JavaScript bundler generates runtime modules. The TypeScript compiler generates declarations from public entries with noEmitOnError, independently of the bundler. Svelte components and declarations use the package compiler.

npm run test:build
npm run test:build:repeat
npm run test:packages

See the package build contract and build checks for the verification scope.