Built-in BOX Templates
The prototype framework ships four general-purpose BOX templates — TABLE,
FORM, TABS and RAW — plus special-purpose ones: PROPBUTTON,
FILTEREDDROPDOWN, SELECT, FACETS, MARKUP and NOVIEW. Each general-purpose template
accepts a set of annotations in its BOX header that tune how the box is rendered.
This page documents those annotations first, then the special-purpose templates.
BOX-template annotations
Annotations are written as keywords (and occasionally key="value" pairs) inside
the angle brackets of a BOX header:
"Categories" : productCategory cRud BOX<TABLE title="Catalog" noHeader>
[ ... ]
The Ampersand compiler passes every BOX-header annotation to the matching template generically (see box-template-architecture.md), so no annotation needs special compiler support — a template simply reads the ones it understands and ignores the rest.
Annotation matrix
| Annotation | TABLE | FORM | TABS | RAW | Value | Effect |
|---|---|---|---|---|---|---|
title | ✅ | ✅ | ✅ | — | string | Renders a title/description line above the box content. |
hideOnNoRecords | ✅ | ✅ | ✅ | — | flag | Hides the whole box (including add-controls) when it has no records. |
hideSubOnNoRecords | — | ✅ | ✅ | — | flag | Hides an individual field row (FORM) or tab panel (TABS) when that sub-field has no records, also a field the user may edit. |
showSubOnNoRecords | — | ✅ | — | — | flag | Shows every field row of a FORM, also an empty one the user cannot edit. |
showOnNoRecords | — | ✅ | — | — | flag | On a group (a FORM box on I): shows the group while all of its fields are empty. |
noHeader | ✅ | — | — | — | flag | Suppresses the column-header row. |
compact | ✅ | — | — | — | flag | Starts the table in the dense stand: one line per row, under a header that stays. The user can switch. See compact. |
hideLabels | — | ✅ | — | — | flag | Renders fields full-width without their labels. |
showNavMenu | ✅ | ✅ | — | — | flag | Adds a navigation menu (links to other interfaces) per record. |
sortable | ✅ | — | — | — | flag | Makes column headers clickable to sort. Combine with sortBy / order. |
sortBy | ✅ | — | — | — | string | Default sort column (a sub-interface label). Use with sortable. |
sortByAndHide | ✅ | — | — | — | string | Sorts the rows on the named column and hides that column. Works without sortable; overrides sortBy. |
order | ✅ | — | — | — | asc/desc | Default sort direction. Use with sortBy or sortByAndHide. |
table | — | — | — | ✅ | flag | Lays each record's fields out as an HTML table row. |
form | — | — | — | ✅ | flag | Wraps each record in a non-submitting <form> element. |
FACETS accepts every TABLE annotation in this matrix, plus its own facets,
facetOnly and facetKind; see BOX \<FACETS>.
Not yet available —
noRootTitle. Historically a root interface box could suppress its automatic interface heading withnoRootTitle. That heading is rendered by the per-interfacecomponent.htmltemplate, which the compiler renders without the box header's key/values, so the annotation cannot reach it from the framework alone. RestoringnoRootTitlerequires a small compiler change (pass the root box'snoRootTitleinto thecomponent.htmlrender context ingenComponentFileFromTemplate). Until then, usetitleto add an explicit box title.
title
title="..." renders a free-text line above the box content. It works on
TABLE, FORM and TABS. The value is a string, so it must be quoted.
"Categories" : productCategory cRud BOX<TABLE title="All categories">
[ "Name" : catName cRud ]
hideOnNoRecords
When set, the entire box is removed from the page while it has no records, including its title and add-controls.
"Notes" : itemNote cRud BOX<TABLE hideOnNoRecords>
[ "Note" : noteText cRud ]
Warning. Because the add-controls disappear together with the box, the user cannot create the first record through this box. Only use
hideOnNoRecordswhere records are created elsewhere, or where an empty box is genuinely irrelevant to the user.
Empty fields in a FORM
A FORM shows what a record has. Without annotation it leaves out a field that is
empty and that the user cannot fill (no C or U in its CRUD), so a detail screen
does not list labels without values. A field the user may fill stays visible while
empty, so the value can be entered.
A group is a FORM box on I inside a FORM: the modeler's way to put fields that
belong together in one block. A group whose fields are all empty is left out as well.
INTERFACE Requirement : I[Requirement] cRud BOX<FORM>
[ "Properties" : I cRud BOX<FORM>
[ "Status" : status cRud
, "Priority" : priority cRud
]
, "Trace in the design" : I cRud BOX<FORM showOnNoRecords>
[ "Rules" : mentions~;I[Rule] cRud
, "Interfaces" : mentions~;I[Interface] cRud
]
]
Groups sit side by side as long as the width allows. Three annotations change the default:
showOnNoRecordson a group shows it while all of its fields are empty, with a dash; use it where the emptiness itself tells the user something, such as a requirement that no rule or interface mentions yet.showSubOnNoRecordson a FORM shows every field row, as FORMs did before.hideSubOnNoRecordshides every empty field row, also one the user may edit.
The FORM reads the CRUD and multiplicity of its fields from interfaces.json,
because the compiler does not pass them to the box template. This default belongs to
FORM only: TABS still shows every tab panel, and hides an empty one only under
hideSubOnNoRecords.
hideSubOnNoRecords
Hides the parts inside a box that have no data, keeping the box itself visible:
- In a FORM, an empty field row is hidden, also one the user may edit.
- In TABS, an empty tab panel is hidden (so the tab disappears from the bar).
"Category" : I[Category] cRud BOX<TABS hideSubOnNoRecords>
[ "Name" : catName cRud
, "Products" : catProducts cRud BOX<TABLE>[ "Code" : code cRud ]
]
A category with no products shows only the Name tab. In TABS this is
implemented by filtering the list of tab panels per record, which avoids the
PrimeNG tab-index problems that a structural *ngIf on a tab panel would cause.
Warning. A hidden field/tab cannot be edited through this box. Make sure the underlying data is reachable through another interface when it matters.
compact (TABLE)
A table has two stands. In the roomy stand a row is as high as its longest text. In the dense stand every row is one line, a long text ends in an ellipsis, and the table scrolls under its own header, so many rows fit on a screen. A click on a dense row shows its full text; a second click folds it again.
INTERFACE Requirements : "_SESSION";V[SESSION*Requirement] cRud BOX<FACETS compact sortable>
[ "Requirement" : I cRud LINKTO INTERFACE Requirement
, "Status" : status cRud
, "Text" : text cRud
]
The modeler chooses the stand a table starts in: compact for a table of data, nothing for a
table that is read. The user switches with the button at the right of the header row, and the
browser remembers that choice per table until the tab closes (in its session storage, see
cookies and browser storage), so it survives a reload and does
not carry over to another table. A table with noHeader has no button and keeps the
stand of the modeler.
noHeader (TABLE)
Suppresses the column-header row of a table. Because the in-header "add" control is suppressed too, the table offers a separate "New …" button instead, which also works while the table is still empty.
"Rows" : someRows cRud BOX<TABLE noHeader>
[ "Col 1" : c1 cRud, "Col 2" : c2 cRud ]
hideLabels (FORM)
Renders each field full-width without its label. Useful for compact, data-dense forms where the values are self-explanatory.
"Address" : I cRud BOX<FORM hideLabels>
[ "Street" : street cRud, "City" : city cRud ]
Accessibility. Hiding labels removes textual context for screen readers. Prefer it only where the meaning of each value is obvious from its content.
showNavMenu (TABLE, FORM)
Adds a per-record navigation menu that links to the other interfaces able to
display that record. It reuses the framework's _ifcs_ navigation data, which is
included in interface reads by default, so no extra configuration is needed.
"Categories" : V[SESSION*Category] cRud BOX<TABLE showNavMenu>
[ "Name" : catName cRud ]
sortable, sortBy, sortByAndHide and order (TABLE)
sortable makes the column headers clickable to sort. sortBy="<column>" picks
the column the table is sorted on when it opens, and order="asc"/order="desc"
picks the direction. The column name is the sub-interface label, written as it
appears in the interface definition.
sortByAndHide="<column>" sorts the rows on the named column and hides that
column from the table. Use it when the sort key is bookkeeping the user should
not see — a rank, a sequence number, a normalized date:
"Steps" : procedureStep cRud BOX<TABLE sortByAndHide="Seq">
[ "Step" : stepText cRud
, "Seq" : stepSeq cRud
]
- It works on its own:
sortableis not required. Combined withsortable, the user can still re-sort on the visible columns;sortByAndHidethen overridessortByas the initial sort. ordersets the direction, as withsortBy.- The named column must be a univalent (
UNI) sub-interface; sorting needs one value per row. - The column is hidden from view only. Its values still travel to the browser in
the interface data, so
sortByAndHideis presentation, not access control — restrict the CRUD rights where the user may not read the values at all (but note that without read rights there is nothing to sort on either).
table and form (RAW)
RAW is an unstyled escape hatch. Two annotations change how each record's fields
are wrapped:
tablelays the fields out as a single HTML table row (<tr><td>…</td></tr>). The sub-expressions must produce content valid inside a<td>.formwraps the fields in a<form onsubmit="return false">element. The form never submits, so it adds no behaviour of its own — it is only a semantic container for styling or grouping.
"Plain" : I cRud BOX<RAW table>[ "A" : a cRud, "B" : b cRud ]
These are expert features; prefer TABLE/FORM for normal use.
BOX \<PROPBUTTON>
The BOX <PROPBUTTON> template creates interactive buttons for toggling boolean properties on atoms. Users click these buttons to set, clear, or toggle property values.
When to use PROPBUTTON
Use PROPBUTTON for boolean properties like on/off, yes/no, or active/inactive states. The template works well when you want users to change a property with one click rather than typing or selecting from dropdowns. Consider PROPBUTTON for status toggles, approval workflows, or feature flags.
Do not use PROPBUTTON for properties with more than two states. Complex validation logic before state changes makes regular fields more suitable. Relations that are not univalent will cause compilation errors.
Alternatives include regular fields with checkboxes (property : relation cRUD), dropdown selection for limited target sets, radio buttons for exclusive boolean relations, or text fields for non-boolean properties.
Simple Example
This example creates a toggle for marking tasks as completed:
CONTEXT TaskManagement
RELATION taskName [Task*TaskName] [UNI,TOT]
RELATION isCompleted [Task] [PROP,UNI]
INTERFACE TaskList : "_SESSION" ; V[SESSION*Task] cRud
BOX <TABLE>
[ "Task Name" : taskName cRud
, "Status" : I cRud BOX <PROPBUTTON>
[ "label" : TXT "Mark Complete"
, "property" : isCompleted cRUd
]
]
The property relation must have both [PROP,UNI] constraints for PROPBUTTON to work. Use the prescribed field names "label" and "property" exactly as shown, including the quotes. The property expression should reference the relation itself, not an I-expression. The CRUD annotation on the property must allow Update (capital U) for toggling to function.
Complete Reference
The syntax structure follows this pattern:
<fieldname> : <term> <crud>? BOX <PROPBUTTON>
[ "label" : <labelExpression>
, "property" : <propertyRelation> <crud>
, "popovertext" : <tooltipExpression>
, "color" : <colorExpression>
, "action" : <actionExpression>
]
PROPBUTTON requires specific field names. The "label" field contains the text displayed on the button. The "property" field specifies the boolean property to toggle and must reference a [PROP,UNI] relation. Optional fields include "popovertext" for tooltip text, "color" for button color using CSS color values, and "action" for button behavior.
Three action types control button behavior. Toggle mode (default) switches between true and false on each click. Set mode always changes the property to true. Clear mode always changes the property to false.
"action" : TXT "toggle" -- Switches between true/false
"action" : TXT "set" -- Always sets to true
"action" : TXT "clear" -- Always sets to false
Advanced Example
This project management interface demonstrates multiple PROPBUTTON instances:
CONTEXT ProjectManagement
RELATION projectName [Project*ProjectName] [UNI,TOT]
RELATION isActive [Project] [PROP,UNI]
RELATION isArchived [Project] [PROP,UNI]
RELATION priority [Project*Priority] [UNI]
INTERFACE ProjectDashboard : "_SESSION" ; V[SESSION*Project] cRud
BOX <TABLE>
[ "Project" : projectName cRud
, "Priority" : priority cRud
, "Active" : I cRud BOX <PROPBUTTON>
[ "label" : TXT "Toggle Active"
, "property" : isActive cRUd
, "popovertext" : TXT "Click to activate/deactivate this project"
, "color" : TXT "#28a745"
, "action" : TXT "toggle"
]
, "Archive" : I cRud BOX <PROPBUTTON>
[ "label" : TXT "Archive"
, "property" : isArchived cRUd
, "popovertext" : TXT "Archive this project"
, "color" : TXT "#dc3545"
, "action" : TXT "set"
]
]
Common Issues
TypeScript compilation errors about property type mismatch occur when the property relation lacks the [UNI] constraint. The error message Type 'Object & { _view_: ...; }[]' is not assignable to type 'PropButtonItem' indicates this problem. Add both [PROP,UNI] constraints to fix it.
Buttons that do not respond to clicks usually have CRUD annotations that prevent updates. Check that the property's CRUD annotation includes a capital U for Update permission.
Interface generation errors often stem from incorrect field names. Use "label" and "property" exactly as written, including the quotes. Misspellings or variations will break the interface.
Multiple buttons interfering with each other suggests they share the same property relation. Create separate relations for different buttons to avoid conflicts.
Integration
PROPBUTTON combines with other interface elements in the same interface:
INTERFACE ItemManager : "_SESSION" ; V[SESSION*Item] cRud
BOX <TABLE>
[ "Name" : itemName cRud
, "Description" : description cRud
, "Actions" : I cRud BOX <FORM>
[ "Active" : I cRud BOX <PROPBUTTON>
[ "label" : TXT "Active"
, "property" : isActive cRUd
]
, "Priority" : priority cRUD
, "Tags" : tags cRUD
]
]
This creates an interface mixing PROPBUTTON with regular fields for different types of user interactions.
BOX \<FILTEREDDROPDOWN>
BOX <FILTEREDDROPDOWN> populates a relation from a server-filtered list of
selectable atoms. The set of options is computed per record by an Ampersand
expression, so each row can offer a different, context-dependent choice list.
This template supersedes the older
OBJECTDROPDOWNandVALUEDROPDOWNtemplates. A singleFILTEREDDROPDOWNworks for both object concepts and value concepts (those with aREPRESENT).
Usage
expr cRud BOX <FILTEREDDROPDOWN>
[ "selectFrom" : selectableAtoms -- options to choose from (per record)
, "setRelation" : theRelation <crud> -- the relation that is filled/changed
]
Both field names are prescribed and must be written exactly, including the quotes:
| Field | Meaning |
|---|---|
selectFrom | Expression yielding the atoms the user may pick. It is filtered on the server for the current record, so it can depend on the record (e.g. only eligible employees for this project). |
setRelation | The relation whose population is read and modified when the user picks (or clears) a value. Its CRUD annotation governs what the user may do. |
selectFrom and setRelation must have the same target concept; a mismatch
raises an error at runtime.
CRUD on setRelation
The CRUD letters on setRelation decide which actions the widget offers (see the
CRUD reference for the general meaning):
- R (read) is required for the box to display the current value.
- U (update) lets the user replace/extend the value by selecting an option.
- D (delete) shows a remove/clear control.
- C (create) lets the user create a new atom by typing a value that is not yet in the list (only meaningful where a new atom makes sense).
For a [UNI] setRelation a selection replaces the current value; for a
non-univalent relation the user builds a list. A [TOT] relation hides the
remove control when removing the last value would violate totality.
Example
"Assign an employee" : I[Project] cRud BOX<FILTEREDDROPDOWN>
[ "selectFrom" : eligible -- eligible employees for this project
, "setRelation" : projectMember cRUd
]
(See test/projects/box-filtered-dropdown for worked examples covering UNI, TOT
and full-CRUD variants.)
BOX \<SELECT>
BOX <SELECT> is a simpler dropdown: it selects from a static option list
that is already present on the record, without per-record server filtering and
without creating new atoms from typed text.
Usage
expr cRud BOX <SELECT>
[ "selectFrom" : selectableAtoms -- options, embedded on the record
]
Use SELECT when the option list is the same for every record and is part of the
interface data. Use FILTEREDDROPDOWN when the options must be filtered per
record or when the user should be able to create new atoms.
BOX \<FACETS>
BOX <FACETS> is a BOX <TABLE> with a facet panel next to it: a faceted search.
Every facet belongs to one box item and lists the values the rows carry for it, each
with the number of rows that have it. Choosing values shrinks the table, and the counts
of the other facets follow. A search field above the table searches every value of a
row. Use it for an overview that is too long to read whole, where the user wants to
narrow it down by status, owner, date and the like.
Usage
INTERFACE Issues : "_SESSION";V[SESSION*Issue] cRud
BOX<FACETS facets="Project, Status, Date, Labels, Author" facetOnly="Author" sortable sortBy="Date" order="desc">
[ "Issue" : I cRud LINKTO INTERFACE Issue
, "Project" : project cRud
, "Status" : status cRud
, "Date" : date cRud
, "Labels" : label cRud
, "Author" : author cRud
]
The box items are the table columns, exactly as in TABLE. Three annotations shape the
facets:
| Annotation | Value | Effect |
|---|---|---|
facets | comma-separated item labels | The items that are facets, in this order. A.B names item B inside the box of item A. Without facets, every item is a facet. |
facetOnly | comma-separated item labels | Items that are facets but no table column. They need not be repeated in facets. |
facetKind | comma-separated label=kind | The kind of facet for an item, instead of the one its technical type gives: list, text, range or date. See below. |
A name in any of these lists that matches no box item, or that names an item that can
never be a facet (see below), leaves a warning in the browser console. An item whose label
contains a comma or a period cannot be named in these lists; it is still a facet when
facets is absent.
Every TABLE annotation (sortable, sortBy, sortByAndHide, order, noHeader,
showNavMenu, title, hideOnNoRecords) works as it does on TABLE, over all rows of
the box: hideOnNoRecords hides the box when it has no rows, not when the chosen facets
leave none.
The kind of facet follows the technical type
The technical type of a concept is the TYPE in its REPRESENT statement, and
OBJECT for a concept without one. The compiler writes a description of every
interface (interfaces.json) and of every concept (concepts.json) next to the
generated backend, and the build serves both to the browser. From them the facet panel
reads the target concept of every box item and that concept's technical type, which
decides how the facet filters:
| Technical type of the item | Facet |
|---|---|
ALPHANUMERIC, OBJECT | Values with counts, most rows first. With more than 40 values that are nearly all different (at least 80% of the rows), a text field instead (on a child item only; see below). |
BIGALPHANUMERIC, HUGEALPHANUMERIC | Text field: rows whose value contains the text (on a child item only; see below). |
BOOLEAN, and a property relation [PROP] | yes / no with counts. |
INTEGER, FLOAT | Up to 12 distinct values: a value list, sorted by value. More: a range with a from and a to field. |
DATE, DATETIME | Years with counts; choosing a year shows its months, a month its days. A DATETIME counts on the calendar day in the time zone of the server. |
PASSWORD, BINARY, BIGBINARY, HUGEBINARY, and the concept ONE | Never a facet. |
The identity item ("Issue" : I) is a text field whatever its type: its value is the
row itself. A text field on an item of the box itself does not appear, because the search
field above the table covers it; see "One search field" below. A value list ends with (empty) when some rows have no value.
facetKind overrides this choice per item. facetKind="Date=date" gives a date tree to
an ALPHANUMERIC item that holds dates as text; a text that starts with a date, such as
2026-08-10, revised 2026-09-17, counts under that date, and a text without one under
(empty). facetKind="Size=range" makes a range of a number with few values, and
list or text choose a value list or a text field; text on an item of the box itself
leaves that item to the search field. Prefer a REPRESENT … TYPE DATE
in the model where the data allows it; facetKind is for data that does not.
One search field, and no column the panel already tells
The search field above the table searches every value of a row, the identity item included. A text facet on an item of the box itself would repeat it, so the panel does not show one; a text facet on a child item (see below) stays.
A column leaves the table while the panel already tells its value: at least two rows
pass, the item has a value-list or date facet, every passing row has the same value (or
none), and the user cannot edit the item (no C or U). The facet then shows that
value checked, also when the user did not choose it. Choosing Project = Patents thus
removes the column Project, and a column in which every row is empty disappears as well.
An editable column stays, so an empty value can still be filled in.
Recursive facets for an OBJECT item
An OBJECT item with a box of its own offers the items of that box as child facets, and
so on, four levels deep. With "Project" : project BOX<FORM> [ "Owner" : owner ], the
Project facet has a fold-out Filter on Project by … with an Owner facet: choose an
owner, and the table keeps the rows whose project has that owner. facets="Project.Owner"
shows only that child facet, titled Project › Owner; facets="Project, Project.Owner"
shows it once, inside Project. A LINKTO item has no box of its own and therefore no
child facets.
Rules for combining
Values chosen within one facet combine with or; facets combine with and. A multi-valued item (a label list) matches when any of its values is chosen. The counts of a facet cover the rows that pass the search and every other facet, so a facet never hides its own alternatives.
The selection is in the URL
Every choice is written to the query string, so a filtered view can be bookmarked or
sent: /issues?f.Status=open&f.Project.Owner=Ann&f.Date=2026-09&q=login. Each facet has
one parameter f.<label path>, with one value per chosen value: the atom identifier for
an OBJECT, true or false for a yes/no facet, min..max for a range (either end may
be empty), ~text for a text field, and a year, month or day for a date. q holds the
search text. The form of a value decides how it filters, so a bookmark keeps its meaning
when the data changes and a facet turns from a value list into a text field or a range. A label that
contains a period appears in the parameter by its item name instead. A FACETS box nested
in another box prefixes its parameters with its item name and the atom of the enclosing
row, such as Tickets.red.f.Status.
Create, delete and edit
With C or D rights the table offers them as TABLE does. A row created through the
table stays visible until the selection changes, even if it does not pass the facets;
after a reload it follows the facets like every other row. A deleted row leaves the
table and the counts. After every edit the panel recounts, so a row whose value changed
moves in or out of the table.
Limits
The facets filter in the browser, over the rows the interface delivers: the interface
still loads every row. One filter step over 5 000 rows and 10 facets takes a few
milliseconds; the test "evaluates 5 000 rows and 10 facets" in facet-engine.spec.ts
measures and logs it (7 ms on 2026-09-28). For far more rows, a server-side filter would
be needed.
A build of this framework version serves concepts.json itself. Should it still be
missing, the panel infers the kind of facet from the values: dates and numbers are
recognised, but a PASSWORD or binary item is not, and becomes a facet unless
facets leaves it out.
BOX \<MARKUP>
BOX <MARKUP> shows text formatted in its markup language: Markdown, GitHub-flavoured
Markdown, HTML, LaTeX or plain text. Use it for a text that its author wrote with markup, such as
a design choice or a document stored in a BIGALPHANUMERIC or HUGEALPHANUMERIC concept.
Without it, the prototype shows the markup characters: **Why:** instead of Why:.
Usage
The box sits on the object that owns the text, and every item of the box is a text:
"Explanation" : I cRud BOX <MARKUP MARKDOWN> [ "text" : explanation ]
The box cannot sit on the text itself. An annotation such as MARKDOWN reaches a template only
through a box header, and a BOX or VIEW on a text concept makes that concept an
OBJECT, which the type checker refuses next to its REPRESENT (Ampersand v5.9.7).
| Annotation | Value | Effect |
|---|---|---|
MARKDOWN | flag | Markdown, close to CommonMark. |
GFM | flag | GitHub-flavoured Markdown: Markdown plus tables and strikethrough. A task list shows as a plain list: the sanitiser removes its check boxes. |
HTML | flag | HTML. |
LATEX | flag | A LaTeX fragment: mathematics, headings, quotes, theorem-like environments, lists and emphasis. See LaTeX. |
TEXT | flag | Plain text with its line breaks kept. This is also the default. |
formatFrom | item label | The format per row: the value of the named item, such as MARKDOWN. That item shows nothing. |
showLabels | flag | Shows the label of every item above its text. Without it, the texts stand without labels, because the enclosing box usually labels the box already. |
Write the format flags in capitals, as in the table: the template reads them by name. With more than one
flag in the header, the first of MARKDOWN, GFM, HTML and LATEX counts.
A format per row
When the format differs from text to text, a relation to a format concept carries it,
and formatFrom names the item that reads it. The expression may start at the owner or at
the text itself:
REPRESENT Format TYPE ALPHANUMERIC
RELATION bodyFormat[Note*Format] [UNI] -- on the owner
RELATION textFormat[Body*Format] [UNI] -- on the text itself
"Body" : I BOX <MARKUP MARKDOWN formatFrom="fmt"> [ "body" : body, "fmt" : bodyFormat ]
"Body" : I BOX <MARKUP formatFrom="fmt"> [ "body" : body, "fmt" : body;textFormat ]
A row without a format falls back to the flag in the header, and without a flag to TEXT.
In a format item the names are case-insensitive, and some have aliases: MD and COMMONMARK for
MARKDOWN, GITHUB-MARKDOWN and GITHUB_MARKDOWN for GFM, TEX for LATEX, PLAIN and ASCII for TEXT. Format may also be
an OBJECT concept; its atom identifier is then the name. A name the framework does not know,
such as RST, gives plain text and one warning in the browser console. EBCDIC is a character
encoding, not a markup language: a text in the database is already decoded, so it is plain
text as well.
Name the format item with a plain word. The template looks the item up in the row by its label, and a label with spaces or punctuation reaches the row under another name.
LaTeX
A text in a database is a fragment, not a document: a claim with a quote, a definition, some
formulas and the author's own macros. LATEX formats such a fragment and leaves what it does
not know readable, so no text disappears.
| In the text | On the screen |
|---|---|
$…$, \(…\), $$…$$, \[…\], equation, align, gather | Mathematics, rendered by KaTeX as MathML, which the browser draws itself. |
\section, \subsection, \subsubsection, \paragraph | Headings. |
\emph, \textit, \textbf, \texttt, \underline | Emphasis, bold, code, underline. |
quote, itemize, enumerate | A block quote and lists. |
theorem, lemma, corollary, proposition, definition, example, remark, proof | A block that starts with its name, and with the optional title. |
\cite, \citep, \citet, \ref, \eqref | The key between brackets, or the label after an arrow. \label shows nothing. |
verbatim, tabular | The source, as preformatted text. |
An unknown command, such as the macro \lean{Stack.pile} | Its argument, in a span with the class markup__cmd--lean that a project may style; without an argument, the name of the command. |
| An unknown macro in a formula | With an argument, the argument as text (a hint such as \why{multiply by $c > 0$}); without one, an operator name (\softmax). |
A formula that KaTeX cannot parse shows its source. Like every other format, the HTML of a
LaTeX text passes Angular's sanitiser. The formulas are in it as source text; KaTeX builds their
MathML in the page, as DOM nodes and with trust: false. \href and \url give text and no
link (DesignChoices OK-25).
Safety
HTML, also the HTML that Markdown produces, passes the sanitiser of Angular before it
reaches the page. That removes scripts, event handlers such as onerror, and makes a
javascript: link inert. A text therefore cannot run code in the browser of its reader.
Reading, not editing
BOX <MARKUP> is for reading. To edit a text, give it a FORM or a table column of its own,
where the text area of BIGALPHANUMERIC does the work.
Every text of one concept
To format every text of a concept in every interface, without an annotation per interface, use a template per concept; see A template per concept.
BOX \<NOVIEW>
BOX <NOVIEW> renders nothing. Use it to keep a sub-interface in the interface
definition (for example, because the backend or a rule needs the field to be
read) while hiding it from the user interface.
"hiddenField" : someRelation cRud BOX <NOVIEW> []
Atomic templates (interface leaves)
A leaf of an interface — a sub-interface with no further BOX and no user
VIEW — is rendered by an atomic template, chosen by the compiler from the
target concept's REPRESENT type. The framework ships one per TType:
| TType | Atomic template |
|---|---|
ALPHANUMERIC | Atomic-ALPHANUMERIC |
BIGALPHANUMERIC | Atomic-BIGALPHANUMERIC |
HUGEALPHANUMERIC | Atomic-HUGEALPHANUMERIC |
BOOLEAN | Atomic-BOOLEAN |
DATE | Atomic-DATE |
DATETIME | Atomic-DATETIME |
INTEGER | Atomic-INTEGER |
FLOAT | Atomic-FLOAT |
PASSWORD | Atomic-PASSWORD |
OBJECT | Atomic-OBJECT (a concept without a REPRESENT) |
TYPEOFONE | Atomic-TYPEOFONE (the singleton ONE atom; rarely used in a UI) |
The Angular components behind these templates, and how the type mapping works, are described in Frontend components.
A template per concept
Before the atomic template of the technical type, the compiler looks for a template named
after the target concept: Concept-<name>.html in the template folder
(frontend/src/app/generated/.templates/). A project that places such a file there changes how
every leaf with that target concept looks, in every interface. Copy the file in from the project's
Dockerfile, before the frontend is compiled:
COPY project/templates/ /var/www/frontend/src/app/generated/.templates/
Two framework components suit this route. app-atomic-url shows a text as a link.
app-atomic-markup shows a text formatted, in the format its format attribute names (see
BOX \<MARKUP> for the formats); with update rights it shows the text area of
BIGALPHANUMERIC, so the text stays editable. A file Concept-Explanation.html that formats
every Explanation as Markdown reads:
<app-atomic-markup
format="MARKDOWN"
[resource]="resource"
[interfaceComponent]="this"
[property]="resource.$name$"
propertyName="$name$"
label="$label$"
crud="$crud$"
$if(exprIsUni)$isUni$endif$
$if(exprIsTot)$isTot$endif$
></app-atomic-markup>
A template per concept fixes one format for the whole concept. A format per text needs
BOX <MARKUP formatFrom="...">, because only a box sees the other items of its row.