# Multiselect

> Several values at once, stored as rows in a cross-reference table rather than as a column.

Source: CBX documentation, version 4.0 preview (unreleased). Canonical page: https://docs.configbox.at/docs/4.0-preview/technical/property-types/multiselect. Last updated 2026-07-27.

---
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`](https://docs.configbox.at/docs/4.0-preview/technical/property-types/join) when exactly one value is picked, and
[`childentries`](https://docs.configbox.at/docs/4.0-preview/technical/property-types/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.
- `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()`
