Data types
A DataType describes one piece of typed data: how it renders as an input, how it validates, and which filter operators apply. The same instance drives a Form field (Forms), a TableView column (Tables), and that column's filters — declare the type once and behaviour stays consistent everywhere.
A field's type is always a DataType instance, constructed with per-field options:
import { DefaultDataTypes } from "@antelopejs/interface-dms/base/data-types";
{
id: "quantity",
label: "Quantity",
type: new DefaultDataTypes.NumberType({ min: 0, max: 1000, step: 1 }),
}
The standard catalog lives in the DefaultDataTypes namespace, with StatusType, DataType, DefaultDataCompareTypes, RegisterDataType, and RegisterDataCompareMode exported alongside it.
The built-in catalog
Each entry is registered with @RegisterDataType("<id>"). "Renders" names the FormComponents.* widget the type produces; "Compare modes" lists its filter operators (default in bold). Most types share one of four operator sets:
- text —
contains,is,is_not,not_contains,is_empty,is_not_empty - numeric —
is,is_not,greater_than,greater_than_or_equal_to,less_than,less_than_or_equal_to,is_empty,is_not_empty - equality —
is,is_not,is_empty,is_not_empty - membership —
include,exclude
| Id | Class | Renders | Key options | Compare modes |
|---|---|---|---|---|
string | StringType | text input, textarea when textarea: true, a read-only value with a copy button when copyable: true | placeholder, minLength, maxLength, showCount, textarea, rows, copyable, width | text |
number | NumberType | numeric input | min, max, step, placeholder, suffix, width | numeric |
price | PriceType (extends NumberType) | numeric input | inherits NumberType; defaults min: 0, step: 0.01 | numeric |
percentage | PercentageType (extends NumberType) | percentage input (value stored 0–1, shown ×100) | inherits NumberType; defaults min: 0, max: 1, step: 0.01 | numeric |
string_time | StringTimeType | duration input: reads 08:15, 8:15, 8h15, 8h, 1h 30m; the field writes the exact duration (7 h 30 min 15 s) and reads back what it writes; a cell shows it rounded (7 h 30 min), as a duration cell does; value: milliseconds | placeholder, min, max (milliseconds) | numeric |
boolean | BooleanType | switch, checkbox or card (display); the filter uses a checked/unchecked select | display (switch, checkbox — required, it must be ticked —, card), label, description, icon (card) | equality |
select | SelectType | dropdown, cards, segments or radios (display); the filter is always a dropdown | items: { label, value, description? }[], placeholder, multiple, deselectable, display (segmented/radio pick one: a multiple select shows cards) | single: is, is_not; multiple: include, exclude; plus is_empty, is_not_empty |
date | DateType | date picker (range picker for is_between) | range (value { start, end }), multiple, minDate, maxDate | is, greater_than, less_than, is_between, is_empty, is_not_empty |
email | EmailType | email input | placeholder | text |
url | UrlType | text input | placeholder, preview (avatar / image: shown read-only — a cell, a record's details — the URL is drawn as the image it points to, a remote photo as a picture rather than a link, named for screen readers after its row's name, label or title, or else the field's label) | equality |
phone | PhoneType | phone input | placeholder, requiredPrefix | text |
password | PasswordType | masked input (optional confirm) | placeholder, minLength, confirmPassword, confirmPlaceholder | none (not filterable) |
color | ColorType | color picker | placeholder | equality |
rich_text | RichTextType | rich-text editor | placeholder | none (not filterable) |
file | FileType | file upload | multiple, constraints (UploadConstraints), path, storage | none (not filterable) |
image | ImageType | image upload with alt text / principal flag | multiple, max, constraints, path, storage, resize ({ maxWidth, maxHeight, fit? }, client-side downscale before upload) | none (not filterable) |
relation | RelationType | searchable relation picker | dataApiController, keyMapping (label/value/avatar/disabled), index, multiple, deselectable, placeholder, filterOnly, filterBy | equality; membership with multiple |
cascader_relation | CascaderRelationType | cascading picker for a self-referencing table | dataApiController, keyMapping (with parent), maxDepth, leafOnly, index, multiple, deselectable, placeholder | equality; membership with multiple |
address | AddressType | structured postal address (optional autocomplete) | placeholder (per-field), autocomplete (suggestions under the street: each place once, the country's first, Escape closes them), defaultCountry (ISO code shown while the address has none, written with its first part), dataApiController, keyMapping | is_empty, is_not_empty |
status | StatusType | online/offline status badge | onlineLabel, offlineLabel, onlineColor, offlineColor | is, is_not |
array | ArrayType | repeater: a row per item, a column per field, "+ Add …", ✕, drag handle | of: { [key]: DataType | { type, label?, required? } }, sortable, min, max, addLabel; value: a list of objects | is_empty, is_not_empty |
key_value | KeyValueType | repeater of a name and a value | valueType (text by default), toggleable (value per key becomes { value, enabled }), addLabel, keyLabel, valueLabel; value: an object | is_empty, is_not_empty |
secret | SecretType | masked input (••••f3a9), Show and copy in the browser, Rotate… | revealable, copyable (both default true), rotateUrl (POST after a confirmation, answering { value }), placeholder; the value travels as is | is_empty, is_not_empty |
code | CodeType | code editor (CodeMirror, loaded on demand): line numbers, highlighting, autocomplete (Ctrl+Space) | language (json, sql, html, javascript, text), lineNumbers, minLines, maxLines, completions, completionsUrl (GET, { items } of { label, detail?, icon? }), placeholder; JSON that does not parse is refused; value: the text | is_empty, is_not_empty |
tags | TagsType | tags input: a pill per item, duplicates ignored | itemType (string, email — an item that is no address shows red and is refused), max, suggestions, placeholder; value: a list of texts | array_contains_string (holds a tag, the text typed matched inside each tag), is_empty, is_not_empty |
A date filter works in whole UTC days: a picked day is the whole day for is and is_between, greater_than keeps what comes after it (from the next day) and less_than what comes before it. A value with a time, such as a view's {{now-7d}}, is compared as that instant.
A multiple relation (multiple: true) stores a list of ids, so its filter asks whether the list includes the picked row (include, the default) or not (exclude), one row at a time.
A number's unit goes inside its field, after the value, with suffix — NumberType({ min: 0, suffix: "$billing.days" }) reads "30 days", and the field stays as wide as the others — while the value stays a bare number; a PriceType takes one too. It is written wherever the value shows: after the form field, and after the formatted number in table cells, expanded rows, cards and read-only fields. A text field shows the characters typed with showCount: true, out of maxLength when it has one ("4 / 60"): at the end of a single-line field, under a textarea.
{ id: "graceDays", label: "Grace period", type: new DefaultDataTypes.NumberType({ min: 0, suffix: "$billing.days" }) },
{ id: "name", label: "Workspace name", type: new DefaultDataTypes.StringType({ maxLength: 60, showCount: true }) },
A short value does not need the whole row. A single-line number, percentage or text field takes width: "compact" to size itself for a few characters (11rem), and width: "full" to fill the row. Left out, the bounds decide: a number whose min / max fit in 6 digits (a percentage, by its 0–100), or a text whose maxLength is 16 or less, is compact; anything else fills the row. A number with no max -- an estimate in hours, a quantity -- is unbounded, so it asks for it. The compact width applies from the sm breakpoint up, in a page form and a drawer alike: on a phone every field keeps the full row.
{ id: "estimateHours", label: "Estimate", type: new DefaultDataTypes.NumberType({ min: 0, step: 0.5, suffix: "h", width: "compact" }) },
{ id: "currency", label: "Currency", type: new DefaultDataTypes.StringType({ maxLength: 3 }) }, // compact by itself
The display options of SelectType and BooleanType, StringType({ copyable }) and the array, key_value, secret, code and tags types are described with examples in Field types.
RelationType's keyMapping.avatar accepts either a URL string or a stored image value ({ key } from an ImageType field on the related record — e.g. a DMS member's profile avatar); image keys are resolved to read URLs by the picker.
A relation picker can depend on another field of its form with filterBy: { field, from }: it lists only the related rows whose field equals the form's current value of from. A ticket's site among its customer's sites, a project's quote among its customer's quotes:
@Column({
name: "Site",
type: new DefaultDataTypes.RelationType({
dataApiController: SiteController,
keyMapping: { label: "name", value: "_id" },
filterBy: { field: "customer", from: "customer" },
}),
})
site!: string;
The picker searches the related select route with the table filter syntax (filter_customer=is:<id>) and searches again when from changes; a pick made for the previous value is cleared, and going back to the value it was made with keeps it. The record's stored pair is kept as the form loads. While from is empty, every row is offered. field must be filterable on the related controller (filterable: true on its column, or @Filter), since the route ignores any other filter: a relation declaring an unfilterable field throws at registration. The picker narrows what is offered; a write rule that must hold (the site belongs to the customer) is still checked by the module's routes.
A RelationType field takes its value as the joined row ({ _id, name }) or as the plain id ("u-5", or a list of ids when multiple). A form whose read answers ids shows each one's label and avatar all the same: the picker asks its target's select route for those rows alone (?ids=u-5&idKey=_id, idKey being keyMapping.value), so a route serving a standalone form need not join the related rows.
A list is the other way round: a table cell, a card and a kanban card draw the joined row and never read the ids themselves, so a route listing rows for them must join every relation it shows. The Foreign join a RelationType or CascaderRelationType column installs does it for the DMS List route; a custom list route has to apply it too, and a column declared filterOnly skips it, so its cells show the raw id. A filter chip names its ids by ?ids=, like the form.
An id the select route does not answer — a row deleted, or out of the reader's reach, which the client cannot tell apart — reads "Not found", with the id in its tooltip, in the picker, a filter chip, a read-only list and a cascader cell.
A CascaderRelationType cell and its picker's field show the path of the picked row ("Electronics / Audio / Speakers"), read by ?ids= one level at a time: the picked rows, then their parents, until a root. That is as many requests as the tree is deep, whatever its size, on the route the picker already reads; the open picker still lists the route's rows to browse the tree.
How a type is consumed
The same instance works as a form field type and a table column type; on a table it also supplies the column's filter operators. Table columns are declared with the @Column({ … }) property decorator on the data-api controller class (Data controller), not on the page:
// Form field (on a page)
{ id: "email", label: "Email", type: new DefaultDataTypes.EmailType({ placeholder: "[email protected]" }) }
// TableView column (on the data-api controller) — the type drives the cell, the filter widget, and the operators
@Column({ name: "Unit Price", type: new DefaultDataTypes.PriceType(), filterable: true })
price!: number;
Compare modes
A compare mode is one filter operator — the is in "status is done". Each type declares the modes it supports and a default; the built-ins live in the DefaultDataCompareTypes namespace:
- Equality —
is,is_not. - Ordering —
greater_than,greater_than_or_equal_to,less_than,less_than_or_equal_to,is_between. An empty value is neither before nor after a bound, so these leave it out: a deal with no last activity is not "less than 21 days ago".is_emptyfinds those rows. - Text —
contains,not_contains. - Sets —
include,exclude,array_contains_string. A row whose list isnullor missing (an optional field saved empty) reads as an empty list:includeandarray_contains_stringleave it out,excludekeeps it. - Emptiness —
is_empty,is_not_empty. These take no value: selecting the mode is the filter.
Writing a custom DataType
The catalog is extensible: register your own type and it behaves like any built-in — as a form field, as a table column, and in that column's filters. A custom type has two halves. The backend class declares the type's validation, compare modes, and input widget; an optional frontend half formats its values for table cells and read views. The full contract, with annotated status and rating walkthroughs, is Custom data types.
Permissions
DataTypes carry no permission logic of their own — access is enforced at the page/component level, so gate field visibility by configuring permissions on the owning page (see Auth & permissions).