enhancement
major
minor
major
minor
major
#29615
TL Views: a <form> spanning a page does not take part in the fill contract, so a split panel or filling panel inside it is sized by its content
Problem
The ticket page of the issue tracker tl-dev shows one object in three areas: a header (key, summary, status, type), a main column (description, attachments, the discussion) and a side column of cards (classification, people, planning, context, history, links). Each area is a <form input="ticket">, so the page carries three Edit / Save / Cancel sets, and a user editing the summary and the priority has to save twice. Wanted: one edit mode for the page. Two ways were tried against the snapshot of 2026-09-16.
1. Three forms sharing one edit-mode channel (not a solution)
FormElement.Config.getEditMode() (editMode="ch") binds a form's mode to a channel two-way. All three forms do switch together, but a Save in one form discards the input of the other two: FormControl.handleEditModeChannelChanged calls executeCancel() when the channel turns false, and a saving form publishes exactly that in discardEditSession. Observed: summary changed in the header, priority changed in a side card, Save in the header - the summary is stored, the priority is back to its old value after a reload. mode-switch="false" on the other forms only hides their buttons (FormElement.contributeEditCommands is skipped); the channel listener stays. No action stores another form's state: <store-form-state> / <discard-form-state> read ViewContext.getFormModel(), their own form, and a nested <form> does not register as a FormParticipant of an outer one (only AttributeFieldControl and AbstractCompositionControl do).
The edit-mode channel is a mode switch, not a shared edit session. Three forms over one object are the wrong structure for one edit mode; the structure is one form (2.).
2. One form spanning the page (the right structure, fails on layout)
A root <panel> (title, workflow commands) → <form input="ticket"> → the existing <split-panel> with its panels and cards. This is functionally right: one Edit / Save / Cancel beside the workflow commands, fields bind through panels, split panels and cards (DefaultViewContext.childContext propagates the form model), one Save persists summary, description and priority in one transaction, and the discussion's <object-list> with its own composer form keeps working nested inside.
It fails on layout: the form's control TLFormLayout is a self-padding CSS grid that does not take part in the fill contract (react-src/bridge/fill.ts: no useFillHost), and FormElement.Config has no fill option. A <split-panel> inside the form is therefore sized by its content instead of by the height the container offers: measured .tlSplitPanel 2284 px inside an 844 px tile-stack frame, the header pane a ~570 px blank band, the page scrolling as a whole. css-class is not applied by TLFormLayout, so nothing on the app side reaches it.
Lösung
A <form> takes part in the fill contract as a container that follows its children, like <inset>, <stack> and the tile-stack frame do: TLFormLayout reports through useFillHost and provides the host to its children. While a child reports filling (a <split-panel>, a <panel fill="true">), the form carries the fill class and lays itself out as a flush column (tlFormLayout--fill: no page inset, the filling child takes the height the form is offered, every other direct child keeps the height of its content), so the child takes the height the form's container offers and scrolls its own body. Around content of its own size the form is the padded responsive grid it always was. No configuration option: the decision is derived from the content, exactly as for the other fill-following containers.
Fields inside the areas of such a form
A form that is flush around a split panel lays none of its fields out itself, and a <panel> does not pad its content (spacing model: containers never introduce padding). The fields of each area therefore stand in a <fields> grid (FieldsElement), which insets them from the pane border, distributes them over the responsive columns and lets them fill the column width - the same grid a plain <form> renders around its fields. An area narrower than the columns of the default grid takes max-columns="1", so that the label position follows the width of the area (beside the input while it is wide, above it once it is narrow):
<form input="ticket">
<split-panel><panes>
<pane><panel>
<fields max-columns="1">
<field attribute="summary"/>
<field attribute="description"/>
</fields>
</panel></pane>
...
</panes></split-panel>
</form>
A <fields> grid standing inside a <form> follows the form's edit mode: the field chrome (required marker, read-only appearance, visibility of errors and help texts) reads the read-only state of the nearest grid, so the grid mirrors the form through a FormModelListener (FormLayoutEditModeBinding, bound in FieldsElement.createControl when the view context carries a form model; the listener is removed when the grid is disposed). A <fields> outside a form is never read-only, as before; a <form> nested inside another <form> keeps its own edit mode. FormControl.fireFormStateChanged notifies a copy of its listener list, so a listener deregistering during the notification is safe. Test: TestFieldsElement.
Forms sharing one edit session over several objects are not part of this ticket.
Documented in docs/faq/react-view-layer.md (the <fields> bullet and the section on the fill contract). Demo in tl-demo-react: sidebar entry "Page form" (demo/form-fill-demo.view.xml) - a selector table beside a panel whose <form> wraps a nested <split-panel>; each pane is a <panel> whose fields stand in a single-column <fields> grid, the panes are sized by the frame, and the single Edit / Save / Cancel set in the panel toolbar saves both panes in one transaction.