Form markup
한국어. This document defines how evaluated form nodes are rendered. React, Vue, Svelte, the HTML renderer and the native renderers produce the same markup. Data, row operations and view state are defined in the form runtime.
Class names
A class name has one of three forms. Each separator has one meaning.
- CUMK-001
crudui-{block}— Meaning: Block: an independent component; Example:crudui-node,crudui-controls - CUMK-002
crudui-{block}__{element}— Meaning: Element (__): a part that exists only inside its block; Example:crudui-node__header,crudui-node__label - CUMK-003
crudui-{block}--{modifier}— Meaning: Modifier (--): a kind or variant of the block; Example:crudui-node--row,crudui-node--sticky
Words inside a name are joined by one hyphen (lang-item). A modifier is always
used together with its block class (class="crudui-node crudui-node--row").
An element name has one level; elements have no modifiers. Classes carry style
only: browser behavior reads the data and ARIA attributes below, never classes.
One exception has a fixed boundary: the browser validation
binding finds the slots it replaces by the classes crudui-form__body, crudui-form__errors,
crudui-node__body and crudui-node__errors, because these slots carry no data or ARIA
attribute. A test of the binding fails when its source names another class. The exception ends
when the renderers write a data attribute on these slots.
Widgets follow the same forms; the only other classes a renderer writes are
valid-target, which marks the controls whose values the rules check, and
valid-target-async, which marks a select with a dynamic source (see
browser validation), the editor
hosts tinymcearea, summernote, contentjs and tuiarea, and the classes a
spec declares in design.
Blocks and attributes
-
CUMK-004
crudui-form: Form root with thecrudui-form__descriptionparagraph of a non-empty root description,crudui-form__errorsholding acrudui-form__errorparagraph per form error,crudui-form__bodyandcrudui-form__footerholding the form buttons. -
CUMK-005
crudui-node: One data node; see the kinds below. -
CUMK-006
crudui-controls: A button group withrole="group"and an accessible name. -
CUMK-007
crudui-action: A button: an action withdata-crudui-action, a form button or the button of a button field;crudui-action--textshows its label as text. -
The structure map (
crudui-outline) and the current data view (crudui-data) are blocks of the TypeScript HTML renderer, documented inpackages/html/README.md. -
The structured condition editor (
crudui-condition) is a block of the widget of a structured condition field, defined in structured condition editor (CUMK-067, CUMK-068). -
CUMK-010
crudui-widget: A control with its__affixprepend and append texts, a file widget's__buttonand a range widget's__output;--searchholds a search select,--rangeholds a range control,--conditionholds a structured condition editor and--unsupportedmarks a type without a widget. -
CUMK-011
crudui-input: A native input, textarea or select;--selectfor a select,--filefor a file input,--rangefor a range input,--conditionfor the textarea of a condition field and--switchfor the checkbox input of a switch. -
CUMK-012
crudui-choices: Radio (choice) or checkbox (multichoice) options: each__inputis followed by its__label;--multiplewraps checkbox options. Each choice may add its own label class and style and input attributes (choice appearance). -
CUMK-013
data-field-path: Data path of field, group, collection and lang nodes, and of structure-map rows -
CUMK-014
data-crudui-row-key: Row key of a row node -
CUMK-015
data-lang: Language code of a lang-item node -
CUMK-016
data-crudui-action: Operation of a button -
CUMK-017
hidden:design.showis false; a collapsed row body; the summary of an expanded row -
CUMK-018
aria-expanded,aria-controls: Row toggle state and the controlled body -
CUMK-019
step="any": Every number control (number,integer,float,decimal); see below -
CUMK-069 A number in a control value is written as its canonical text: the shortest text that reads back as the same double
A button's collection path is the nearest [data-field-path] at or above it. Its
row key is the nearest [data-crudui-row-key] inside that element; without one
the action applies to the collection.
Validation rules are not written as native constraint attributes; the
validation rules decide validity. A number control carries
step="any" so native constraint validation accepts every number: without it the
default step of 1 counts from the value attribute, and a stored value such as
2886.5 would make every whole number a step mismatch. The step rule owns
increments. A range control is the one exception.
Nodes
Every data node is crudui-node with one kind modifier and the slots
crudui-node__header, crudui-node__body, crudui-node__errors and crudui-node__footer, in
that order. The header is present only with content, the errors slot only with errors (one
crudui-node__error paragraph per message, see complete form)
and the footer only with controls. Child nodes are
placed only in the body, header parts only in the header, and controls only in
the header or footer. design.wrapper applies to the node root, design.label
to the header, design.group to the body of a group or group row and to the crudui-choices
element of a choice or multichoice field, design.class
to the control and design.prepend to the widget prepend.
- CUMK-020
field— Header:__labeland__description; Body: Widget; the node hascrudui-node--inlinein an inline layout - CUMK-021
group— Header:__labeland__description; Body: Child nodes; the node hascrudui-node--linewhen it declares the line layout, and alsocrudui-node--inlinein an inline layout - CUMK-022
collection— Header:__label,__descriptionand__count; Body: Row nodes; the footer holds theadd-rowcontrol when there are no rows - CUMK-023
row— Header: Toggle,__label,__number,__title,__summary, controls; Body: Widget of a scalar row, or child nodes of a group row - CUMK-024
lang— Header:__label,__descriptionand__title(lang.title); Body: lang-item nodes; the node hascrudui-node--framedwhenlang.frameis true (the default) - CUMK-025
lang-item— Header:__labelwith the language code; Body: Widget
A label is a label element targeting the control when the body has exactly one
label target; otherwise it is a span. A checkbox or switcher caption is the
input's own label inside the body, and its header holds only a description, except in an inline
layout, where the header holds the label. A
hidden field has no header. A row with language: 'en':
<div class="crudui-node crudui-node--row" data-crudui-row-key="__0000000000001__">
<div class="crudui-node__header">
<button type="button" class="crudui-action" data-crudui-action="toggle-row"
aria-expanded="true" aria-controls="crudui:companies.__0000000000001__:body" aria-label="Expand or collapse"></button>
<span class="crudui-node__label">Company</span>
<span class="crudui-node__number">1</span>
<span class="crudui-node__title">ACME</span>
<span class="crudui-node__summary" hidden="">Nested rows: 2</span>
<div class="crudui-controls" role="group" aria-label="Row controls">…</div>
</div>
<div class="crudui-node__body" id="crudui:companies.__0000000000001__:body">…</div>
</div>
Declared attributes
A field declares attributes for its control with design.attributes and for its node root with
design.wrapper.attributes (schema). The control is the element
that design.class targets: the input, select or textarea of a widget, the file input of an
image, file or cover field, the div of a display field (dummy, image-viewer), the button of
a button or action field and the input of a checkbox or switcher. For a choice or multichoice field, whose
design.class targets the option labels, the control is every option input. A field with lang
writes the control attributes on the control of every language item, and a repeated scalar field
on the control of every row. The root of a field, group, collection or lang node takes the
wrapper attributes; row and lang-item nodes take none. A dynamic choice source renders no
option input and therefore no control attributes.
The models carry the declared attributes in declaration order:
- CUMK-026 A node model has them as
attributesafterstyleand beforehidden. - CUMK-027 A checkbox model has them as
attributesaftercaption. - CUMK-028 A widget model appends them to the attribute list of its control:
attrs, orextra.filefor afilelayout. Achoiceslayout keeps them inextra.optionafterextra.input.
Each member is present only when the declaration has at least one attribute. Every renderer writes
the declared attributes after the attributes CRUDUI writes on the element, in declaration order:
after hidden on a node root, and after data-is-default and checked on an option input that
has behavior attributes. React's server rendering is the reference serialization, so two
placements of the reference apply: a control without behavior attributes writes style after
the attribute list, and an input without behavior attributes writes name, checked and
value last. For example, a text field with design.attributes: { data-setting: theme, aria-describedby: theme-help } writes:
<input type="text" class="valid-target crudui-input" data-name="theme" data-rule-name="theme"
data-default="" id="crudui:theme" data-setting="theme" aria-describedby="theme-help"
name="theme" value=""/>
Layout
A group declares the layout of its fields with design.layout (schema). The
node model writes the layout as modifiers at the start of the root class (className), before the
design.wrapper class, as it writes crudui-node--framed:
- CUMK-029 A field node in an inline layout has
crudui-node--inline. - CUMK-030 A group that declares
linehascrudui-node--line, preceded bycrudui-node--inlinewhen the group is in an inline layout.
The stylesheet lays an inline node out as a grid of the label column (--crudui-label-width) and the
control column. The header takes no box (display: contents), so the label is in the label column
and the description in the control column below the body, followed by the errors; a class or style
of design.label that draws a box around the header therefore has no box to draw on. The label is
at least one control high and centers its text on the first line of the control, and the body of a
checkbox or switcher field is one control high and centers its input. A line group places
the child nodes of its body in one wrapping row; each child is as wide as its content, and a child
declares design.wrapper.style: "flex: 1" to take the remaining width. The example of the schema
writes:
<div class="crudui-node crudui-node--field crudui-node--inline" data-field-path="appearance.theme">
<div class="crudui-node__header"><label class="crudui-node__label"
for="crudui:appearance.theme">Theme</label><p class="crudui-node__description">Applies to every
window.</p></div>
<div class="crudui-node__body">…</div>
</div>
<div class="crudui-node crudui-node--group crudui-node--inline crudui-node--line"
data-field-path="appearance.font">
<div class="crudui-node__header"><span class="crudui-node__label">Font</span></div>
<div class="crudui-node__body"><div class="crudui-node crudui-node--field"
data-field-path="appearance.font.family">…</div>…</div>
</div>
In an inline layout, a checkbox or switcher field with a label writes the label in the header,
as every other field does: a label element targeting the input, followed by the description. Its
checkbox model has no caption member, so the body holds the input alone and the input starts the
control column. A checkbox or switcher field without a label, and every checkbox or switcher field
outside an inline layout, keeps the caption label in the body. A switcher sync with the label
Sync and the description Keeps every window in step. in the inline group look writes:
<div class="crudui-node crudui-node--field crudui-node--inline" data-field-path="look.sync">
<div class="crudui-node__header"><label class="crudui-node__label" for="crudui:look.sync">Sync</label><p
class="crudui-node__description">Keeps every window in step.</p></div>
<div class="crudui-node__body"><input class="valid-target crudui-input crudui-input--switch"
id="crudui:look.sync" type="checkbox" role="switch" name="look[sync]" value="1"/></div>
</div>
Choice appearance
A choice of a choice or multichoice field may declare class, style and attributes
(schema). The label of the choice takes the class after its label
classes (crudui-choices__label and design.class) and the style after the class. The input of the
choice takes the attributes after the field's control attributes; a name declared in both keeps the
position of the field's attribute and takes the choice's value. The crudui-choices element takes
the class of design.group after its own classes and the style of design.group. An option model
has the members className, style and attributes after id, each present only when declared;
style is normalized as a design style is. With the data accent: green, the example of the schema
writes:
<div class="crudui-choices swatches"><input data-name="accent" data-rule-name="accent" type="radio"
autoComplete="off" class="valid-target crudui-choices__input" id="crudui:accent:0"
data-is-default="" aria-label="Blue accent" name="accent" value="blue"/><label
for="crudui:accent:0" class="crudui-choices__label swatch"
style="--swatch-bg:#1d4ed8"><span>Blue</span></label><input data-name="accent"
data-rule-name="accent" type="radio" autoComplete="off" class="valid-target crudui-choices__input"
id="crudui:accent:1" data-is-default="" name="accent" checked="" value="green"/><label
for="crudui:accent:1" class="crudui-choices__label swatch"
style="--swatch-bg:#15803d"><span>Green</span></label></div>
Choice groups
The choice list of a select field may contain groups (schema). The
select writes the options in written order: a plain choice as an option element, and the
choices of a group as the option elements of one optgroup element whose label attribute is
the translated label of the group. A select with behavior attributes writes the groups in its raw
markup the same way. An option model of a choice inside a group has the member group after its
other members, an object with index, the zero-based position of the group in items, and
label, the translated label; the renderers write the runs of consecutive options of one group or
outside groups. With the data region: eu-north, the example of the schema writes:
<div class="crudui-widget"><select name="region" class="valid-target crudui-input crudui-input--select"
data-name="region" data-rule-name="region" data-default="" id="crudui:region"><option
value="auto">Automatic</option><optgroup label="Europe"><option value="eu-west">West</option><option
value="eu-north" selected="">North</option></optgroup><optgroup label="Asia"><option
value="ap-east">East</option></optgroup></select></div>
Range fields
A range field (schema) renders a crudui-widget crudui-widget--range
widget: the prepend affix, an input of type range, a crudui-widget__output output element
for that input and the append affix, which names the unit. The input has the class
valid-target crudui-input crudui-input--range and the min, max and step attributes, the
canonical texts of validate.range and validate.step. They are the only validation parameters
written as native attributes: a slider has no positions without them, and every position it
offers passes the range and step rules. The output shows the value the form was rendered with;
a renderer that renders again after a change, as connectForm does, shows the new value. An empty
value is written as an empty value, which the browser shows at the middle of the bounds, and an
empty output.
The widget model has the layout range, the tag input, the input attributes as attrs and the
output text as text. A field volume with default: 50, append: "%" and
validate: { range: [0, 100], step: 5 } writes:
<div class="crudui-widget crudui-widget--range"><input type="range" min="0" max="100" step="5"
class="valid-target crudui-input crudui-input--range" data-name="volume" data-rule-name="volume"
data-default="50" id="crudui:volume" name="volume" value="50"/><output
class="crudui-widget__output" for="crudui:volume">50</output><span
class="crudui-widget__affix">%</span></div>
Condition fields
A condition field (schema) renders the widget of a textarea field:
the prepend affix, a textarea and the append affix inside crudui-widget. The textarea has the
class valid-target crudui-input crudui-input--condition followed by the class of design.class,
rows="5" and data-crudui-value="json", which marks its text as a JSON text for the browser
bindings (condition values); then come the style, the event
attributes and the data attributes of a textarea. Its text is the condition text of the field value,
or of default when the data has no value for the field, and data-default is the condition text of
default:
- CUMK-066 The condition text of a value: a string is itself,
nulland an absent value are empty, and any other value is its JSON text as ECMAScript'sJSON.stringifywrites it without indentation. That text has no whitespace, writes the members of an object in their data order, writes a number in the shortest form of ECMAScript'sNumber::toStringand writestrue,falseandnull. A string is in double quotes:"and\are escaped by a backslash; U+0008, U+0009, U+000A, U+000C and U+000D are written as\b,\t,\n,\fand\r; every other character below U+0020 and every unpaired surrogate is written as\uand four lowercase hexadecimal digits; and every other character is written as itself.
The widget model has the layout widget, the tag textarea, the textarea attributes as attrs and
the condition text as text. The stylesheet writes the text in a monospace font. A field pass with the label Pass condition,
validate: { required: true, conditionTree: judge } and the data
{ "any": [{ "field": "clues.size", "op": "ge", "value": 2 }, { "judge": "Jian finds it." }] }
writes:
<div class="crudui-widget"><textarea name="pass" class="valid-target crudui-input crudui-input--condition"
rows="5" data-crudui-value="json" data-name="pass" data-rule-name="pass" data-default=""
id="crudui:pass">{"any":[{"field":"clues.size","op":"ge","value":2},{"judge":"Jian finds it."}]}</textarea></div>
Structured condition editor
A condition field with design.editor: structured (schema)
writes the widget crudui-widget crudui-widget--condition. It holds the prepend affix, the textarea
of a condition field, which keeps the value and is what a form submits, the editor of the tree and
the append affix:
- CUMK-067 A value that the editor shows (CUSC-086) writes the textarea with the attribute
hiddenand then<div class="crudui-condition" data-condition-for="{id}">, where{id}is the id of the textarea, holding the root condition. Each condition carriesdata-condition-path, the JSON Pointer (RFC 6901) of the condition in the tree: the root is the empty pointer and the condition at position{n}of a group is the pointer of the group,/,allorany,/and{n}. A group is<fieldset class="crudui-condition__group">with a<select class="crudui-condition__mode" data-condition-part="mode">of the optionsallandany(the mode of the group selected), an<ol class="crudui-condition__list">whose<li class="crudui-condition__item">hold its conditions in list order, and the buttonsadd-comparison,add-group,add-judge(only underconditionTree: judge) and, below the root,remove. A comparison is<div class="crudui-condition__comparison">with the field control, a<select class="crudui-condition__op" data-condition-part="op">of the eightopvalues (CUSC-085, the op of the comparison selected), an<input class="crudui-condition__value" data-condition-part="value">whose value is the condition text (CUMK-066) of the value, and the buttonremove. The field control is<input class="crudui-condition__field" data-condition-part="field">with the field as its value, or, withoptions.fields,<select class="crudui-condition__field" data-condition-part="field">of those names in their order, the field of the comparison selected and, when it is not among them, written first as its own option. A judged text is<div class="crudui-condition__judge">with<input class="crudui-condition__text" data-condition-part="judge">holding the text and the buttonremove. Each button is<button type="button" class="crudui-action crudui-action--text" data-condition-action="{action}">with the interface message of its action (conditionAddComparison,conditionAddGroup,conditionAddJudge,conditionRemove), and the mode select has thearia-labelofconditionMode. No editor control has aname, so a form submits the textarea alone. - CUMK-068 A value that the editor does not show (CUSC-086) writes, before the textarea, which keeps
no
hidden,<p class="crudui-condition__error" role="alert">with the interface messageconditionInvalid, whose{path}is the data path of the field, and writes no editor.
The widget model of a structured field has the layout condition and adds condition:
{ for, judge, fields, labels, root }, where for is the textarea id, judge whether judged text is
offered, fields the list of options.fields or null, labels the interface messages
{ mode, addComparison, addGroup, addJudge, remove } of the controls, and root the root node, or
{ for, error } with the error text. A node is
{ kind: "group", path, mode, items }, { kind: "comparison", path, field, op, value } with the
value text, or { kind: "judge", path, text }. A field opens_when with options.fields: [clues.size] and the data { "all": [{ "field": "clues.size", "op": "ge", "value": 2 }] } writes, in
English:
<div class="crudui-widget crudui-widget--condition"><textarea name="opens_when" class="valid-target
crudui-input crudui-input--condition" rows="5" data-crudui-value="json" data-name="opens_when"
data-rule-name="opens_when" data-default="" id="crudui:opens_when" hidden="">{"all":[…]}</textarea><div
class="crudui-condition" data-condition-for="crudui:opens_when"><fieldset class="crudui-condition__group"
data-condition-path=""><select class="crudui-condition__mode" data-condition-part="mode"
aria-label="Condition mode"><option value="all" selected="">all</option><option value="any">any</option></select><ol
class="crudui-condition__list"><li class="crudui-condition__item"><div class="crudui-condition__comparison"
data-condition-path="/all/0"><select class="crudui-condition__field" data-condition-part="field"><option
value="clues.size" selected="">clues.size</option></select><select class="crudui-condition__op"
data-condition-part="op"><option value="eq">eq</option>…<option value="ge" selected="">ge</option>…</select><input
class="crudui-condition__value" data-condition-part="value" value="2"><button type="button"
class="crudui-action crudui-action--text" data-condition-action="remove">Remove</button></div></li></ol><button
type="button" class="crudui-action crudui-action--text" data-condition-action="add-comparison">Add condition</button><button
type="button" class="crudui-action crudui-action--text" data-condition-action="add-group">Add group</button></fieldset></div></div>
Switches
A switcher field renders a switch. Its node model has the checkbox model of a checkbox field
with the member role: "switch" after checked, and the control class
valid-target crudui-input crudui-input--switch followed by the class of design.class. A
checkbox field has no role member and the control class valid-target followed by the class of
design.class. The input of a switch keeps type="checkbox", so the form submits and binds it as
a checkbox, and has role="switch", so assistive technology announces it as on or off. Every
renderer writes role after type and the declared attributes after role. The caption is the
input's own label, as for a checkbox, except in an inline layout. A field sync with the label Sync and the data
sync: true writes:
<div class="crudui-node__body"><input class="valid-target crudui-input crudui-input--switch"
id="crudui:sync" type="checkbox" role="switch" name="sync" checked="" value="1"/><label
for="crudui:sync">Sync</label></div>
The stylesheet draws the input as a track with a round thumb at its start: the track has the
--crudui-control-border border and the --crudui-subtle background, and the thumb has the
--crudui-surface color. When the switch is on, the track has the --crudui-accent border and
background and the thumb is at its end. A focused switch has a 2px --crudui-accent outline.
Button fields
A button field, or its alias action, renders one button element in the node body. The button
has type="button", the class crudui-action crudui-action--text followed by the class of
design.class, the style of design.style, the id of the control, the event attributes of
behavior and the declared attributes, in that order, and the content text of the field as its
escaped content. A behavior script, such as onclick, is an event attribute of the button as it
is of every other control, so a click runs it; CRUDUI writes no script, no hidden input and no
name for the field, and the field submits no value. A button without event attributes writes its
style last, and a button with event attributes is written with its attributes as declared, as
every control is (declared attributes).
The widget model has the layout button, the tag button, the button attributes as attrs and
the content text as text. A field with a label has a header whose label targets the button. A
field without a label has no header, and in an inline layout its button starts the
control column as every other control does. The stylesheet makes the button as wide as its content
and as high as the other controls (--crudui-control-height). A field run with the label
Sync, the content Run now and behavior: { onclick: "sync()" } writes:
<div class="crudui-node crudui-node--field" data-field-path="run"><div class="crudui-node__header"><label
class="crudui-node__label" for="crudui:run">Sync</label></div><div class="crudui-node__body"><button
type="button" class="crudui-action crudui-action--text" id="crudui:run"
onclick="sync()">Run now</button></div></div>
Rows
-
CUMK-031 The number joins the one-based positions of the enclosing rows with
.. -
CUMK-032
multiple.titlenames a direct child whose value becomes the row title. An empty value shows the untitled message. -
CUMK-033 Group rows are collapsible. The summary counts the rows of the collections directly in the row body, or shows the collapsed message when there are none. Scalar rows have no toggle, title or summary.
-
CUMK-034 Rows of a
multiple: onlycollection have no row controls, and the structure map shows none for them. -
CUMK-035 Controls follow this order:
move-upandmove-down(multiple.sortable),add-row,copy-row(multiple.copy) andremove-row.move-upis unavailable on the first row andmove-downon the last.add-rowandcopy-roware unavailable when the row count reachesmultiple.max;remove-rowwhen it is at or belowmultiple.min. -
CUMK-036 Every action button (
data-crudui-action), in the form and in the structure map, marks an unavailable action witharia-disabled="true", neverdisabled. It stays focusable and a click on it does nothing. A focused control that becomes disabled keeps focus in Chromium and loses it in WebKit, sodisabledwould make focus depend on the browser and on rendering timing. Field controls keepdisabled, which is declared data state and excludes the field from submission. -
CUMK-037
multiple.controlsplaces row controls in the rowheader(default) orfooter. Withoutlinethe row controls move to the structure map lines; an empty collection's Add control is not a row control and stays in the collection footer. -
CUMK-038
multiple.header: stickyaddscrudui-node--sticky, and the row root style sets--crudui-sticky-depthto the number of enclosing sticky rows. The row's sticky line is that depth times--crudui-node-header-heightless one row border (--crudui-row-border). Sticky rows are CSS only, so they behave the same wherever the form scrolls: in a page, a frame or a scrolling box. The header container pins on the line and has exactly the header height, border included, without wrapping (a long title is truncated). The line under a header is the header's own bottom border, inside the container, so a pinned container lands on the line of the container above it and pinned levels share that one border. A sticky row has no top border of its own: its card top edge is drawn inside the header container, the box that pins, and goes while the container is stuck, when the line of the container above is the seam. A row arriving on the line puts its own edge on that same line, so a seam is one border wide wherever a row rests and nothing is covered. The level label shows, and the card top edge goes, only while the container is stuck, by ascroll-state(stuck: top)container query. In a browser without scroll-state queries (Firefox, Safari),connectFormsetsdata-crudui-stuckon a header container while sticky positioning moves it from the top of its row, and the stylesheet shows the label and hides the card top edge from that attribute. That attribute is the only thing a script changes; it never measures rows for layout or moves a scroll position. A control inside a sticky row has a top scroll margin of the headers pinned above it (--crudui-sticky-cover, the line plus one header height), and every form control has a bottom scroll margin of the footer height. When a binding moves focus it focuses without scrolling and then scrolls the control into view only as far as needed (scrollIntoViewwithblock: 'nearest'), which keeps to those margins in every engine, so the control lands clear of them.A sticky node wraps its header in
crudui-node__header-container. The wrapper is the sticky scroll-state container and holds the card top edge;crudui-node__headerremains the header content and draws the line under it. The wrapper supplies the sticky background and top radius while the header supplies its content layout. This wrapper is required so state-dependent styles target header descendants without changing the header's semantic slot.
Form buttons
A spec declares form buttons at its root with buttons, a list of { type, text, name, value, href, design, behavior } where type is submit, reset, button or
link. A spec without buttons has one submit button. Submit and reset buttons
without text show the interface text for their type; a button or link needs text,
and a link needs href. action (method, url, enctype) is the submission
target, kept in the template; renderForm writes it on the form element of the
complete form. Both belong to the form root.
Every form ends with crudui-form__footer, one crudui-controls group whose
accessible name is the form actions text. bindButtons(template, data, options)
evaluates the buttons (design classes and styles follow the field design rules), and
formButtonsHtml(buttons) is the only markup builder: a link is an a, the other
types are button elements, with attributes in the order type, class, style,
name, value, href, onclick. The footer pins to the bottom of its scroll
container at --crudui-form-footer-height, as sticky row headers pin to the top.
Structure map and data view
The structure map has one rule: one line per form row. buildOutline(nodes)
returns the form's rows, each with the rows nested in it, so the map nests exactly
as the form does; collections, counts and empty collections are not rows and stay in
the form. Each row has a select-row button with its number and title. A nested row body indents
one step. Its header holds expand-all, collapse-all, undo and redo
(each unavailable when its direction has no history). React, Vue and Svelte provide Outline
and DataView, and the stateless OutlineView and DataPanel (Vue: outlineVNode
and dataVNode) for data owned through bindForm; the HTML renderer provides renderOutline(form) and
renderData(form), and renderOutlineView(state, messages) and
renderDataPanel(data, messages) for the same data. All four renderers
reproduce the shared structure map fixture. connectForm runs actions in the form.
connectOutline(element, form, formElement) runs structure-map actions and focuses
the first control of the form row a select-row button names and scrolls it into
view clear of the sticky headers and the footer.
Interface messages
- CUMK-065 Every implementation embeds a source generated from
contracts/interface-messages.jsonand a test fails when the embedded text differs from it, so every implementation uses the same text.
Control labels, counts and summaries come from one message table for ko, en,
ja and zh, contracts/interface-messages.json.
Every implementation embeds a source generated from that file and a test fails when
the embedded text differs from it, so every implementation uses the same text.
Authors do not override this text; text that belongs to the data, as a row title
that is empty, is declared by the author (see schema). Binding rejects a language that is not a
string with Language must be a string, then a present keyPrefix or idPrefix
that is not a string with {name} must be a string, then an unsupported other
than throw or marker with unsupported must be throw or marker, then any other
language with Unsupported language: {language}. An absent or null option uses
its default.
Styles
@polyspec/crudui-generator-core/crudui.css is the only stylesheet a form needs: slot
layout, row cards, widgets, control icons, sticky headers, the structure map and the
data view. Every rule is scoped to a crudui block, including box sizing and hiding
[hidden] elements, and none depends on page styles or a CSS framework. The page
styles its own layout and nothing inside the crudui blocks.
The form, structure map, data view, list and detail share one set of --crudui-*
custom properties. One rule, :where(.crudui-form, .crudui-outline, .crudui-data, .crudui-list, .crudui-detail), declares their defaults with zero specificity, and no
other rule writes a color. A page themes the blocks with a rule that selects them and
sets the properties, for example .crudui-form, .crudui-list { --crudui-accent: … };
the rule overrides the defaults in any stylesheet order.
- The
--crudui-*custom properties with their defaults and uses are documented, with the stylesheetcrudui.css, inpackages/generator/ts/README.md.
--crudui-node-header-height, --crudui-row-padding, --crudui-row-border and
--crudui-form-footer-height size the sticky rows and the form footer (see above).
tests/style-properties.test.mts checks the property rule, that every property a rule
reads is declared and that no other rule writes a color.
Viewport widths
crudui.css lays out every block at any viewport width from 360 CSS pixels. At the viewport
widths 360 and 1280, the expected HTML of every case of the shared render fixtures
(tests/fixtures/form-render, form-complete, list-render and detail-render), placed in a
page with crudui.css and no other stylesheet, causes no horizontal overflow of the document:
the scroll width of the document element is not greater than the viewport width. Every control
(input, select, textarea, button) and every action lies within the viewport, except
inside an element that scrolls horizontally on its own (overflow-x auto or scroll), such
as the table of a list, whose content may be wider than the viewport. A list scrolls
horizontally on its own, so a wide table does not widen the page. tests/viewport.test.mts
checks every case at both widths in Chromium, Firefox and WebKit.
Tailwind CSS
A page styled with Tailwind CSS 4 imports @polyspec/crudui-generator-core/crudui.css into the
cascade layer components, after tailwindcss: @import "@polyspec/crudui-generator-core/crudui.css" layer(components);.
The rules of crudui.css lie unchanged in that layer, so a utility class of the page, which lies in
the later layer utilities, overrides a CRUDUI rule. The markup is the same. No copy of the
stylesheet is generated. A page themes the blocks with the
--crudui-* properties, as with crudui.css, for example with the colors of its Tailwind theme:
.crudui-form, .crudui-list { --crudui-accent: var(--color-indigo-600); }.
Compiled with the theme and the utilities of the latest stable Tailwind CSS and no other
stylesheet, that import gives every element of the expected HTML of every shared render
fixture the same computed style, without custom properties, as crudui.css at the viewport widths
360 and 1280. tests/tailwind-styles.test.mts compares them in Chromium, Firefox and WebKit.
List and detail markup
Display output uses the same block/element/modifier grammar as form output. A list
root is crudui-list; its description, table, headings, cells, cards, empty state, actions
and pagination use crudui-list__description, crudui-list__table, crudui-list__heading,
crudui-list__cell, crudui-list__cards, crudui-list__card, crudui-list__empty,
crudui-list__actions, crudui-list__action and crudui-list__pagination.
A pagination navigation contains buttons with crudui-list__pagination-prev,
crudui-list__pagination-page and crudui-list__pagination-next. Each button has
data-page; the current page has aria-current="page" and is disabled. The first
and last boundary controls are disabled. The renderer emits a bounded page-number
window for very large totals.
A detail root is crudui-detail; each field uses crudui-detail__field,
crudui-detail__label and crudui-detail__value. The actions of a detail are written before
the crudui-detail element in crudui-detail__actions, with one crudui-detail__action per
action (display formats).
Every displayed value also carries crudui-value and the closed format modifier
crudui-value--text, --date, --number, --choice-label, --badge, --link,
--bool, --image or --html. Badge variants are data, not class names:
crudui-badge carries data-crudui-variant when a variant is present. Boolean
presentation uses crudui-bool with the closed --text, --check or --icon
modifier and data-crudui-state="true|false". CSS may style these attributes;
scripts must read the declared data and ARIA attributes, never infer behavior from
presentation classes.
Behavior not adopted from the reference form
The reference form that motivated this grammar differs in these deliberate ways:
-
The readiness and scroll behaviour of the TypeScript renderer (synchronization after rendering; sticky headers marked on scroll and resize at most once per animation frame) is documented in
packages/generator/ts/README.md. -
CUMK-057 Expansion is view state, not record data.
-
CUMK-058 Rows are identified by keys, not array positions.
-
CUMK-059 Depth is unlimited; one recursive node replaces per-depth components.
-
CUMK-060
multiple.maxis enforced. -
CUMK-061 Undo records every data change and merges consecutive edits of one path.
-
CUMK-062 Sticky headers and their labels are CSS only; no script follows the scroll position, so nothing runs or renders while scrolling.
-
CUMK-063 A row shows one number; there is no separate position counter.
-
CUMK-064 Layout options are declared in the specification, not chosen in a panel.