enhancement
major
minor
major
minor
major
#29542
TL Views: channel-bound value inputs for every primitive and reference type, with a submit hook
Problem
Every input in the view layer (com.top_logic.layout.view) is a <field attribute="…"/> inside a <form input="…">: it edits an attribute of a model object, and FieldControlService.createFieldControl(context, part, fieldModel) chooses the control from the attribute's TLStructuredTypePart (the providers and value-type-providers of FieldControlService.Config, plus the attribute's <input-control> annotation).
The one exception was <text-input value="channel"> (TextInputElement, #29530): an input whose value belongs to the view, not to a model object, bound directly to a channel. It was built by hand from an AbstractFieldModel and a ReactTextInputControl, and it existed for text only.
A value the view owns is not always a text: the boundary of a date range, a count, a toggle, an enum literal, a reference to a project or a person chosen from options. Such a value could be entered only by wrapping it in a model object: a transient type with one attribute, a <form> over a transient instance of it, and a channel written from that object. The issue tracker built on the view layer chooses the project of a new ticket exactly this way (TicketTarget, a transient type with a single project reference, edited in a dialog form), and every filter that is not a text term would have to do the same.
The second gap: an input could not fire a command. Enter in a text input did nothing; the only way to act on the value was a separate <button> whose command reads the channel. An "open the ticket with this key" input in the app bar, a filter term applied on Enter, a quick-add field - all need the keyboard to submit.
The third gap: an input standing outside a <form> had no layout. A <field> never renders alone - its <form> renders the responsive grid around its fields that insets them from the container border, distributes them over columns and places each label beside or above its input depending on the column width. An input bound to a channel rendered the same field chrome straight into a flush container, so it stood without padding and with a fixed side label whatever the width.
Solution
One `<value-input>` element for any value type
<value-input value="channel" type="…"> (ValueInputElement) replaces <text-input>: an input bound two ways to a channel, whose control is chosen by the type it declares:
{{{#!xml <value-input value="term" type="tl.core:String" label="…"/> <value-input value="due" type="tl.core:Date"/> <value-input value="onlyOpen" type="tl.core:Boolean"/> <value-input value="status" type="demo.tickets:TicketStatus"/> <value-input value="project" type="my.module:Project" options="all(my.module:Project).filter(p -> $p.get(my.module:Project#active))"/> <value-input value="assignees" type="tl.accounts:Person" multiple="true"/> }}}
- type is a primitive (default tl.core:String), an enumeration or a class; options (a TL-Script expression, optionally over <inputs> channels whose values are its arguments, re-evaluated when one of them changes) supplies the choices of an enumeration or reference input, defaulting to the literals of the enumeration and to all instances of a class; multiple makes the value a collection; label, readonly and label-position as on <field>.
- The channel is written when the value is committed (change for a select, date or checkbox, blur or Enter for a text), and a channel value written from elsewhere - a URL parameter, a <write-channel> - is shown by the input (ChannelFieldBinding, the reusable two-way binding between a channel and a FieldModel).
- Control selection is the same FieldControlService: its type-based dispatch is the base entry point, createFieldControl(context, TLType, FieldSpec, FieldModel) with fieldSpec(type, annotations, label, multiple, model); the attribute-based path builds on it and only adds the <input-control> annotation lookup. The providers that exist serve both paths unchanged.
- The tag is value-input, not input: 21 element configurations declare an input channel property, and a content tag input inside a container with children is ambiguous with that property for the configuration reader.
- <text-input> is removed; its usages (the object-list demo, the documentation) are <value-input type="tl.core:String"> now.
A submit hook
An input may name a command to run with its committed value:
{{{#!xml <value-input value="key" type="tl.core:String">
<on-submit>
<execute-script function="key -> all(demo.tickets:Ticket).filter(t -> $t.get(demo.tickets:Ticket#name) == $key).firstElement()"/>
<write-channel name="ticket"/>
</on-submit>
</value-input> }}}
- <on-submit> holds an ordinary ViewCommand configuration, defaulting to <generic-command> so the actions stand directly inside it; the command is instantiated once with the element and carries its own executability rules, evaluated against the committed value.
- Enter in a single-line text or number input (ReactFormFieldControl.hasSubmitGesture(), the submit command of the client, setSubmitListener) first writes the channel and then runs the command with the committed value; a select, date or checkbox submits on the commit of a chosen value through the binding's commit listener. A multi-line text has no submit gesture.
- ViewCommandModel.forCommand(ViewContext, ViewCommand, ViewCommand.Config) and ViewCommandModel.execute(ReactContext, Object input) run a configured command with a caller-supplied input; a button's click is the same call with its channel value.
The field grid on its own: `<fields>`
<fields> (FieldsElement) lays its content out as the fields of a form, for inputs that belong to the view and therefore stand outside a <form>:
{{{#!xml <fields>
<value-input value="term" type="tl.core:String" label="…"/>
<value-input value="status" type="demo.tickets:TicketStatus" label="…"/>
</fields> }}}
- It renders the same responsive grid a <form> renders around its fields (ReactFormLayoutControl, React component TLFormLayout): the page inset that content owns in the spacing model, auto-fit columns up to max-columns (3 by default), and the layout context that moves a label from beside its input to above it when the column gets narrow.
- label-position is auto (responsive, the default), side or top; the field-level positions are rejected as a configuration error. com.top_logic.layout.react.control.layout.LabelPosition is ExternallyNamed for that, its external name being the value written in a configuration as well as the one sent to the client.
- A <form> needs no <fields>, being such a grid already; a <field> still needs a <form>, since <fields> carries no object.
Documentation and demo
docs/faq/react-view-layer.md describes view-owned values, the <value-input> element, the submit hook and the <fields> grid. com.top_logic.demo.react, view "Object list": the filter term is a <value-input>, a status <value-input> over the TicketStatus enumeration narrows both ticket lists, and a "Jump to ticket" input selects the ticket whose name is entered on Enter; the three stand in one <fields>. Tests: TestValueInputElement, TestFieldsElement (tl-layout-view).
Migration
<text-input value="…"/> no longer exists; write <value-input value="…" type="tl.core:String"/> (the type may be omitted, tl.core:String is the default). The attributes value and label are unchanged. A <value-input> standing outside a <form> is placed in a <fields> to receive the padding, columns and responsive labels of a form.