Skip to content

FieldRenderer

<caos-field-renderer>

draws whichever field it is handed, reading or editing, in that field’s own type

May be placed on a record page, a page inside an app and a step of a guided flow.

Name Attribute Description Type Default Required
field property only The field’s metadata — storageType plus label, required, format, scale and formulaReturnType where they apply. Everything the piece draws follows from this. With nothing set it draws nothing at all. A Text field in the rich format reads as formatted text, sanitised by the allow-list the Text kind documents, and edits with a formatting toolbar. THE FORMATS THIS PIECE HONOURS ARE NAMED, not passed through: long reads and edits as several lines; email, phone and url read as the thing they reach, a mail, a dial or a link; currency and percent read as money and as a proportion; relative reads a day or an instant as “3 days ago”. A day, a time of day and an instant each read in ONE named display chosen here rather than per page, so the same stored value reads the same way on a record page, in a list and in a document. A day and a time of day carry no zone and are read back in UTC; only an instant is read in the reader’s own zone. A url value is linked only when it is the web or a path within it: the stored text is the whole URL, so a value naming any other scheme is read as text rather than turned into something a click would run, and so is one naming a user before its host, because the host a reader sees is then not the host a click opens. An email value is linked only when it is ONE address: everything after a ? in a mail URL is the message itself, and a comma adds recipients. That is what makes a component per format unnecessary rather than merely discouraged. json none declared Required
value property only The value, in whatever shape the field’s own type says: a string, a number, a boolean, an ISO date, an array for a multi-select, an id for a reference. Set as a property. json none declared Optional
mode mode, also a property read for the value under its label, edit for the boxed form control, inline for the record-detail affordance — the value in place, click it to edit, commit on blur or Enter, cancel on Escape. Anything else reads as read. text — recognises read, edit, inline; anything else reads as read read Optional
masked masked, also a property This person may not see the value. Draws a padlock and dots in every mode, and never draws the value or an editor — including in edit mode, which is checked before the control is chosen. boolean false Optional
options property only The selectable values for a Select or MultiSelect, as { value, label } pairs. They are also what read mode uses to turn a stored value back into its label, so a select without them reads as its raw value. json none declared Optional
search property only The result source handed on to the lookup for a Reference field — (query) => options. Scoping it to what the current person may read is this function’s job, not the renderer’s. json none declared Optional
refDisplay property only A Reference field’s linked-record name, so the link reads as the record rather than as its id. Set as a property; the attribute spelling is not observed. text none declared Optional
refHref property only Where a Reference field’s link goes. Set as a property; the attribute spelling is not observed. text none declared Optional
richTextToolbar property only The buttons packages add to the editor of a Text field in the rich format, each { key, label, icon, group, order, sourceKey } with exactly one of insert (markup placed at the cursor, sanitised) or command (a name raised in caos-rich-text-command). They come after the platform’s own buttons. The workspace reads them off installed packages’ rich-text capability claims. Set as a property. json none declared Optional
Name When it fires What it carries
caos-field-change When the control it drew reports a change — a field committed, a box toggled, an option chosen, a lookup re-pointed or cleared, or an inline edit committed with Enter. Never when the page sets the value itself. { value } — the new value, in the field type’s own shape.
caos-rich-text-command When a package’s toolbar button that names a command is pressed, in the editor of a rich Text field. Nothing in the platform handles it: it is there for the component the package ships. { command, key, sourceKey, selectedText } — the command name, the button, the package it came from, and the text selected when it was pressed.

Nothing goes inside this piece.

Design token What it controls
--caos-font The typeface of the value and the masked placeholder.
--caos-font-mono The typeface of the small uppercase label above a read value.
--caos-font-weight-label The weight of that label.
--caos-text-body The size of the value.
--caos-color-text The colour of the value.
--caos-color-text-muted The label above the value, a blank em dash, the masked dots and the padlock.
--caos-color-accent The dashed hover cue on an inline value, and the colour a reference reads as.
--caos-space-1 The gap under the label.
--caos-space-2 The gap between the padlock and the value it stands beside.
--caos-duration-fast How quickly the inline hover cue fades in. Falls back to 120ms.

Nothing else drives this piece by calling it.

Nothing on this piece can be read back.

Part Which piece of it When it is there
::part(control) the field body, and the editor it builds for the field type Always
::part(label) the kicker drawn above the value Not in edit mode, where the control carries its own label and this piece draws none.
::part(value) the value as read, the dash standing in for an empty one included In read and inline mode. Edit mode draws a control instead, which carries control rather than this.
::part(masked) the stand-in drawn where the value is hidden Only while masked, which is how a value the reader may not see is shown at all.
::part(help) the field’s own help text, drawn under the control it explains Only where somebody is typing: edit mode, and an inline editor while it is open. A field that declares no help text draws nothing, and a masked field draws none either.

