Skip to content
V
For developers
Architecture, conventions, the core and security
UI layer / Picker

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#

  1. 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',
    );
}
  1. The autocomplete returns id + label, optionally data for filling in further fields. Always with a limit, otherwise the autocomplete chokes.
  2. labelResolver is mandatory wherever editing happens — without it there is nothing to show when the edit form loads.
  3. The presenter uses PickerHostTrait and returns an array of configurations from pickers(), keyed by a logical name.
  4. In the form, replace the select with a hidden field named exactly as targetField.
  5. Render {control picker-parentId} in the template and write the field's label by hand — a hidden field has none.
  6. Switch on picker mode in the target presenter inside the grid factory (see Admin presenter).
  7. 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:

fieldMap: ['email' => 'ownerEmail', 'company' => 'ownerCompany'],

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