Multiselect
- Version
- 3.x
- Updated
Several values at once, stored as rows in a cross-reference table rather than as a column.
Storage kind: derived · Column: none
Use it for a many-to-many relationship the operator edits as a set: which customer groups a discount
applies to, which languages are active. Use join when exactly one value is picked, and
childentries when the related things are records with their own fields rather
than references — a multiselect assigns existing rows, it does not create them.
Settings
The option list comes from either a model method or a table read directly:
| Setting | Meaning |
|---|---|
modelClass / modelMethod | Options from a model. modelMethod is called with no arguments |
tableOther | Alternative: read options straight from this table |
keyOther | The option's key column — required for both paths, it indexes the option list |
displayColumnOther | The option's label column — required for both paths |
The xref side:
| Setting | Meaning |
|---|---|
xrefTable | The table holding the assignments |
fkOwn | Column pointing back at this record. Empty means a plain value table, not a true xref |
fkOther | Column holding the selected value |
keyOwn | This model's key column, used to look the current assignments up |
usesOrdering | Maintain an ordering column on the xref rows |
Presentation:
| Setting | Meaning |
|---|---|
asCheckboxes | Render checkboxes instead of a multi-select <select> |
activeLanguageHack | Special handling for the active-languages case |
Note that keyOther and displayColumnOther are read on both paths: even when the options come
from modelClass/modelMethod, the returned records are indexed by keyOther and labelled by
displayColumnOther. Omitting them because "the model method returns everything" gives you an
option list of empty labels. Options are sorted naturally by label, not by key.
The form always offers "Select All" / "Deselect All" links.
Absent is not empty
store() deletes the existing rows before writing the submitted set back. That makes the
difference between "no value submitted" and "an empty set submitted" load-bearing:
- absent — the caller gave no instruction. The existing rows are left alone.
- empty array — a real instruction to clear them. The rows are deleted.
Until this was distinguished, any save that did not carry the field wiped the relationship and
re-inserted nothing, with a foreach() warning as the only trace. A single scalar is normalised to
a set of one.
This matters most to API and import callers, which routinely send partial records. A form always
posts the field (an empty multi-select posts an empty array), so the admin UI never hits the
ambiguity — but storeData over MCP does.
Notes
- Existing rows are read before the delete so that per-row extras (such as
ordering) survive a save that merely re-orders or adds. fkOwnempty is the degenerate case — a table of values with no owner, such as#__configbox_active_languages. There the store is a plain delete-all-and-insert.- Deleting the record deletes its xref rows, but only when both
xrefTableandfkOwnare declared. The degenerate no-owner case has nothing to key a cleanup on, which is correct: those rows do not belong to one record. check()always passes.requiredis not enforced for a multiselect — an empty set is a valid set as far as this type is concerned. Enforce a minimum in the model'svalidateData()if you need one.- Being
derived, it contributes no column to the base table and appears in no generated column description.storeExternallyis meaningless here.
@see join.md — the single-value equivalent
@see childentries.md — when the related things are records, not references
@see ../com_configbox_kenedo_model.md — how the model calls a property's store()