The type is not known when the page is written — a record detail, a generated form, an inspector, anything driven by field metadata. Reach for a typed primitive instead when you know at authoring time that the value is text, or a number, or a date. Note before choosing it for a form that several storage kinds are shown behind a padlock rather than edited: the computed ones because they are not writable, and the temporal, JSON and localizable ones because no editor for them exists yet.

  • TextField — You know at authoring time that the value is a line of text.
  • NumberField — You know it is a number, and you want the min/max/step the metadata cannot carry.
  • DateField — You know it is a calendar date.
  • LocationField — You know it is a place, and you want the latitude and longitude boxes without the metadata.
  • PicklistField — You know it is one of a fixed list, and the list is written into the page rather than read from metadata.
  • LookupField — You know it is a reference, and you would rather drive the read and edit halves yourself.
  • KeyValueRow — The value is only ever read, is already a string, and no type-driven formatting is wanted.
  • AccessDeniedPanel — It is the whole record that is out of reach, not one field on it.

Whatever it draws, it draws the real control, so everything those pieces manage comes with it — the labels, the combobox on a reference, the named group of boxes a multi-select edits with. It adds three things of its own: a multi-select read is a pill container named by the field label, so its values are announced as one list of so many items rather than as loose words; a masked value carries a name saying it is hidden and why, instead of announcing as a row of dots; and an inline value is a role="button" in the tab order, named “Edit “ and the field name, which is what makes click-to-edit reachable without a mouse. What is left to the author is the field LABEL in the metadata — everything the piece announces is built out of it — and the option labels for a select, since read mode falls back to the stored value when it has none.

The same text field both ways: the value under its small label, and the boxed control that carries its own.

The page around it: sets the value on both. A placement writes a string as an attribute, and this piece reads its value only as a property — so the metadata is placed and the value is handed over in code.

{
"id": "example",
"section": "One field, read and edited",
"columns": 1,
"items": [
{
"id": "field_renderer_1",
"type": "component",
"key": "field_renderer",
"inputs": {
"mode": "read",
"field": {
"storageType": "Text",
"label": "Account",
"required": true
}
}
},
{
"id": "field_renderer_2",
"type": "component",
"key": "field_renderer",
"inputs": {
"mode": "edit",
"field": {
"storageType": "Text",
"label": "Account",
"required": true
}
}
}
]
}
<caos-field-renderer id="fieldRenderer1" mode="read"></caos-field-renderer>
<caos-field-renderer id="fieldRenderer2" mode="edit"></caos-field-renderer>
// What an attribute cannot hold, set as a property — the same values the preview is drawn with.
const fieldRenderer1 = document.getElementById('fieldRenderer1');
const fieldRenderer2 = document.getElementById('fieldRenderer2');
fieldRenderer1.field = {
"storageType": "Text",
"label": "Account",
"required": true
};
fieldRenderer2.field = {
"storageType": "Text",
"label": "Account",
"required": true
};

The same two modes over a currency number instead: read formats it as money and lines it up in tabular figures, edit draws a number control stepped to the scale the metadata declares. Nothing about the page changed.

The page around it: sets the value on both, for the same reason as above.

{
"id": "example",
"section": "A different storage kind",
"columns": 1,
"items": [
{
"id": "field_renderer_1",
"type": "component",
"key": "field_renderer",
"inputs": {
"mode": "read",
"field": {
"storageType": "Number",
"format": "currency",
"label": "Total",
"scale": 2
}
}
},
{
"id": "field_renderer_2",
"type": "component",
"key": "field_renderer",
"inputs": {
"mode": "edit",
"field": {
"storageType": "Number",
"format": "currency",
"label": "Total",
"scale": 2
}
}
}
]
}
<caos-field-renderer id="fieldRenderer1" mode="read"></caos-field-renderer>
<caos-field-renderer id="fieldRenderer2" mode="edit"></caos-field-renderer>
// What an attribute cannot hold, set as a property — the same values the preview is drawn with.
const fieldRenderer1 = document.getElementById('fieldRenderer1');
const fieldRenderer2 = document.getElementById('fieldRenderer2');
fieldRenderer1.field = {
"storageType": "Number",
"format": "currency",
"label": "Total",
"scale": 2
};
fieldRenderer2.field = {
"storageType": "Number",
"format": "currency",
"label": "Total",
"scale": 2
};

