Most form fields ask users for a single value like a name, an email, or a date. But some need a list of values.
A plain text input with comma-separated values can technically do the job, but it gives no feedback while typing, no suggestions, and one invalid entry rejects the whole field.
The ipl-web TermInput solves this problem. Each value becomes a separate term with its own validation; terms can be enriched, and the input even supports suggestions.
In this post we will build a small “weekday picker” to show the core features of the TermInput.
Let’s start with a minimal working setup, a form with a single TermInput.
<?php
namespace Icinga\Module\Test\Forms;
use ipl\Web\Compat\CompatForm;
use ipl\Web\FormElement\TermInput;
use ipl\Web\FormElement\TermInput\Term;
use ipl\Web\Url;
class WeekdayPickerForm extends CompatForm
{
protected function assemble(): void
{
$weekdays = (new TermInput('weekdays', [
'label' => 'Selected weekdays',
]))
->setRequired()
->setVerticalTermDirection()
->setReadOnly();
$this->addElement($weekdays);
$this->addElement('submit', 'save', ['label' => 'Save']);
}
}
- setVerticalTermDirection() stacks the selected terms below the input field instead of showing them inline.
- setReadOnly() means existing terms can’t be edited, only removed.
- setRequired() requires at least one term to be selected before submitting.
This already allows the user to enter any term they like and displays it below the input field.

Validation and Enrichment
As we can see, there is still room for improvement. “Mon” is displayed as a literal three-letter code and “January” is simply accepted as a weekday.
To solve this, let’s create a simple function that maps the short form of a day to its full name and ensures the input is actually a weekday.
private array $weekdays = [
'Mon' => 'Monday',
'Tue' => 'Tuesday',
'Wed' => 'Wednesday',
'Thu' => 'Thursday',
'Fri' => 'Friday',
'Sat' => 'Saturday',
'Sun' => 'Sunday',
];
/** @param Term[] $terms */
protected function validateTerms(array $terms): void
{
foreach ($terms as $term) {
$value = $term->getSearchValue();
if (isset($this->weekdays[$value])) {
$term->setLabel($this->weekdays[$value]);
} elseif (in_array($value, $this->weekdays, true)) {
$term->setLabel($value);
} else {
$term->setMessage('Unknown weekday');
}
}
}
The TermInput offers four events that we can listen to. We can simply register our function on all of them, except for ON_SAVE, which is not necessary for a read-only TermInput.
$weekdays->on(TermInput::ON_ENRICH, $this->validateTerms(...))
->on(TermInput::ON_ADD, $this->validateTerms(...))
->on(TermInput::ON_PASTE, $this->validateTerms(...));
Now we have added both a very basic enrichment mechanism and validation that ensures only valid weekdays are entered. The message we display is shown right next to the term that caused it, and prevents the form from being submitted until the user resolves the problem.
Wiring the form into a controller
Without one more piece, the enrichment we just added is incomplete. Every time the user adds a new term the form auto-submits, and the labels on previously entered terms get lost. The TermInput’s multipart update fixes that by streaming the re-rendered terms back to the page on each auto-submit.
We just need to expose the updates in the form:
public function getPartUpdates(): array
{
$this->ensureAssembled();
return $this->getElement('weekdays')->prepareMultipartUpdate($this->getRequest());
}
The controller just needs to forward each part when the form was auto-submitted:
public function indexAction(): void
{
$redirectUrl = Url::fromPath('test/weekdaypicker');
$form = (new WeekdayPickerForm())
->setAction((string) Url::fromRequest())
->on(WeekdayPickerForm::ON_SENT, function (WeekdayPickerForm $form) use ($redirectUrl) {
if ($form->hasBeenSubmitted()) {
$this->redirectNow($redirectUrl);
} else {
foreach ($form->getPartUpdates() as $update) {
if (! is_array($update)) {
$update = [$update];
}
$this->addPart(...$update);
}
}
})
->handleRequest($this->getServerRequest());
$this->addContent($form);
}
Suggestions
Now one thing we still lack is suggestions. We can use ipl-web’s SearchSuggestions to add a simple action to our controller that suggests all weekdays that aren’t already selected.
public function suggestAction(): void
{
$weekdays = [
'Mon' => 'Monday',
'Tue' => 'Tuesday',
'Wed' => 'Wednesday',
'Thu' => 'Thursday',
'Fri' => 'Friday',
'Sat' => 'Saturday',
'Sun' => 'Sunday',
];
$suggestions = new SearchSuggestions((function () use (&$suggestions, $weekdays) {
foreach ($weekdays as $code => $label) {
if (
$suggestions->matchSearch($code)
&& ! in_array($code, $suggestions->getExcludeTerms(), true)
) {
yield ['search' => $code, 'label' => $label];
}
}
})());
$this->getDocument()->addHtml($suggestions->forRequest($this->getServerRequest()));
}
And register it on the TermInput like this:
$weekdays->setSuggestionUrl(Url::fromPath('test/weekdaypicker/suggest'));
Just like that, each term is validated as soon as it is entered and displayed in its long form, while the user gets a list of the remaining weekdays suggested as they type.

Have a look at ipl-web on GitHub for the full source.






