Skip to main content
Version: 4.0 preview

Multiselect

Version
4.0 preview
Updated
View markdown

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:

SettingMeaning
modelClass / modelMethodOptions from a model. modelMethod is called with no arguments
tableOtherAlternative: read options straight from this table
keyOtherThe option's key column — required for both paths, it indexes the option list
displayColumnOtherThe option's label column — required for both paths

The xref side:

SettingMeaning
xrefTableThe table holding the assignments
fkOwnColumn pointing back at this record. Empty means a plain value table, not a true xref
fkOtherColumn holding the selected value
keyOwnThis model's key column, used to look the current assignments up
usesOrderingMaintain an ordering column on the xref rows

Presentation:

SettingMeaning
asCheckboxesRender checkboxes instead of a multi-select <select>
activeLanguageHackSpecial 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.
  • fkOwn empty 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 xrefTable and fkOwn are 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. required is 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's validateData() if you need one.
  • Being derived, it contributes no column to the base table and appears in no generated column description. storeExternally is 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()