A location field both ways. Read names each number by its hemisphere, latitude first, so the pair is never two bare numbers to guess the order of. Edit draws a Latitude box and a Longitude box that save together.

The page around it: sets the same place on both, for the same reason as above.

{
"id": "example",
"section": "A place, read and edited",
"columns": 1,
"items": [
{
"id": "field_renderer_1",
"type": "component",
"key": "field_renderer",
"inputs": {
"mode": "read",
"field": {
"storageType": "Geolocation",
"label": "Site"
}
}
},
{
"id": "field_renderer_2",
"type": "component",
"key": "field_renderer",
"inputs": {
"mode": "edit",
"field": {
"storageType": "Geolocation",
"label": "Site"
}
}
}
]
}
<caos-field-renderer id="fieldRenderer1" mode="read"></caos-field-renderer>
<caos-field-renderer id="fieldRenderer2" mode="edit"></caos-field-renderer>
// What an attribute cannot hold, set as a property — the same values the preview is drawn with.
const fieldRenderer1 = document.getElementById('fieldRenderer1');
const fieldRenderer2 = document.getElementById('fieldRenderer2');
fieldRenderer1.field = {
"storageType": "Geolocation",
"label": "Site"
};
fieldRenderer2.field = {
"storageType": "Geolocation",
"label": "Site"
};

The record-detail mode: the value sits in the running text with a dashed cue under it, and clicking it swaps in the underline editor — commit on Enter or on leaving, cancel on Escape.

The page around it: sets the value, for the same reason as above.

{
"id": "example",
"section": "Edited in place",
"columns": 1,
"items": [
{
"id": "field_renderer_1",
"type": "component",
"key": "field_renderer",
"inputs": {
"mode": "inline",
"field": {
"storageType": "Text",
"label": "Account",
"required": true
}
}
}
]
}
<caos-field-renderer id="fieldRenderer1" mode="inline"></caos-field-renderer>
// What an attribute cannot hold, set as a property — the same values the preview is drawn with.
const fieldRenderer1 = document.getElementById('fieldRenderer1');
fieldRenderer1.field = {
"storageType": "Text",
"label": "Account",
"required": true
};

A formula asked to edit: it draws the padlock and the formatted value instead of a control, because the kernel does not let a calculated field be typed over. This is what a form full of fields does with the ones it cannot edit.

The page around it: sets the value, for the same reason as above.

{
"id": "example",
"section": "Calculated, in edit mode",
"columns": 1,
"items": [
{
"id": "field_renderer_1",
"type": "component",
"key": "field_renderer",
"inputs": {
"mode": "edit",
"field": {
"storageType": "Formula",
"label": "Extended price",
"formulaReturnType": "currency"
}
}
}
]
}
<caos-field-renderer id="fieldRenderer1" mode="edit"></caos-field-renderer>
// What an attribute cannot hold, set as a property — the same values the preview is drawn with.
const fieldRenderer1 = document.getElementById('fieldRenderer1');
fieldRenderer1.field = {
"storageType": "Formula",
"label": "Extended price",
"formulaReturnType": "currency"
};

A field the current person may not see: a padlock and dots in place of the value, in read and edit alike, with no value ever put on the page. The only example here that needs nothing from the page around it — there is no value to supply.

{
"id": "example",
"section": "Hidden from this person",
"columns": 1,
"items": [
{
"id": "field_renderer_1",
"type": "component",
"key": "field_renderer",
"inputs": {
"mode": "read",
"masked": true,
"field": {
"storageType": "Number",
"format": "percent",
"label": "Margin",
"scale": 1
}
}
},
{
"id": "field_renderer_2",
"type": "component",
"key": "field_renderer",
"inputs": {
"mode": "edit",
"masked": true,
"field": {
"storageType": "Number",
"format": "percent",
"label": "Margin",
"scale": 1
}
}
}
]
}
<caos-field-renderer id="fieldRenderer1" mode="read" masked></caos-field-renderer>
<caos-field-renderer id="fieldRenderer2" mode="edit" masked></caos-field-renderer>
// What an attribute cannot hold, set as a property — the same values the preview is drawn with.
const fieldRenderer1 = document.getElementById('fieldRenderer1');
const fieldRenderer2 = document.getElementById('fieldRenderer2');
fieldRenderer1.field = {
"storageType": "Number",
"format": "percent",
"label": "Margin",
"scale": 1
};
fieldRenderer2.field = {
"storageType": "Number",
"format": "percent",
"label": "Margin",
"scale": 1
};