Forms
Form()
Section titled “Form()”function
declare function Form({ row, layout, children }: FormProps): JSX.Element;A form editing one row: the context of its fields, and what renders them.
<Form row={ ws.edit(bar) }/> // the layout derived from the row<Form row={ ws.edit(bar) } layout={ barLayout }/> // a layout<Form row={ ws.edit(bar) } layout="barEdit"/> // a layout by name, from config().layouts<Form row={ ws.edit(bar) }> // the application's own fields <Field path="name"/> <Field path="owner"/></Form>One form edits one row: its type and merge state are the working set’s for the draft. A screen editing several rows has several forms.
Without children, the form renders a layout, the one given or the one derived from the row (deriveLayout()), and wraps every widget in the conflict buttons (ConflictResolution). With children, it only provides the context, and the application’s fields and components render inside it as they are; useFormContext() reads it.
Either way the form has a live region telling a screen reader what somebody else changed (FormStatus): at its top, unless a FormStatus inside it places it. That region is the one markup a form with children adds of its own.
Throws Error for a form given both a layout and children, for a layout name config().layouts doesn’t have, and for a layout of another type than the row’s
FormProps
Section titled “FormProps”interface
interface FormPropsProps of Form.
FormProps.row
Section titled “FormProps.row”row: object;the draft the form edits, what ws.edit(row) or ws.create(type) returned
FormProps.layout
Section titled “FormProps.layout”layout?: Layout | string;what the form renders, or the name of one in config().layouts (namedLayout()); derived from the row without
either
FormProps.children
Section titled “FormProps.children”children?: ReactNode;the application’s own fields, in place of a layout
FormStatus()
Section titled “FormStatus()”function
declare function FormStatus(): JSX.Element;Where the form’s live region goes: what a screen reader is told when somebody else changed a field on screen, or a save left a conflict – once per such event, not once per field. Each field is called by what its label says, and the row by its name fields, so that a page of several forms says which one; a field the form doesn’t show is not mentioned, and what the user changes is not announced. A field is on screen while a Field or useField() renders it.
Every form has the region, at its top unless a FormStatus inside it places it elsewhere, at any depth. A layout
places it with a {status: true} node. The region is there from the start and empty while there is nothing to say,
since a screen reader only announces changes to a region it already knows. It is visually hidden by default; its
class is qlive-form-status.
One FormStatus per form: a second one warns in development and announces everything twice.
Throws Error outside a Form
useFormContext()
Section titled “useFormContext()”function
declare function useFormContext(): FormContext;The context of the form the calling component is in: the row, its merge state, the text fields hold that didn’t parse, and the fields’ modes. What an application’s own component in a form reads it all through.
Rules of hooks apply.
Throws Error outside a Form
FormContext
Section titled “FormContext”interface
interface FormContextWhat a form holds for every field in it, read with useFormContext(). The fields themselves read it through useField().
FormContext.row
Section titled “FormContext.row”row: object;the draft the form edits
FormContext.domainType
Section titled “FormContext.domainType”domainType: string;its type
FormContext.layoutName
Section titled “FormContext.layoutName”layoutName: string | null;the name of the layout the form renders, null for a derived layout and a form with children
FormContext.merge
Section titled “FormContext.merge”merge: MergeAccessor;its merge state
FormContext.invalid
Section titled “FormContext.invalid”invalid: ReadonlyMap<string, string>;The fields whose text didn’t parse, by path, holding the text. That text never reaches the draft, which holds values, and lasts as long as the form, so a field that unmounts and comes back still shows it.
FormContext.modes
Section titled “FormContext.modes”modes: ReadonlyMap<string, FieldMode>;The modes the layout gave fields and setMode() changed them to, by path. A field not in it has the mode its Field gives, or the default (defaultFieldMode()).
FormContext.setMode
Section titled “FormContext.setMode”setMode(path: string, mode: FieldMode): void;changes a field’s mode
Field()
Section titled “Field()”function
declare function Field(props: { path: string;} & FieldOptions): JSX.Element;One field of the form it is in: a label, the widget, the error, with the merge mark on the control.
The widget is the one given, else the most specific override in config().widgets (WidgetOverride), else QLive’s
own for the type: a text input, a number input, a date or date-time picker, a checkbox, a select for a nullable
Boolean, a RowPicker for an enum or a to-one relation, a RowSet for a many-to-many field. A field QLive has none
for, a scalar without input text, shows its value and says so.
A field in text mode renders no widget at all, but the value as text in a qlive-field-text span.
The control, widget or text, carries the merge mark; a decorator goes around the control, so what it adds sits
beside the marked box rather than in it. Before the control sits the mark’s icon, so the states differ by shape
and not by color alone; after it a visually hidden text naming the state, which the widget’s input is described
by (field.descriptionId, see Widget).
Parameters
props |
the field path, and the options of useField() |
useField()
Section titled “useField()”function
declare function useField(path: string, options?: FieldOptions): FieldState;One field of the form the calling component is in: what it is, the mode it is in, its value, and its text.
A path is a field of the form’s row. A to-one relation is named by its object field, owner, and edits the
foreign key behind it; a many-to-many field holds the related rows. A field of a related row, owner.login, is
not a form field.
A widget editing text calls setText(): text that parses (parseInput()) sets the value, text that doesn’t stays as
it was typed, with an error, and is listed in the form’s invalid. A widget editing a value, a checkbox or a
picker, calls setValue() and never has an error.
Rules of hooks apply.
Parameters
path |
field path |
options |
what is given in place of what is derived |
Throws Error outside a Form, and for a path the form’s type has no field for
FieldOptions
Section titled “FieldOptions”interface
interface FieldOptionsWhat a field is given where it is written, in place of what is derived.
FieldOptions.mode
Section titled “FieldOptions.mode”mode?: FieldMode;the mode the field starts in, see FormContext.modes
FieldOptions.widget
Section titled “FieldOptions.widget”widget?: Widget | string;the widget, or the name it is registered under (registerWidget())
FieldOptions.decorator
Section titled “FieldOptions.decorator”decorator?: FieldDecorator | null;the component around the widget, null for none, see WidgetOverride
FieldOptions.label
Section titled “FieldOptions.label”label?: ReactNode;the label, in place of the i18n() of Type.field
FieldOptions.condition
Section titled “FieldOptions.condition”condition?: FilterExpression | null;For a to-one relation, which related rows it offers: a FilterDSL condition written against the form’s row, which reaches the related row through the relation. A related row is offered where the condition holds for the form’s row with that row in the relation’s place.
<Field path="owner" condition={ field("owner.login").ne(field("name")) }/>Since it reads the form’s row, the rows on offer follow it as it is edited.
FieldState
Section titled “FieldState”interface
interface FieldStateOne field of a form, as of this render: what it is, the mode it is in, its value and its text.
FieldState.path
Section titled “FieldState.path”path: string;the path the field was asked for by
FieldState.domainType
Section titled “FieldState.domainType”domainType: string;the form’s type
FieldState.name
Section titled “FieldState.name”name: string;the field’s name in it
FieldState.fieldType
Section titled “FieldState.fieldType”fieldType: string;the field’s type: a scalar or enum type, or the related type for a relation
FieldState.kind
Section titled “FieldState.kind”kind: "scalar" | "toOne" | "manyToMany";scalar: a scalar or enum fieldtoOne: a to-one relation, named by its object field and edited through its foreign keymanyToMany: a many-to-many field, holding the related rows
FieldState.label
Section titled “FieldState.label”label: ReactNode;FieldState.id
Section titled “FieldState.id”id: string;the input’s element id, for the label’s htmlFor
FieldState.labelId
Section titled “FieldState.labelId”labelId: string;the label’s element id, for a widget’s aria-labelledby where the label can’t name the input by htmlFor
FieldState.descriptionId
Section titled “FieldState.descriptionId”descriptionId: string;The id of the text saying what the merge did to the field, which a screen reader reads in place of the mark’s
color. A widget puts it in its input’s aria-describedby, as it puts id on the input: one that doesn’t
leaves a screen reader without the field’s merge state, and nothing looks wrong. Field renders the text; it is
empty while nothing happened to the field.
FieldState.mode
Section titled “FieldState.mode”mode: FieldMode;FieldState.required
Section titled “FieldState.required”required: boolean;non-null in the schema
FieldState.meta
Section titled “FieldState.meta”meta: DomainFieldMeta;the field’s meta data, maxLength and the like
FieldState.condition
Section titled “FieldState.condition”condition: FilterExpression | null;which related rows a to-one relation offers, see FieldOptions.condition; null for all
FieldState.value
Section titled “FieldState.value”value: unknown;the value, read through the draft: the foreign key for a to-one relation
FieldState.setValue
Section titled “FieldState.setValue”setValue(value: unknown): void;writes a value to the draft, and drops text the field held
FieldState.text
Section titled “FieldState.text”text: string;what a text input shows: the text being typed, else the input text of the value
FieldState.setText
Section titled “FieldState.setText”setText(text: string): void;takes what the user typed: text that parses sets the value, text that doesn’t stays, with an error
FieldState.error
Section titled “FieldState.error”error: string | null;why the text didn’t parse, a message for the user; null where it did
FieldState.merge
Section titled “FieldState.merge”merge: MergeField;the merge state of the field the value is in
fieldText()
Section titled “fieldText()”function
declare function fieldText(field: FieldState, row: object, value?: unknown): string;The value of a field as text, what text mode shows: the formatted value, an enum value’s label, the related
row’s name for a relation.
Parameters
field |
the field |
row |
the row it is a field of |
value |
the value to show, the field’s own by default |
FieldMode
Section titled “FieldMode”type
type FieldMode = "normal" | "disabled" | "readOnly" | "text";How a form renders one field.
normal– an input the user editsdisabled– a disabled input: not focusable, and dimmedreadOnly– a read-only input where the widget has one: focusable and selectable, but not editabletext– the value as text in a span, lined up with the inputs around it
readOnly and text both show a value the user can’t change. readOnly keeps the look of an input, text reads
as what it is, which is why it is what a field no form edits gets.
defaultFieldMode()
Section titled “defaultFieldMode()”function
declare function defaultFieldMode(type: string, field: string): FieldMode;The mode a field of a type has where nothing says otherwise: text for a field no form edits, normal for every
other.
No form edits
- a field of the primary key, which identifies the row rather than describing it,
- a computed field, which nothing stores,
- the version field, which only the merge writes,
- a field whose meta data says
editable: false.
A foreign key is edited, through the picker of the relation it is the key of.
Parameters
type |
GraphQL type name |
field |
name of a field of it |
Widget
Section titled “Widget”type
type Widget = ComponentType<WidgetProps>;The input of one field. It gets the whole field state and renders the input only: label, error and merge marks are
Field’s. It only ever sees the modes normal, disabled and readOnly, since a field in text mode renders no
widget.
Two ids tie the input to what Field renders, and a widget must apply both: field.id as the input’s id, which the
label names it by, and field.descriptionId in the input’s aria-describedby, the text saying what the merge did
to the field. A widget that skips the second shows the mark to the eye and hides it from a screen reader.
<input id={ field.id } aria-describedby={ field.descriptionId } ... />WidgetProps
Section titled “WidgetProps”interface
interface WidgetPropsProps of a Widget.
WidgetProps.field
Section titled “WidgetProps.field”field: FieldState;FieldDecorator
Section titled “FieldDecorator”type
type FieldDecorator = ComponentType<FieldDecoratorProps>;A component given a field’s control as children, rendering it with whatever it adds around it. What QLive’s conflict buttons are
(ConflictResolution), and where an application puts UI about a field that no widget should have to know about.
FieldDecoratorProps
Section titled “FieldDecoratorProps”interface
interface FieldDecoratorPropsProps of a FieldDecorator.
FieldDecoratorProps.field
Section titled “FieldDecoratorProps.field”field: FieldState;FieldDecoratorProps.children
Section titled “FieldDecoratorProps.children”children: ReactNode;the field’s control, widget and merge mark
WidgetOverride
Section titled “WidgetOverride”interface
interface WidgetOverrideA widget or decorator for the fields an application doesn’t name one for, listed in config().widgets. It matches
on where the field is and on what it is, each by one key at most:
- where: the
layoutthe field is in, by name, or thedomainTypeof the row the form edits - what: the field’s
path, or itsfieldType
A key left out matches anything, and an override giving both keys of one kind is an error.
config.widgets = [ {fieldType: "Money", widget: MoneyInput}, {fieldType: "AppUser", widget: UserPicker}, {domainType: "Bar", path: "num", widget: Slider}]Of the overrides that match, the most specific wins: what the field is counts first, a path over a field type over neither, then where it is, a layout over a domain type over neither. Of two equally specific ones the later wins. Widgets and decorators are matched separately, so a decorator override doesn’t hide a widget override that is less specific.
WidgetOverride.layout
Section titled “WidgetOverride.layout”layout?: string;the name of the layout the field is in
WidgetOverride.domainType
Section titled “WidgetOverride.domainType”domainType?: string;the type of the row the form edits
WidgetOverride.path
Section titled “WidgetOverride.path”path?: string;the field’s path from that type
WidgetOverride.fieldType
Section titled “WidgetOverride.fieldType”fieldType?: string;the field’s type: a scalar or enum type, or the related type for a relation
WidgetOverride.widget
Section titled “WidgetOverride.widget”widget?: Widget;WidgetOverride.decorator
Section titled “WidgetOverride.decorator”decorator?: FieldDecorator;registerWidget()
Section titled “registerWidget()”function
declare function registerWidget(name: string, widget: Widget): void;Gives a widget the name a layout refers to it by. A layout written in TSX holds the component itself and needs no name; a layout the server delivers can’t hold one.
Parameters
name |
widget name |
widget |
the widget |
ConflictResolution()
Section titled “ConflictResolution()”function
declare function ConflictResolution({ field, children }: FieldDecoratorProps): JSX.Element;The buttons that resolve a field both writes changed, around its widget: one keeping the user’s value, one taking the saved one. Where the saved value isn’t known yet – a push message says which fields changed, not to what – a note instead, until a save brings both.
What a form rendering a layout decorates every control with, unless an override gives a decorator (WidgetOverride). A
hand-written form gives it where it wants it, <Field path="name" decorator={ ConflictResolution }/>.
TextInput()
Section titled “TextInput()”function
declare function TextInput({ field, type, inputMode }: { field: FieldState; type?: string; inputMode?: "decimal" | "numeric";}): JSX.Element;Edits a field as text: String, ID, and every type with an input text (see inputText()).
NumberInput()
Section titled “NumberInput()”function
declare function NumberInput({ field }: WidgetProps): JSX.Element;Edits a number as text. Not type="number": a browser hands that over as empty while it can’t parse it, which
would hide what the user typed, and it has no idea of a bigint.
DateInput()
Section titled “DateInput()”function
declare function DateInput({ field }: WidgetProps): JSX.Element;a date picker
DateTimeInput()
Section titled “DateTimeInput()”function
declare function DateTimeInput({ field }: WidgetProps): JSX.Element;a date and time picker in the user’s time zone
Checkbox()
Section titled “Checkbox()”function
declare function Checkbox({ field }: WidgetProps): JSX.Element;Edits a non-null Boolean. A checkbox has no read-only state, so readOnly disables it.
BooleanSelect()
Section titled “BooleanSelect()”function
declare function BooleanSelect({ field }: WidgetProps): JSX.Element;Edits a nullable Boolean: yes, no, or nothing.
MissingWidget()
Section titled “MissingWidget()”function
declare function MissingWidget({ field }: WidgetProps): JSX.Element;What a field gets where QLive has no widget for it, which a form shows rather than failing: a scalar without an input text.
RowPicker()
Section titled “RowPicker()”function
declare function RowPicker({ field }: WidgetProps): JSX.Element;Picks a value of a closed set: a text input searching the set by what its values are called, and the list of the values found, the first page of them. Choosing one sets the field; a nullable field is cleared by emptying the input. The input shows the chosen value’s name while it isn’t being typed into, and what the user typed while it is; typed text that chooses nothing is dropped when the input loses focus.
What the set is depends on the field:
- a to-one relation picks a related row and sets the foreign key. The rows come from relatedRows(), the server’s related-rows endpoint, found by their name fields and sorted by them.
- a to-one relation to a catalog type picks one of its rows, which the bootstrap brought (see catalogRows()). The set is known in the browser and searched there, by the rows’ names, in the order the server read them.
- an enum field picks one of the type’s values, but the deprecated ones, each called by its
Type.VALUEtranslation. The set is known in the browser too.
A to-one field’s condition (FieldOptions.condition) narrows the rows: tested on each catalog row in the browser, sent to the endpoint otherwise, where the rows are asked for again when a value of the form’s row it reads changes.
QLive’s widget for all of them, so a form edits a closed set without being told how, and the same way however large the set grows.
RowSet()
Section titled “RowSet()”function
declare function RowSet({ field }: WidgetProps): JSX.Element;Edits a many-to-many field: a picker adding one of the rows not yet associated, and after it the associated rows, each named and with a button removing it. The picker comes first so it stays in place while rows come and go. It searches as RowPicker searches a to-one relation’s rows: through the related-rows endpoint, or in the browser for a catalog type. The field is set to the new list of rows, and the merge writes the difference as associations gained and lost.
A row added comes from the picker, with its key and name fields only: other fields of it read as missing until the row’s query is read again.
In disabled and readOnly the rows are listed without the buttons and without the picker.
The whole is a group named by the field’s label, which names the picker too; a hidden text describes what the picker is for. After a row is removed, focus moves to the button of the row after it, else the one before, else the picker.
relatedRows()
Section titled “relatedRows()”function
declare function relatedRows<R = any>(type: string, delta?: QueryConfigDelta): Promise<RelatedRows<R>>;The rows of a type a foreign key references, as the given config narrows and pages them: what a picker offers.
The server answers for the targets of foreign keys only, with the fields they reference and the type’s name fields, and the config filters and sorts by those only. The type’s own query config applies under the one given, and the page is held to the type’s maximum page size.
Running on a fixture, there is no server to ask, and this rejects without trying.
Parameters
type |
the related type, e.g. “AppUser” |
delta |
the config, over the type’s own and the defaults |
RelatedRows
Section titled “RelatedRows”interface
interface RelatedRows<R = any>What relatedRows() resolves with.
RelatedRows.type
Section titled “RelatedRows.type”type: string;the type the rows are of
RelatedRows.rows
Section titled “RelatedRows.rows”rows: R[];the rows of the page, with the key fields foreign keys reference and the type’s name fields
RelatedRows.rowCount
Section titled “RelatedRows.rowCount”rowCount: number;how many rows the condition finds, the page aside
catalogRows()
Section titled “catalogRows()”function
declare function catalogRows<R = any>(type: string): R[];Every row of a catalog type, with every scalar and enum field, in the order the server read them: the type’s sort fields, else its primary key.
A catalog type is an enum bound at application start: a table whose rows don’t change while the application runs, declared with the server’s CatalogMetadataProvider. Its rows come with the bootstrap, so reading them takes no request, and a picker of a relation to it filters them in the browser.
const fooTypes = catalogRows<FooType>("FooType")The rows are shared: treat them as read-only.
Parameters
type |
the catalog type |
Throws Error for a type that is no catalog type, or a page whose config came without its rows – a reduced one, for a view that declared noSchema()
isCatalog()
Section titled “isCatalog()”function
declare function isCatalog(type: string): boolean;Whether the type is a catalog type, see catalogRows().
Parameters
type |
a type name, e.g. “FooType” |
Layout
Section titled “Layout”interface
interface LayoutWhat a form renders for one entity: its fields, grouped and arranged by the nodes.
Layout.name
Section titled “Layout.name”name?: string;the name overrides match it by (WidgetOverride), if it has one
Layout.domainType
Section titled “Layout.domainType”domainType: string;the type of the row the form edits
Layout.nodes
Section titled “Layout.nodes”nodes: LayoutNode[];LayoutNode
Section titled “LayoutNode”type
type LayoutNode = LayoutFieldNode | LayoutSectionNode | LayoutComponentNode | LayoutStatusNode;One node of a layout.
LayoutFieldNode
Section titled “LayoutFieldNode”interface
interface LayoutFieldNodeOne field, rendered by Field with these options.
LayoutFieldNode.field
Section titled “LayoutFieldNode.field”field: string;field path, see useField()
LayoutFieldNode.widget
Section titled “LayoutFieldNode.widget”widget?: Widget | string;the widget, or the name it is registered under (registerWidget())
LayoutFieldNode.mode
Section titled “LayoutFieldNode.mode”mode?: FieldMode;the mode the field starts in
LayoutFieldNode.label
Section titled “LayoutFieldNode.label”label?: ReactNode;LayoutFieldNode.condition
Section titled “LayoutFieldNode.condition”condition?: FilterExpression | null;which related rows a to-one relation offers, see FieldOptions.condition
LayoutSectionNode
Section titled “LayoutSectionNode”interface
interface LayoutSectionNodeA labeled group of nodes, a <fieldset> whose legend is the section, as given. A section that should be translated
is written as an i18n() call, section: i18n("Foo.details"), which keeps its tag where the analysis finds it.
LayoutSectionNode.section
Section titled “LayoutSectionNode.section”section: string;LayoutSectionNode.nodes
Section titled “LayoutSectionNode.nodes”nodes: LayoutNode[];LayoutComponentNode
Section titled “LayoutComponentNode”interface
interface LayoutComponentNodeA component of the application’s, with the nodes as its children.
LayoutComponentNode.component
Section titled “LayoutComponentNode.component”component: ComponentType<any> | string;the component, or the name it is registered under (registerComponent())
LayoutComponentNode.props
Section titled “LayoutComponentNode.props”props?: Record<string, unknown>;LayoutComponentNode.nodes
Section titled “LayoutComponentNode.nodes”nodes?: LayoutNode[];LayoutStatusNode
Section titled “LayoutStatusNode”interface
interface LayoutStatusNodeWhere the form’s live region goes, see FormStatus. Without one it is at the top of the form.
LayoutStatusNode.status
Section titled “LayoutStatusNode.status”status: true;deriveLayout()
Section titled “deriveLayout()”function
declare function deriveLayout(type: string, row: object): Layout;The layout of a form nobody wrote one for: a field node for every field the row has, in the order it has them, which is the order the query selected them in. A new row has every field, in schema order after those set.
- A foreign key and the relation object it is the key of become one node, the relation’s, where the first of the
two is:
ownerIdandownerare edited asowner. - A list is dropped unless it is a many-to-many field.
- A field the schema doesn’t have is dropped.
Every node is a plain field node, so the modes are the defaults (defaultFieldMode()).
Parameters
type |
the row’s type |
row |
a row, or a draft of one |
namedLayout()
Section titled “namedLayout()”function
declare function namedLayout(name: string): Layout;The layout of the given name, from config().layouts: one the server sent with the bootstrap (a LayoutProvider’s),
or one the application put there in startup()’s init hook. The layout carries the name it was found by, which is
what overrides match (WidgetOverride), whatever its own name says.
Throws Error for a name no layout has
registerComponent()
Section titled “registerComponent()”function
declare function registerComponent(name: string, component: ComponentType<any>): void;Gives a component the name a layout refers to it by. A layout written in TSX holds the component itself and needs no name.
Parameters
name |
component name |
component |
the component, rendered with the node’s props and its nodes as children |