List and detail development and verification
한국어. Display formats define each format, the accepted input and the markup. The specification structure defines list and detail fields and member order.
Lists and details
A list and a detail are read-only displays. A list declares columns and receives an array of
rows; a detail declares fields and receives one record. The rows or the record are queried, filtered,
sorted and paginated before they are passed in. The generators do not access a
database and do not derive a page or a total from the rows.
Each runtime provides two levels:
- A model function composes the declaration, evaluates conditions and appearance, translates
content and formats every value. A detail model is
{ fields, actions, design }. - A render function builds the model and writes HTML from it.
Invalid input fails with code INVALID_FORM_INPUT and the messages listed under
input. An unresolved composition reference and a forbidden key
fail with ComposeLoadError. A declaration that does not follow the
declaration rules fails with INVALID_FORM_INPUT, so a
list or detail is rendered only from a specification of the documented shape.
Options
| Option | JavaScript and PHP | Go | Rust | Detail |
|---|---|---|---|---|
Content language, default ko |
language |
Language |
language |
used |
| Context for visibility expressions | data |
Data |
data |
used |
| Current page | page |
Page |
page |
ignored |
| Total record count | total |
Total |
total |
ignored |
table or card layout |
layout (render only) |
Layout |
layout |
ignored |
| Composition files | files |
Files |
files |
used |
| Base path for references | basepath |
Basepath |
basepath |
used |
| Composition loader | loader (JavaScript only) |
Loader |
loader |
used |
A detail neither checks nor uses page, total and layout. In TypeScript,
BuildDetailOptions omits them from the type. Go DetailOptions is the same type as
ListOptions, and Rust DetailOptions is the same type as ListOptions.
JavaScript
@polyspec/crudui-generator-core exports buildList(spec, rows, options) and
buildDetail(spec, record, options). @polyspec/crudui-generator-html exports
renderList(spec, rows, options) and renderDetail(spec, record, options), which return HTML
strings with image preload links before the list or detail. @polyspec/crudui-validator exports
validateList(spec, options) and validateDetail(spec, options).
import { buildDetail, buildList } from '@polyspec/crudui-generator-core';
import { renderDetail, renderList } from '@polyspec/crudui-generator-html';
import { validateDetail, validateList } from '@polyspec/crudui-validator';
const listSpec = {
pagination: true,
columns: {
name: { field: 'name', label: { ko: '이름', en: 'Name' } },
joined: { field: 'joined', label: 'Joined', format: { type: 'date', pattern: 'YYYY-MM-DD' } },
},
};
const rows = [{ name: 'Ada', joined: '2026-01-02T09:30:00Z' }];
validateList(listSpec);
const listModel = buildList(listSpec, rows, { language: 'en', page: 1, total: 1 });
const listHtml = renderList(listSpec, rows, { language: 'en', page: 1, total: 1, layout: 'card' });
const detailSpec = { fields: { name: { field: 'name', label: 'Name' } } };
validateDetail(detailSpec);
const detailModel = buildDetail(detailSpec, rows[0], { language: 'en' });
const detailHtml = renderDetail(detailSpec, rows[0], { language: 'en' });
The React, Vue and Svelte packages re-export buildList and buildDetail and provide
server rendering and components:
| Package | Server rendering | Components |
|---|---|---|
@polyspec/crudui-generator-react/server |
renderList and renderDetail return strings |
<List vm layout />, <Detail vm /> from @polyspec/crudui-generator-react |
@polyspec/crudui-generator-vue |
renderList and renderDetail return promises |
List(vm, layout) and Detail(vm) return VNodes |
@polyspec/crudui-generator-svelte |
renderList and renderDetail return strings |
List with vm and layout props, Detail with a vm prop |
A component receives a model built with buildList or buildDetail. The render functions
take the same options as the HTML renderer.
PHP and the PHP extension
Polyspec\Crudui\Generator::buildList($spec, $rows, $options) returns the evaluated list model. Polyspec\Crudui\Generator::renderList($spec, $rows, $options) renders rows,
Generator::renderDetail($spec, $record, $options) renders one record and
Generator::buildDetail($spec, $record, $options) returns the detail model. Polyspec\Crudui\Validator::validateList($spec, $options) and
Validator::validateDetail($spec, $options) validate structure. Generation options are
language, data, page, total, layout, files and basepath; validation options are
files and basepath. Run the example from the repository root:
<?php
require 'vendor/autoload.php';
use Polyspec\Crudui\Generator;
use Polyspec\Crudui\Validator;
$listSpec = json_decode('{"pagination":true,"columns":{"name":{"field":"name","label":"Name"}}}', false, 512, JSON_THROW_ON_ERROR);
$rows = [json_decode('{"name":"Ada"}', false, 512, JSON_THROW_ON_ERROR)];
Validator::validateList($listSpec);
$listHtml = Generator::renderList($listSpec, $rows, ['language' => 'en', 'page' => 1, 'total' => 1, 'layout' => 'card']);
$detailSpec = json_decode('{"fields":{"name":{"field":"name","label":"Name"}}}', false, 512, JSON_THROW_ON_ERROR);
Validator::validateDetail($detailSpec);
$detailModel = Generator::buildDetail($detailSpec, $rows[0], ['language' => 'en']);
$detailHtml = Generator::renderDetail($detailSpec, $rows[0], ['language' => 'en']);
The crudui extension registers the same Polyspec\Crudui\Generator and Polyspec\Crudui\Validator classes and
methods. The same code returns the same HTML and models with the extension loaded, for example
with php -d "extension=$(pwd)/var/php-ext/polyspec_crudui.so". The
PHP API contract defines which PHP values are objects.
Go
The github.com/polyspec/crudui/packages/generator/go package provides
BuildList(spec, rows, ListOptions), RenderList(spec, rows, ListOptions),
BuildDetail(spec, record, DetailOptions) and RenderDetail(spec, record, DetailOptions).
Specifications, rows and records are *generator.Object values. ListOptions has the fields
Language, Data, Page, Total, Files, Loader, Basepath and Layout; Data, Page,
Total and Layout accept decoded values and are checked by the input rules. The
validator/validate package provides ValidateList and ValidateDetail with Options
(Files, Basepath), and ValidateListJSON and ValidateDetailJSON for JSON bytes. A
structure failure is returned as an error.
package main
import (
"fmt"
generator "github.com/polyspec/crudui/packages/generator/go"
"github.com/polyspec/crudui/packages/validator/go/validator/validate"
)
func object(source string) *generator.Object {
decoded, err := generator.DecodeJSON([]byte(source))
if err != nil {
panic(err)
}
return decoded.(*generator.Object)
}
func main() {
listSpec := object(`{"pagination":true,"columns":{"name":{"field":"name","label":"Name"}}}`)
if _, err := validate.ValidateList(listSpec, validate.Options{}); err != nil {
panic(err)
}
rows := []*generator.Object{generator.NewObject("name", "Ada")}
listHTML, err := generator.RenderList(listSpec, rows, generator.ListOptions{
Language: "en", Page: 1, Total: 1, Layout: "card",
})
if err != nil {
panic(err)
}
detailSpec := object(`{"fields":{"name":{"field":"name","label":"Name"}}}`)
if _, err := validate.ValidateDetail(detailSpec, validate.Options{}); err != nil {
panic(err)
}
detailModel, err := generator.BuildDetail(detailSpec, rows[0], generator.DetailOptions{Language: "en"})
if err != nil {
panic(err)
}
detailHTML, err := generator.RenderDetail(detailSpec, rows[0], generator.DetailOptions{Language: "en"})
if err != nil {
panic(err)
}
fmt.Println(listHTML, detailHTML, detailModel.Len())
}
Run it in a module that requires the generator and validator modules.
Rust
The polyspec-crudui-generator crate exports build_list, render_list, build_detail,
render_detail, ListOptions and DetailOptions. ListOptions has the fields files,
loader, basepath, language, data, page, total and layout; data, page, total
and layout are JSON values, and the default language is ko. The polyspec-crudui-validator crate
exports validate_list with ValidateListOptions and validate_detail with
ValidateDetailOptions (files, loader, basepath). A structure failure is
Err(ComposeLoadError).
use polyspec_crudui_generator::{build_detail, render_detail, render_list, DetailOptions, ListOptions};
use polyspec_crudui_validator::{validate_detail, validate_list, ValidateDetailOptions, ValidateListOptions};
use serde_json::json;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let list_spec = json!({"pagination": true, "columns": {"name": {"field": "name", "label": "Name"}}});
validate_list(&list_spec, &ValidateListOptions::default())?;
let rows = vec![json!({"name": "Ada"})];
let list_html = render_list(
&list_spec,
&rows,
&ListOptions {
language: "en".into(),
page: json!(1),
total: json!(1),
layout: json!("card"),
..Default::default()
},
)?;
let detail_spec = json!({"fields": {"name": {"field": "name", "label": "Name"}}});
validate_detail(&detail_spec, &ValidateDetailOptions::default())?;
let options = DetailOptions {
language: "en".into(),
..Default::default()
};
let detail_model = build_detail(&detail_spec, &rows[0], &options)?;
let detail_html = render_detail(&detail_spec, &rows[0], &options)?;
println!("{list_html}\n{detail_html}\n{}", detail_model["fields"][0]["key"]);
Ok(())
}
Use serde_json with the preserve_order feature.
Structure validation
Structure validation composes a list or detail declaration and rejects forbidden keys. It does
not validate rows or a record. A valid structure returns { valid: true, errors: [] } in
JavaScript and PHP and a valid ValidationResult in Go; Rust returns Ok(()). A composition or
forbidden-key failure is a ComposeLoadError: JavaScript and PHP throw it, Go returns it as the
error and Rust returns it as Err. The model and render functions run the same composition and
forbidden-key scan on the columns, fields and search declarations and then check the
declaration rules.
Examples
| Example | List and detail |
|---|---|
| Go server | / renders the form, /list renders the stored record with RenderList and /detail renders it with RenderDetail. |
| PHP pages | / renders the form, ?view=list renders the stored record with Generator::renderList and ?view=detail with Generator::renderDetail. The file runs with the PHP implementation or the extension. |
| Rust program | Renders a form, turns each company row into a list row, renders the list with render_list and each company with render_detail, and prints one HTML document. |
| Cross-check console | The list tab validates list structure in four languages and renders rows in React, Vue and Svelte. The detail tab validates detail structure in four languages and renders a record in the three frameworks. |
(cd packages/generator/go && go run ./examples/server -data /tmp/crudui-go-record.json)
CRUDUI_DATA_FILE=/tmp/crudui-php-example.json php -S 127.0.0.1:8082 -t packages/generator/php/examples
cargo run --locked --manifest-path packages/generator/rust/Cargo.toml --example form
The Go server listens on 127.0.0.1:8087. The Go and PHP examples store one record, so the
list has that record as its only row, and no rows before the first save. The package READMEs
describe each page and its formats.
Checks
Shared fixtures define the expected results in every runtime:
| Fixture | Content |
|---|---|
list-render |
Specification, rows, options and the expected list HTML or error |
detail-render |
Specification, record, options and the expected detail HTML or error |
list-validity |
List declarations and their structure validation results |
detail-validity |
Detail declarations and their structure validation results |
npm run test:forms
make crudui-test-native
(cd examples/cross-check-console/server && npm test)
make docs-check
test:forms runs the list and detail conformance tests of React, Vue, Svelte and the HTML
renderer, which compare normalized bodies with the render fixtures. make crudui-test-native runs the
PHP, Go and Rust package tests and the
native generator suite, which compares complete
original list and detail HTML and detail models from JavaScript, the HTML renderer, PHP, the PHP
extension, Go and Rust. The console suite builds the Go and Rust validator programs and tests the
gateway, including the list and detail endpoints.
Run the structure validation cases of each validator from the repository root:
npm test -w @polyspec/crudui-validator -- src/validate-list/validate-list.conformance.test.ts src/validate-detail/validate-detail.conformance.test.ts
COMPOSER_VENDOR_DIR=../../../vendor node node_modules/@polyspec/kit/dist/run-tests.mjs phpunit --cwd packages/validator/php -- --filter 'ListValidateConformanceTest|DetailValidateConformanceTest'
node node_modules/@polyspec/kit/dist/run-tests.mjs go --cwd packages/validator/go -- ./validator/validate -run 'TestValidateListMatchesFixture|TestValidateDetailMatchesFixture'
node node_modules/@polyspec/kit/dist/run-tests.mjs cargo -- --locked --manifest-path packages/validator/rust/Cargo.toml --test list_validity_conformance --test detail_validity_conformance