// Field renderer registry (D-05). A form field's `type` selects the control // component; any type without a renderer gets UnsupportedField, the design's // dashed box, so an unknown type never breaks the form. The relation manager // is registered like any control but holds no form value: it edits its // relation through its own endpoints and renders only on an existing record. // Phase 10.1 adds the plugin extension types: a widget mounts a plugin custom // element (D-04, D-09) and a partial renders a server template (D-17). They // hold no form value either, but they render on create and update. // Phase 12.2 adds the fileupload control: it edits its files through the file // routes against the form's session key (D-02, D-03), so it holds no form // value either and renders on create and update. The datepicker control is // a plain value field: its string value is part of the save body (D-18). // Phase 12.1 adds the password control: a plain value field whose value the // server never sends back, so it is empty on load and after every save. import type { Component } from 'vue' import CheckboxField from './fields/CheckboxField.vue' import DatepickerField from './fields/DatepickerField.vue' import DropdownField from './fields/DropdownField.vue' import FileuploadField from './fields/FileuploadField.vue' import NumberField from './fields/NumberField.vue' import PartialField from './fields/PartialField.vue' import PasswordField from './fields/PasswordField.vue' import RelationManager from '../relation/RelationManager.vue' import RelationField from './fields/RelationField.vue' import SwitchField from './fields/SwitchField.vue' import TextField from './fields/TextField.vue' import TextareaField from './fields/TextareaField.vue' import UnsupportedField from './fields/UnsupportedField.vue' import WidgetField from './fields/WidgetField.vue' // The control helpers live in ./control so the field components never import // this module: registry -> field -> registry would be an import cycle whose // evaluation order decides whether a renderer is registered at all. export { controlAttributes, controlClass, toggleOn, toggleValue } from './control' export type { FieldControlProps } from './control' /** The relation manager's field type (design screen 5). */ export const RELATION_MANAGER = 'relation-manager' // The relation manager renders child forms (FormGrid -> FormField -> this // registry), so it sits on an import cycle with this module. It is looked up // when a field renders, never while this module evaluates: whichever module // of the cycle loads first, the binding is complete by then. const lazyRenderers = new Map Component>([[RELATION_MANAGER, () => RelationManager]]) const renderers = new Map([ ['text', TextField], ['textarea', TextareaField], ['number', NumberField], ['dropdown', DropdownField], ['switch', SwitchField], ['checkbox', CheckboxField], ['relation', RelationField], ['widget', WidgetField], ['partial', PartialField], ['fileupload', FileuploadField], ['datepicker', DatepickerField], ['password', PasswordField], ]) /** Types whose control shows the label itself (toggle cards, relation manager). */ const selfLabelled = new Set(['switch', 'checkbox', RELATION_MANAGER]) /** * Types that need a saved record: never rendered on create (D-05, design * screen 5), except a relation manager whose field is deferrable (Phase * 12.2, D-03): FormView keeps it and it works on owner id 0. */ const recordBound = new Set([RELATION_MANAGER]) /** Types that hold no form value: never part of the save body (D-09). */ const valueless = new Set([RELATION_MANAGER, 'widget', 'partial', 'fileupload']) /** * Types whose control is a group rather than one focusable element: the * visible label is a span the group points at, not a label for an input. */ const groupLabelledTypes = new Set(['widget', 'partial', 'fileupload']) export function rendererFor(type: string): Component { return renderers.get(type) ?? lazyRenderers.get(type)?.() ?? UnsupportedField } /** Whether the SPA can edit values of this field type. */ export function isRegistered(type: string): boolean { return (renderers.has(type) || lazyRenderers.has(type)) && !valueless.has(type) } /** Whether a field type renders only on an existing record (relation manager). */ export function needsRecord(type: string): boolean { return recordBound.has(type) } export function ownsLabel(type: string): boolean { return selfLabelled.has(type) } /** * Whether a field type's label names a group (widget, partial) instead of a * control. Such a row shows no label row when the field declares no label. */ export function groupLabelled(type: string): boolean { return groupLabelledTypes.has(type) }