Skip to main content

Template

Template widgetTemplate widget

A fixed shape with named slots to fill in. The shape's fixed text renders as context and only the slots are typed into, so a value that has to be formatted a particular way does not put that burden on the reader. It collects a string - the assembled shape - and its parts are available alongside it.

$p->template('crate', 'Crate label')
->pattern('{{orchard}}-{{fruit}}-{{grade}}') // The shape to fill in.
->default('valley-pear-a'); // Initial value, assembled.

// Label a slot, and validate it apart from the others:
$p->template('crate', 'Crate label')
->pattern('{{orchard}}-{{fruit}}-{{grade}}')
->slot('orchard', 'Orchard')
->slot('fruit', 'Fruit')
->slot('grade', 'Grade', fn(string $value): ?string => preg_match('/^[a-c]$/', $value) === 1 ? NULL : 'use a single letter a-c');

Runnable script: playground/02-widgets-template.php.

Options

NameDescriptionRequiredDefault
pattern()The shape to fill in, carrying {{name}} slots.YesNone
default()Initial value, as the assembled string.No'' (empty)
slot()Label and validator of one slot: slot(string $name, string $label = '', ?Closure $validate = NULL).NoNone

A slot name is a word ({{grade}}, {{part_1}}), and inner whitespace is allowed ({{ grade }}). Every other character in the pattern is fixed text.

Only the slot name is required: an omitted label falls back to the slot name, and an omitted validator leaves the slot checked by the field's own validator alone - so ->slot('grade') is enough to name a slot without constraining it.

Two rules are enforced when the form is built, both raising a FormException: a pattern must declare at least one slot, and two slots must be separated by some fixed text. A slot ends where the next fixed chunk begins, so {{a}}{{b}} could not be read back into its parts.

Reading the answer

The answer is the whole assembled string; the parts are read back off it, so the two can never disagree.

$answers = (new Tui($form))->run();

$answers->value('crate'); // 'valley-pear-a'
$answers->parts('crate'); // ['orchard' => 'valley', 'fruit' => 'pear', 'grade' => 'a']

parts() returns an empty array for any question that is not a template, and for a template answer that does not have the shape.

Keyboard

KeyAction
printable keysInsert into the slot holding the caret
Tab / Move to the next slot
Move to the previous slot
/ Move the caret inside the slot
BackspaceDelete the character before the caret
EnterAccept
EscCancel

Moving off a slot validates it. A rejected slot shows its error but does not hold the caret, so slots can be filled in any order; accepting validates every slot and puts the caret back on the first one that fails.

Because a slot ends where the next fixed chunk begins, a slot's value cannot contain that chunk: valley-west in the first slot of {{orchard}}-{{fruit}} would move the boundary, and the answer would read back as orchard: valley, fruit: west. Accepting rejects it. The last slot runs to the end of the string, so it has no such limit.

Headless behavior

An unattended run asks for the assembled value directly - there are no slots to tab between - and holds it to the same rules the editor does: it must have the shape, and every slot must pass its own validator.

APP_CRATE=valley-pear-a php collect.php

An empty value is an unfilled template, left to the required() check rather than rejected as a mismatch. In the agent schema the field is a string carrying a pattern - the expression the assembled answer must match - so a generated answer can be checked before it is sent.

Display modes

In all four display modes - Unicode or ASCII, color on or off:

ANSINo ANSI
UnicodeTemplate: Unicode + ANSITemplate: Unicode + ANSITemplate: Unicode + No ANSITemplate: Unicode + No ANSI
ASCIITemplate: ASCII + ANSITemplate: ASCII + ANSITemplate: ASCII + No ANSITemplate: ASCII + No ANSI