Picker#
Choosing a relation instead of using a dropdown: a text field with autocomplete plus a
modal window holding the full listing. The code lives in
app/UI/Admin/System/Components/PickerComponent/.
When to use it and when not to#
| Situation | What to use |
|---|---|
| up to ~50 items, a list that does not change | addSelect() |
| hundreds or more, or searching is needed | the Picker |
| selecting several values at once | addCheckboxList(), or a Picker inside a Multiplier |
A dropdown with ten thousand items does not load, cannot be searched, and pulls the whole table on every opening of the form.
How it is put together#
| Part | Role |
|---|---|
PickerConfig |
a value object — what is being picked and from where |
a picker trait (UserPickerTrait, CategoryPickerTrait, …) |
builds the PickerConfig with its own manager |
PickerHostTrait |
the host in the presenter — provides createComponentPicker() |
Picker + PickerFactory |
the component itself |
picker.js |
the autocomplete, the modal and filling in the fields |
The modal's content is the target presenter's DataGrid in picker mode — there is no second implementation of a listing.
The Picker knows nothing about managers
It takes its data through callbacks in PickerConfig. That is why it works in the
administration and on the front end alike, and why the same picker can be used over
different sources.
Procedure: replacing a select with a picker#
- Write a picker trait into
Presenters/Traits/Pickers/— two methods: autocompletion and translating an id into a label.
protected function userPicker(string $formName, string $targetField): PickerConfig
{
return new PickerConfig(
suggestCallback: fn(string $q): array => $this->suggestUsers($q),
formName: $formName,
targetField: $targetField,
labelResolver: fn(int $id): ?string => $this->resolveUserLabel($id),
targetPresenter: ':Admin:System:User',
);
}
- The autocomplete returns
id+label, optionallydatafor filling in further fields. Always with alimit, otherwise the autocomplete chokes. labelResolveris mandatory wherever editing happens — without it there is nothing to show when the edit form loads.- The presenter
usesPickerHostTraitand returns an array of configurations frompickers(), keyed by a logical name. - In the form, replace the select with a hidden field named exactly as
targetField. - Render
{control picker-parentId}in the template and write the field's label by hand — a hidden field has none. - Switch on picker mode in the target presenter inside the grid factory (see Admin presenter).
- Click through: autocompletion, picking from the modal, saving, reopening the edit form.
pickers() has to be pure and idempotent
It is also called on the autocomplete's AJAX signal, when the form may not be built
yet. Anything in that method touching $form crashes — and it crashes only while
typing into the autocomplete, not when loading the page.
The label must be filled in when the edit form loads
When labelResolver is missing or returns null, the field looks empty even though
the relation exists — and because an empty field is a valid value, saving clears
that relation. All the user did was open the form and click Save.
On editing, prefill through setDefaults()
Not by setting the control's value directly — see Forms.
Filling several fields at once#
fieldMap maps a data key from the autocomplete onto the name of another form field:
The autocomplete returns data: ['email' => '…'] with the item and JavaScript fills those
fields in after the pick. From the modal, the same data is supplied by setPickerData()
on the grid.
A Picker inside a Multiplier#
| Parameter | When it is needed |
|---|---|
targetContainer |
the field sits inside a multiplier container (addresses) |
nestedContainer |
a multiplier inside a multiplier (groups → parameters) |
displayField |
a text control for the label, a sibling of the hidden field |
The instance is then named picker-<key>_<index> (nested: picker-<key>_<i0>_<i1>). The
separator is an underscore, not a hyphen — a hyphen is Nette's component path
separator.
Without displayField the label does not survive an AJAX row redraw
Without it the picker renders a plain, non-submitted input that comes back empty on a redraw. The user sees their picked value "disappear" — while the hidden field still holds it.
A configuration key must not end with _<number>
The instance name is split into key and row indices by a regular expression. A key of
address_2 would break apart into address plus index 2.
Self-reference (parent category, parent page)#
A picker over the same entity has to exclude itself and its subtree — that is what the
persistent pickerExclude parameter and the pickerExcludeSubtreeLft/Rgt pair are for
(Admin presenter).
A cycle in the tree only surfaces at the recalculation
A category set as its own parent saves without an error. What it breaks is the
lft/rgt recalculation of the whole tree — and that happens later, during a
completely different operation.
Redrawing dependent fields after a pick#
submitButton names the submit button the picker should "click" after a successful pick,
so that fields depending on the chosen value get rendered (the options of a chosen
parameter, say).
That button must have an empty validation scope
setValidationScope([]) + onClick → redrawControl. Otherwise the redraw would be
blocked by validation of fields the user has not filled in yet. It will not save — the
admin base only saves through save/update (Admin presenter).
Without a button you can hook your own behaviour onto the pick
picker.js always fires a picker:success DOM event; calling preventDefault() on it
suppresses the automatic redraw.
Creating a new item from the picker#
The picker can create a new item without leaving the form.
A newly created item has to come back with its label too
Otherwise only the id lands in the field and the user sees emptiness — even though the record really was created.
Hidden fields and the form context#
A picker inside a component with its own context can produce a SECOND hidden field of the same name
The wrong one is submitted and the relation is saved empty. The fix is n:snippetArea
around the redrawn part, not {formContext}.
Where to look#
| I want | Where |
|---|---|
| every configuration option | app/UI/Admin/System/Components/PickerComponent/PickerConfig.php |
| the host in a presenter | app/UI/Admin/System/Presenters/Traits/Pickers/PickerHostTrait.php |
| the host in a component | …/PickerComponent/ComponentPickerHostTrait.php |
| the canonical trait example | app/UI/Admin/System/Presenters/Traits/Pickers/UserPickerTrait.php |
| picker mode in the grid | app/UI/Admin/System/Components/DataGridComponent/DataGrid.php |
Follow-up chapters: Working with tables · Admin presenter · Forms