# Configurator Questions & Answers, in Detail

> > Scope: how the configurator's questions and answers are rendered to HTML, how the > browser reacts to a selection, how that selection round-trips to the se…

Source: CBX documentation, version 4.0 preview (unreleased). Canonical page: https://docs.configbox.at/docs/4.0-preview/technical/configurator_questions. Last updated 2026-08-25.

---
> **Scope:** how the configurator's **questions** and **answers** are rendered to HTML, how the
> browser reacts to a selection, how that selection round-trips to the server, and how the server's
> answer is applied back to the page — the whole live-configurator loop, end to end ·
> **Last reviewed:** 2026-08-02

The goal is that anyone using the **customization layer** — a `custom_questions.js` module, a
template override, a custom question type — can see exactly where their code plugs in and what
contract it must honour.

All paths are relative to `docroot/components/com_configbox/`. Source references track **current
master** (sampled at the last-reviewed date above); exact line numbers drift, so verify against the
code before relying on one — the function names are the stable anchors. The two frontend files at
the centre of this are `assets/javascript/configurator.js` (the engine) and
`assets/javascript/questions.js` (the twelve built-in question types).

---

## 0. Mental model

A configurable **product** has ordered **pages**; each page has ordered **elements** (the things
this doc calls **questions**); a choice-type question has **answers** (options). The customer works
through the pages; every time they touch a control, one question's selection is sent to the server,
the server re-runs rules + pricing for the *whole* configuration, and sends back a set of
**instructions** the browser applies to the page it already has.

Four ideas carry the whole system:

1. **One question per view, keyed by type.** Each question is rendered by its own view
   (`ConfigboxViewQuestion_<Type>`) into a self-contained `<div class="question" …>`. The page view
   concatenates them. There is no monolithic form.

2. **The DOM is the state — sort of.** Each question wrapper carries its current value in
   `data-selection` / `data-output-value` and its type in `data-question-type`. But the wrapper is
   *not* authoritative: the **server** holds the real configuration (`ConfigboxConfiguration`). The
   DOM is a mirror the JS keeps in sync.

3. **One selection at a time, over XHR.** There is no form submit and no batch of selections. A
   change to question *N* sends only question *N*'s value to the task
   `configuratorpage / makeSelection`. The server persists it, then recomputes everything.

4. **The response is instructions, not (question) HTML.** `makeSelection` returns a **JSON object** —
   "select this, deselect that, hide these questions, these are the new prices, this field's min is
   now 5" — and the client mutates the existing DOM. It does **not** return re-rendered *question*
   markup. (Page navigation and the initial paint do fetch HTML — see §4.6; and a customization can
   add its own HTML fragment to the response via `postMakeSelection` — see §7.4.)

The rest of this document is: how a question is built server-side (§1–§2), the markup contract each
type emits (§3), how the JS wires itself to that markup (§4), the round-trip in both directions
(§5–§6), and — the payoff — the customization seams (§7).

> **Validated against a live site.** Every DOM-contract claim (§3–§4), the `makeSelection` request
> payload (§5), and the full response schema (§6) in this document were confirmed by driving a real,
> heavily-customized production configurator (Beta-Calco) — including the `postMakeSelection` seam
> (§7.4), the deprecated-`registerQuestion` alias, and the 12 built-in types coexisting with custom
> ones. Where a real deployment diverges from stock behavior, it is called out inline.

---

## 1. Server-side: from a page to a stack of questions

The orchestrator is **`ConfigboxViewConfiguratorpage`** (`views/configuratorpage/view.html.php`),
*not* the product view. `views/product/` is a product-detail/landing page; it only links to the
configurator (`views/product/view.html.php:108`).

`prepareTemplateVars()` builds the page (`views/configuratorpage/view.html.php:335`):

1. **page → product** via the assignments helper:
   `ConfigboxAssignmentsHelper::getProductIdForPage($this->pageId)` (`:342`).
2. Load the product and page models; abort if either is unpublished.
3. **product → pages** — `$pageModel->getPages()` for tab/button nav and `pageSequence`.
4. **page → questions** — again from the assignments helper (`:503`):
   ```php
   $questionIds = ConfigboxAssignmentsHelper::getPageQuestionIds($this->pageId);
   ```
   The list order **is** the question ordering. The helper's per-request memo uses the canonical
   CB4 key `page_to_question` (`helpers/assignments.php:204-214`); the legacy whole-catalog
   `ConfigboxCacheHelper::getAssignments()` blob still exists for customizations, with the old
   `page_to_element` name kept only as a **deprecated alias** — its own comment says to move
   custom code to the `question_*` keys (`helpers/cache.php:607-618`). New code uses the helper
   methods, not the blob.
5. **The core render loop** (`:513-523`) — one view per question, keyed by type:
   ```php
   foreach ($questionIds as $questionId) {
       $question = ConfigboxQuestion::getQuestion($questionId);
       $questionViewClass = 'ConfigboxViewQuestion_'.ucfirst($question->question_type);  // type → view
       $view = KenedoView::getView($questionViewClass);
       $view->questionId = $questionId;
       $view->prepareTemplateVars();
       $this->questionsHtml[$questionId] = $view->getViewOutput();                        // captured HTML
       $this->questions[$questionId]     = $view->question;
   }
   ```
   Each question's HTML string is captured to `$this->questionsHtml` keyed by question id.
6. The page template emits them: `echo implode('', $this->questionsHtml);` inside
   `<div class="configurator-page-questions">` (`views/configuratorpage/tmpl/default.php:35`).

### The client bootstrap blob

Alongside the questions, the page view serializes a JSON object and drops it into the DOM
(`views/configuratorpage/view.html.php:557-571`, emitted at `tmpl/default.php:77`):

```html
<div id="configurator-data" data-json="{…}"></div>
```

It carries `cartPositionId`, `productId`, `pageId`, `pageSequence`, `questions`, `dateFormat`,
`blockNavigationOnMissing`, `changeUrlsOnNav`, `missingPageSelections`, `missingProductSelections`.
The frontend reads it via `configurator.getConfiguratorData(key)` (`configurator.js:1398`) and keeps
it current with `setConfiguratorDataItem` (`:1424`). This is the client's picture of the
configuration; `pricing` gets added to it after the first `makeSelection`.

### How the JS gets loaded (the view-asset engine)

The page view declares its init calls (`views/configuratorpage/view.html.php:239-254`):

```php
function getJsInitCallsOnce() { $calls[] = 'configbox/configurator::initConfiguratorPage'; … }
function getJsInitCallsEach() { $calls[] = 'configbox/configurator::initConfiguratorPageEach'; … }
```

These `moduleId::method` strings feed the view-asset engine in `assets/main.js:159-302`, which
`cbrequire`s the module and calls `module.method(view)` — once per page for `…Once`, on every XHR
injection for `…Each`. (This is the same engine documented in
`customization/com_configbox_assets_and_amd.md` §4.) So `initConfiguratorPage` runs once and attaches
all the document-level handlers; `initConfiguratorPageEach` runs on every (re)paint and wires up the
questions currently on the page.

---

## 2. The question and answer objects

### `ConfigboxQuestion` (`classes/ConfigboxQuestion.php`)

`ConfigboxQuestion::getQuestion($id)` (`:99`) is a factory: it resolves a per-type subclass
`ConfigboxQuestion<Type>` from `classes/question_types/` (or the customization dir), falling back to
the base class, and returns a **clone** of a cached instance. Constructor runs `loadData()` +
`loadAnswers()`.

Fields the templates and engine lean on: `question_type`, `title`, `title_display`
(`heading`|`label`|other), `description` + `desc_display_method`, `required`, `rules`,
`default_value`/`prefill_on_init`, `unit`, `input_restriction`, `minval`/`maxval` and their
`set_min_value`/`set_max_value` modes (`none`|`static`|`calculated`), `slider_steps`, `choices`,
`display_while_disabled` (`hide` vs grey-out), `el_image`, `element_css_classes`, and
`answers` (`ConfigboxAnswer[]`).

Key methods:
- **`applies()`** → `ConfigboxRulesHelper::ruleIsFollowed($this->rules, 'question', $this->id)`
  (`:651`). *This is the question's connection to the rule engine.* Empty rules ⇒ `true`. (The
  code literally passes `' question'` with a stray leading space — harmless, since the record-type
  string is only used to label error logging, but don't grep for an exact `'question'` match.)
- **`getOutputValue($selection = null)`** (`:313`) — the human-readable label for a raw selection
  (see §2.3).
- **`getSku($selection)`** — the SKU a selection stands for. Base implementation: the picked answer's
  `sku` column, or `NULL` (no selection, no matching answer, empty column). Unlike `getOutputValue()`
  the argument is required — `NULL` means "nothing selected", not "look it up". Its one core caller is
  `ConfigboxModelOrderRecord::createOrderRecord()`, which freezes `(string) $question->getSku(...)`
  into `#__cbcheckout_order_configurations.option_sku`; a custom type overrides it to put a derived
  part number on an order line (`customization/com_configbox_custom_question_types.md` §3).
- **`getMinimumValue()`/`getMaximumValue()`** (`:473`) — for `calculated` mode call
  `ConfigboxCalculation::calculate(...)`.
- **`isValidValue($value)`** (`:398`) — upload extension/MIME/size, else min/max range. Returns
  `true` or an error message string.
- **`getInitialValue()`** (`:366`) — default answer or `default_value` prefill.
- **`onBeforeSetSelection()`** (`:164`) — upload handling (moves the file, rewrites the selection JSON).

### `ConfigboxAnswer` (`classes/ConfigboxAnswer.php`)

Constructed per option from a data row. Fields: `id`, `title`, `description`+`desc_display_method`,
`price`/`price_recurring`/`was_price`/`was_price_recurring`, `sku`, `rules`, `default`, `available`,
`availibility_date`, `disable_non_available`, `option_image`(+`_href`), `option_picker_image`(+`_href`),
`display_while_disabled`. Its rules hook mirrors the question's:
**`applies()`** → `ConfigboxRulesHelper::ruleIsFollowed($this->rules, 'option_assignment', $this->id)`
(`ConfigboxAnswer.php:114`).

The base question view builds a **`ConfigboxAnswerPresentation`** per answer holding the runtime
fields the templates use: `isSelected`, `disableControl`, `cssId`, the css classes, `pickerImageSrc`,
`optionImageSrc`, the resolved prices and the availability text. The answer itself is not written
to — it keeps its own columns, so `$answer->price` is the price column and
`$answerPresentation->price` is what the customer pays. Templates iterate `$this->answers` (a view
property) and read the record through `$answerPresentation->answer`.

The QUESTION has no such object: what the view computes for it — `applies`, `disableControl`,
`minValue`/`maxValue`, the content-modified `description`, the image src and classes — are
properties of `ConfigboxViewQuestion` itself, alongside `selection`, `price` and `outputValue`.

Until 2026-08-07 the view wrote all of that onto the answer instead, over the record columns; see
`migration-to-cb4/breaking-changes-log.md` for the mapping if you maintain a template override.

### `selection` vs `getOutputValue()` — the two representations

Every question carries two values, both emitted on the wrapper (`view.html.php:328-339`):

| | What it is | Source |
|---|---|---|
| **`selection`** (`data-selection`) | the machine value the server stores | `ConfigboxConfiguration::getInstance()->getSelection($id)` |
| **`outputValue`** (`data-output-value`) | the human-readable label | `$question->getOutputValue()` |

What `selection` actually contains depends on the type:

| Type family | `selection` is… |
|---|---|
| checkbox / radiobuttons / dropdown / images | the chosen **answer id** (integer) |
| choices | the chosen **choice text** |
| textbox / textarea / slider / colorpicker / ralcolorpicker | the raw **text / number / colour string** |
| calendar | a normalized **datetime string** |
| upload | a **JSON string** `{name, path, url, …}` |

`getOutputValue()` turns that into a label — the answer's `title` for choice types, a formatted date
for calendar, the file name for upload, the number + `unit` for numeric inputs (`:313-361`).

### Where the selection lives — `ConfigboxConfiguration` (`classes/ConfigboxConfiguration.php`)

A **singleton per cart position**: `getInstance($positionId = NULL)` (`:18`); with no id it resolves
the current position via `ConfigboxModelCartposition::getId()`. Two backends: the **session** is the
live working store, the **DB** table `#__configbox_cart_position_configurations` is the persisted
copy. The constructor prefers session over DB (`:45-50`), filtering to questions still in the product.
Reads/writes:

- `getSelection($questionId)` (`:344`) — simulated selection if set, else the real one, else `null`.
- `getSelections($includeSim = true)` (`:320`) — merges simulated over real; **this is the map the
  rule engine evaluates against**.
- `setSelection($questionId, $value)` (`:285`) — fires `ConfigboxQuestion::onBeforeSetSelection()`
  (upload handling), sets/unsets the value (a `null` value is a removal), and **immediately persists
  to the session**. The DB copy is written later (`storeSelectionsInDb()`, e.g. when the position is
  finalized).
- `unsetSimSelections()` — drop the temporary "what-if" selections used during inconsistency checks.

---

## 3. The twelve question types and the markup contract

The per-type views (`views/question_<type>/view.html.php`) are almost all empty subclasses of
`ConfigboxViewQuestion` — e.g. `class ConfigboxViewQuestion_Checkbox extends ConfigboxViewQuestion {}`.
The markup lives in each `tmpl/default.php`. Two subclasses add data:
`ConfigboxViewQuestion_Calendar` injects a localized calendar-locale JSON into the wrapper's data
attributes, and `ConfigboxViewQuestion_Ralcolorpicker` loads the RAL palette and resolves the
selected colour/group.

**Every type shares the wrapper contract:**

```html
<div id="question-<id>"
     class="question type-<type> applying-question|non-applying-question …"
     data-question-id="<id>"
     data-question-type="<type>"
     data-selection="<machine value>"
     data-output-value="<label>">

  <!-- getViewOutput('question_edit_buttons')  -->
  <!-- getViewOutput('question_heading')       -->
  <div class="answers"> … <div class="validation-message-target"></div> … </div>
</div>
```

The registered type names (`questions.js:1758-1769`) are: `calendar`, `colorpicker`,
`ralcolorpicker`, `checkbox`, `choices`, `dropdown`, `images`, `radiobuttons`, `slider`, `textbox`,
`textarea`, `upload`.

### Choice types — `name="question-<id>"`, `value` = answer id

| Type | Control | Notes |
|---|---|---|
| **checkbox** (`question_checkbox`) | one `<input type="checkbox">` | Renders **only the first answer** (`$answer = reset($this->question->answers)` — `tmpl/default.php:4`); a boolean toggle bound to that answer's id. |
| **radiobuttons** (`question_radiobuttons`) | `<input type="radio">` per answer | Each in a `.radio` wrapper; loops all answers. |
| **dropdown** (`question_dropdown`) | radio inputs inside a custom dropdown | Same radio markup, wrapped in `.configbox-dropdown-trigger` + `.configbox-dropdown`. Not a native `<select>`. |
| **images** (`question_images`) | image buttons | radios, **or a checkbox when there is exactly one answer**. Picker image via `pickerImageSrc`. |

All four share the per-answer pricing (`.answer-price-display` with `.answer-price-<id>` etc.),
description (`desc_display_method` → tooltip popover or Bootstrap modal), and availability
(`.xref-available`) blocks. Answer wrappers are `#answer-<answerId>`, inputs `#answer-input-<answerId>`.

### Free-value and special types

| Type | Control | Field id / name | Value carried |
|---|---|---|---|
| **choices** (`question_choices`) | radios from the question's `choices` (newline-split), plus an optional free-text field when a choice is literally `custom` | `name="choice-<id>"`, class `.configbox-choice-field` (+ `.configbox-choice-custom-field`), `data-choice` | the **choice text** |
| **slider** (`question_slider`) | `<input type="range" class="form-range">` mirrored to a text box | `#cb-slider-<id>` + `.wrapper-input input` | a number; min/max/step from question settings |
| **textbox** (`question_textbox`) | `<input type="text" class="form-control">` | `#input-question-<id>` | free text/number, optional `unit` |
| **textarea** (`question_textarea`) | `<textarea>` | `#input-question-<id>` | free text |
| **calendar** (`question_calendar`) | readonly display + hidden value div, opened by `.trigger-show-calendar` | `#input-<id>` (with `data-selection`), `#input-display-<id>` | normalized date string |
| **colorpicker** (`question_colorpicker`) | Spectrum picker + `.color-picker-output` swatch | `.spectrum-input` / `.color-picker-input` | colour string (e.g. `#aabbcc`) |
| **ralcolorpicker** (`question_ralcolorpicker`) | RAL swatch + modal grid of `.trigger-pick-ral-color` | `.ral-color-input` (wrapper adds `data-selection-group-id`) | `"RAL <code>"` |
| **upload** (`question_upload`) | `.upload-drop-zone` + `<input type="file">` fallback | `.trigger-show-file-browser`, `.trigger-remove-file`, `.has-file` | JSON file descriptor |

> **Contract note.** Choice types post their value under a real input `name` (`question-<id>` /
> `choice-<id>`); the free-value types carry their value only in a field addressed by **CSS id** and
> ship it to the server via XHR (§5). So don't assume a question value is reachable by input `name` —
> the JS reads each type from its own selector.

### Shared sub-templates (`views/question/tmpl/`)

Pulled in with `$this->getViewOutput('<name>')`, which runs through the same 5-slot precedence chain
as the main template (§ next):

- **`question_heading.php`** — `<h2 class="question-title">` when `title_display == 'heading'`, plus
  the description icon/inline block per `desc_display_method` (1 = inline, 2 = popover, 3 = modal).
- **`question_decoration.php`** — the element image (`<img class="question-decoration …">`) when
  `el_image` is set.
- **`question_edit_buttons.php` / `answer_edit_buttons.php`** — quick-edit buttons when
  `ConfigboxPermissionHelper::canQuickEdit()`.
- **`validation_feedback.php`** — a static `<div class="validation-feedback">` placeholder; the live
  message goes into the inline `.validation-message-target` instead.
- **`loading_symbol.php`** — the spinner markup.
- **`question_pricing.php` / `answer_pricing.php`** — the question-level / answer-level price blocks.
- **`question_desc_modal.php`** — the Bootstrap modal `#question-description-<id>`.

### How type → view → template resolves

Three parallel resolutions all key off `question_type`:

1. **View class** — `'ConfigboxViewQuestion_'.ucfirst($type)` in the page loop
   (`configuratorpage/view.html.php:515`). `checkbox` → `ConfigboxViewQuestion_Checkbox`.
2. **View file** — `KenedoView::getView()` derives the view name from the class
   (`external/kenedo/classes/KenedoView.php:195`): `ConfigboxViewQuestion_Checkbox` → dir
   `views/question_checkbox/`, checking the customization dir first.
3. **Template** — the base view's overridden `renderView()` (`views/question/view.html.php:360-405`)
   tries **five** paths in order:
   1. Joomla template override (`…/templates/<yourtemplate>/html/com_configbox/question_<type>/`)
   2. `data/customization/templates/question_<type>/<template>.php`
   3. `data/customization/templates/question/<template>.php` (the base question dir)
   4. `views/question_<type>/tmpl/<template>.php` (the type's own template — normally wins)
   5. `views/question/tmpl/<template>.php` (base fallback)

That chain — and the customization slots (2) and (3) — is why you can override one type's markup, or
the shared heading/decoration for **all** types, without touching core (see
`customization/com_configbox_overriding_views_and_templates.md`). The model class resolves the same
way: `'ConfigboxQuestion'.ucfirst($type)` from `classes/question_types/`.

---

## 4. Client-side: how the JS wires itself to the markup

### 4.1 The two modules

- **`configbox/configurator`** (`configurator.js`, deps `cbj`, `configbox/server`, `cbj.bootstrap`) —
  the engine. Returns a singleton object of methods. Its two entry points are `initConfiguratorPage`
  (once: attach all document-delegated handlers and custom-event listeners, `:15`) and
  `initConfiguratorPageEach` (each paint: `initQuestions()`, image preloading, sticky block, popovers,
  modals, `:91`).
- **`configbox/questions`** (`questions.js`, deps `cbj`, `configbox/configurator`) — defines twelve
  per-type handler objects and, at the bottom, **registers** each with
  `configurator.registerQuestionType(...)` (`:1758-1769`). It returns nothing; it exists to register.

### 4.2 `initQuestions` — binding a type object to each question

`configurator.initQuestions()` (`configurator.js:653`) `cbrequire`s `configbox/questions` and,
**conditionally, `configbox/custom/custom_questions`** when `server.config.requireCustomQuestionJs`
is true. Then, for each `.question` on the page:

```js
var type = cbj(this).data('questionType');                 // from data-question-type
var questionType = configurator.getQuestionType(type);     // the registered object
if (!questionType) throw 'type "'+type+'" not registered — make/register it in custom_questions.js';
if (questionType.initEach) questionType.initEach();

cbj(this).on('cbQuestionActivation',      questionType.onQuestionActivation);
cbj(this).on('cbQuestionDeactivation',    questionType.onQuestionDeactivation);
cbj(this).on('cbAnswerActivation',        questionType.onAnswerActivation);
cbj(this).on('cbAnswerDeactivation',      questionType.onAnswerDeactivation);
cbj(this).on('cbSystemSelectionChange',   questionType.onSystemSelectionChange);
cbj(this).on('cbValidationChange',        questionType.onValidationChange);
cbj(this).on('cbValidationMessageShown',  questionType.onValidationMessageShown);
cbj(this).on('cbValidationMessageCleared',questionType.onValidationMessageCleared);

if (first time this type is seen) questionType.init();     // once per type per page
```

So a question of an **unknown type throws** — every type on a page must be registered. `init()` runs
once per type; `initEach()` runs for every question of that type on every paint. The eight `cb*`
custom events are the type object's callback interface (§7).

### 4.3 The DOM contract the JS relies on

- **Find a question:** `cbj('.question[data-question-id='+id+']')`
  (`configurator.getQuestionDiv`, `:1389`), or `#question-<id>`.
- **Type dispatch:** the `type-<name>` class (e.g. `.question.type-slider`) and `data-question-type`.
- **Selection state on the wrapper:** written by `updateSelection` (`:1155-1156`):
  `question.data('selection', selection); question.data('outputValue', outputValue);`.
- **Applying/greying:** `.applying-question` / `.non-applying-question` on the wrapper;
  `.applying-answer` / `.non-applying-answer` on `#answer-<id>`. `display_while_disabled` decides
  hide vs grey (`hide-non-applying` / `grey-out-non-applying`).
- **Selected option:** `.selected` on the `.answer`, and `#answer-input-<id>` `.prop('checked', …)`.
- **Validation:** `.has-error` on the `.form-group`; message HTML into `.validation-message-target`.
- **Global config:** `#configurator-data[data-json]` via `getConfiguratorData`/`setConfiguratorDataItem`.

### 4.4 Reacting to user input

Each type's handlers are attached in `init` (document-delegated) or `initEach` (per question), and
all converge on **`configurator.sendSelectionToServer(questionId, selection)`**. Highlights:

- **checkbox / radiobuttons / images / dropdown** — `change` on the input; toggle `.selected`; send
  the answer id (checkbox sends `val()` when checked, `''` when unchecked).
- **textbox / textarea** — `keyup` **debounced 400 ms** (`questions.js:540`, `637`).
- **slider** — range and text box mirror each other; store on `change`, or on the text input's
  `input` **debounced 700 ms** (`:1189-1213`); only sends if the value actually changed.
- **colorpicker** — Spectrum's `change` sends immediately; `move` sends **debounced 400 ms**.
- **calendar** — the datepicker's `onSelect` sends the picked date.
- **choices** — `change` on the radio sends the choice text; `keyup` on the custom field sends when
  it changes (and checks its radio).
- **upload** — drag/drop or browse builds a `FormData` and its **own** `XMLHttpRequest` with a
  progress bar; on completion it fires `serverResponseReceived` with the parsed JSON manually — it
  does **not** go through `sendSelectionToServer`. Remove-file sends `''`.

> **There is no batch.** Each change sends exactly one question's value. The client keeps no list of
> all selections to submit — the server is the source of truth.

### 4.5 Concurrency

A single coarse flag, `configurator.requestInProgress`, is set on `serverRequestSent` and cleared on
`serverResponseReceived` (`:69-75`). `configurator.queueRequest(fn)` (`:323`) runs `fn` now, or defers
it to the next `serverResponseReceived` if a request is in flight — used for **add-to-cart** and
**page navigation**, so those wait for pending selection saves. Ordinary selection sends are **not**
serialized, aborted, or sequence-numbered; they're only rate-limited by the per-type debounce. A slow
response for an older change can therefore land after a newer one — worth knowing when writing a
custom type that stores rapidly-changing values.

### 4.6 What *does* fetch HTML

Two flows fetch server-rendered markup rather than instructions:
- **Initial paint** — the configurator page is normal server-rendered HTML (§1).
- **Page navigation** — `switchPage` (`configurator.js:367`) requests the next page's HTML
  (`configuratorpage / getPageHtml`, controller `:48`) and injects it, firing `cbViewInjected` so the
  view-asset engine re-runs `initConfiguratorPageEach` and re-binds the new questions.

The per-selection `makeSelection` round-trip (next) carries **no HTML**.

---

## 5. The round-trip, client → server

### The request

`configurator.sendSelectionToServer(questionId, selection, confirmed)` (`configurator.js:959`):

```js
cbj(document).trigger('serverRequestSent');
// optimistic visualization update …
server.makeRequest('configuratorpage', 'makeSelection', {
    languageTag:      server.config.languageTag,
    questionId:       questionId,
    selection:        selection,
    confirmed:        confirmed ? '1' : '0',
    cart_position_id: configurator.getCartPositionId(),
    productId:        configurator.getProductId(),
    pageId:           configurator.getPageId()
}).done(function(response) {
    cbj(document).trigger('serverResponseReceived', [response]);
});
```

`server.makeRequest(controller, task, data)` (`server.js:93`) POSTs to the generic API endpoint —
`server.endpointUrl(controller, task)`, which yields `/{lang}/cb-api/configuratorpage/makeSelection.html`
on a SEF Joomla site (and falls back to `server.config.urlXhr` when no endpoint template is present,
e.g. SEF off) — with `option=com_configbox&controller=configuratorpage&task=makeSelection&output_mode=view_only&lang=…`
still sent in the body, `dataType: 'json'`. If any value is a `File`/`FileList` it switches to
`FormData` automatically (that's how upload posts its file). It returns the jqXHR promise. The endpoint
mechanism is documented in `platform/joomla/com_configbox_sef_urls.md` §6.

### The server task

`ConfigboxControllerConfiguratorpage::makeSelection()` (`controllers/configuratorpage.php:101`) reads
`questionId`, `selection`, `pageId`, `confirmed`, `languageTag`, then delegates the whole computation
to a helper and JSON-encodes the result:

```php
$response = ConfigboxConfiguratorHelper::getMakeSelectionResponse($questionId, $selection, $pageId, $gotConfirmation);
$this->sendResponse($response);   // → json_encode, application/json
```

`sendResponse()` (`:129`) also calls a global **`postMakeSelection($response)`** if one is defined —
a customization hook to enrich the response before it goes out (`:132`).

### What the server does — `getMakeSelectionResponse` (`configurator.php`, encoded)

This is the entire server-side sequence. It builds an associative array `$response`:

1. **Set up** — cart-position model, `ConfigboxConfiguration::getInstance()`, validate
   `cart_position_id` and `questionExists($questionId)`; on failure set `$response['error']` and return.
2. **Record** `requestedChange` (`{questionId, selection, outputValue}`) and `originalValue` (the
   pre-change selection, for reverting).
3. **Guard** (only if selection is non-empty): `$question->applies()` (rules — else
   `error: 'Question does not apply'`) and `$question->isValidValue($selection)` (validation — else
   the error message).
4. **Inconsistencies** — `ConfigboxRulesHelper::getInconsistencies(...)` simulates the change against
   the rules. If any require confirmation and the client hasn't confirmed, return `inconsistencies`
   + a `confirmationText` and stop (the two-phase confirm, see §6).
5. **Resolve inconsistencies** — for each, either `setSelection(...)` a replacement (→
   `configurationChanges.add`) or clear it (→ `configurationChanges.remove`).
6. **Persist the requested change** — `$configuration->setSelection($questionId, $selection)`,
   `unsetSimSelections()`. (Upload questions get their full file info folded into `requestedChange`
   here.)
7. **Auto-selects / prefills** — `ConfigboxRulesHelper::getAutoSelectItems(...)` → more
   `configurationChanges.add`.
8. **Validation values** — `getDynamicValidationValues($pageId)` → `validationValues` (recomputed
   min/max for calculated bounds).
9. **Item visibility** — `getPageItemVisibility($pageId)` → `itemVisibility` (which questions/answers
   now show/hide).
10. **Missing selections** — `getMissingSelections(...)` for page and product →
    `missingPageSelections` / `missingProductSelections`.
11. **Pricing** — if `ConfigboxPermissionHelper::canSeePricing()`, `getPricing()` → `pricing`.

Note the ordering: **persist first, then recompute rules → auto-selects → validation → visibility →
pricing** against the new configuration. The rule and calculation engines are ionCube-encoded, but
`getMakeSelectionResponse` itself is part of that engine — `helpers/init.php` loads the plaintext copy
on this dev machine (path-based bypass) and the `helpers/encoded/{13,14,15}/` copy in production; the
public API is identical, so the sequence above is authoritative either way. (See
`technical/com_configbox_rule_engine.md` and `com_configbox_calculation_engine.md`.)

---

## 6. The round-trip, server → client

The `.done` handler fires `serverResponseReceived`, handled by
**`configurator.processServerResponse(event, data)`** (`configurator.js:1008`). The response is a set
of **instructions**, applied in this order:

| Response key | Client action |
|---|---|
| `error` | `showValidationError(requestedChange.questionId, error)` and stop |
| `confirmationText` (+ `originalValue`) | `window.confirm(...)`; if OK, re-send with `confirmed=true`; if cancelled, `updateSelection(...originalValue..., 'system')` to revert |
| `requestedChange` | `updateSelection(questionId, selection, outputValue, 'user')` |
| `validationValues` | `processValidationUpdate(...)` → per-question `cbValidationChange` |
| `itemVisibility` (`{questions:{qid:bool}, answers:{qid:{aid:bool}}}`) | `processItemVisibility(...)` → `cbQuestionActivation`/`Deactivation`, `cbAnswerActivation`/`Deactivation` |
| `configurationChanges` (`add`/`remove`) | `processAutomaticSelections(...)` → `updateSelection(..., 'system')` |
| `pricing` | trigger `cbPricingChange` (also carries each question's `outputValue`/`showInOverview`, refreshing the selections overview) |
| `missingPage/ProductSelections` | store, then fire `cbRequiredPage/ProductSelectionsMissing/Made` |

A real captured response (picking a `radiobuttons` answer, live) — note the top-level `cart_position_id`
and the trailing `htmlConfiguratorControls` a `postMakeSelection` customization added:

```
cart_position_id, requestedChange {questionId, selection, outputValue:"Pendant"},
originalValue, configurationChanges {add, remove}, validationValues,
itemVisibility {questions, answers}, missingProductSelections [], missingPageSelections [],
pricing, htmlConfiguratorControls        ← custom key (§7.4)
```

`updateSelection(questionId, selection, outputValue, source)` (`:1142`) writes the new value onto the
wrapper's `data-selection`/`data-outputValue`, and fires `cbSystemSelectionChange` (when
`source === 'system'`) and always `cbSelectionChange` (`:1160-1178`). The type object's
`onSystemSelectionChange` handler is what reflects a **server-driven** change (an auto-select or an
inconsistency fix) back into that type's control — because the user didn't touch it, so nothing else
would.

`processItemVisibility` compares the response against the cached `questions` blob and only fires
activation/deactivation events for questions/answers whose applying-state actually **changed**; the
engine's own handlers (`:424-455`) toggle the `applying-*`/`non-applying-*` classes, and each type's
handlers enable/disable their inputs. `cbPricingChange` drives the running total, per-answer price
spans, and the selections overview.

**The key asymmetry:** the browser never receives new question HTML from a selection. It receives a
description of *what changed* and mutates the DOM it already has. A custom question type is therefore
responsible for reflecting these instructions into its own markup, through the callback interface
below.

---

## 7. Customization seams

This is where `custom_questions.js` and custom question types plug in. The auto-loaded entry point
is `data/customization/assets/javascript/custom_questions.js` (module id
`configbox/custom/custom_questions`), pulled in by `initQuestions` when it exists (§4.2; the
auto-detection is in `customization/com_configbox_assets_and_amd.md` §2).

### 7.1 Registering a question type

```js
define(['cbj', 'configbox/configurator', 'configbox/server'], function (cbj, configurator, server) {
    "use strict";

    var questionMytype = {
        init:                     function () { /* once per page: document-delegated handlers */ },
        initEach:                 function () { /* optional: per question, every paint (idempotent!) */ },
        onSystemSelectionChange:  function (event, questionId, selection) { /* reflect a server-driven change */ },
        onQuestionActivation:     function (event, questionId) { /* question now applies */ },
        onQuestionDeactivation:   function (event, questionId) { /* question no longer applies */ },
        onAnswerActivation:       function (event, questionId, answerId) {},
        onAnswerDeactivation:     function (event, questionId, answerId) {},
        onValidationChange:       function (event, questionId, minMax) { /* new min/max */ },
        onValidationMessageShown: function (event, questionId, message) {},
        onValidationMessageCleared: function (event, questionId) {}
    };

    configurator.registerQuestionType('mytype', questionMytype);
});
```

`registerQuestionType(type, obj)` (`configurator.js:596`) **requires all of** `init`,
`onQuestionActivation`, `onQuestionDeactivation`, `onAnswerActivation`, `onAnswerDeactivation`,
`onSystemSelectionChange`, `onValidationChange`, `onValidationMessageShown`,
`onValidationMessageCleared` — it **throws** if any is missing, listing them, even if your type
doesn't need them (stub them empty). `initEach` is optional. `registerQuestion` is a **deprecated
alias** (`:585`). The `type` string must match the question's `question_type` /
`data-question-type` — a page with an unregistered type throws in `initQuestions` (§4.2).

The stub the product ships at `data/_customization/assets/javascript/custom_questions.js` registers a
`headingonly` type (all-empty handlers) and a full `lof` (length-of-something) type — read it as a
worked template. Its `lof` type shows the whole pattern: bind inputs in `initEach`, POST to a custom
controller task via `server.makeRequest('bcconfigurator', 'storeLofValues', data)`, and then call
`configurator.sendSelectionToServer(questionId, JSON.stringify(response.values))` so the **standard**
rule/pricing round-trip still runs for the stored value.

### 7.2 The client API a custom type can call

| Function | Purpose |
|---|---|
| `configurator.sendSelectionToServer(questionId, value)` | run the standard `makeSelection` round-trip for a value |
| `configurator.getCartPositionId()` / `getProductId()` / `getPageId()` | current context ids |
| `configurator.getCurrentSelection(questionId)` | the question's stored value |
| `configurator.getQuestionDiv(questionId)` | the `.question` wrapper |
| `configurator.getQuestionPropValue(questionId, prop)` / `questionHasProperty(...)` | read a question setting from the config blob |
| `configurator.getConfiguratorData(key)` / `setConfiguratorDataItem(key, val)` | read/update the `#configurator-data` blob |
| `server.makeRequest(controller, task, data)` | the XHR primitive → jqXHR promise (auto-`FormData` for files); posts to the `/cb-api/…` endpoint, see `platform/joomla/com_configbox_sef_urls.md` §6 |

### 7.3 The custom events (all triggered on `document` unless noted)

Beyond the eight per-type callbacks (which are these same events, bound to the wrapper), a
customization can `.on(...)` any of these globally:

- **Round-trip:** `serverRequestSent`, `serverResponseReceived` (`configurator.js:961`, `985`),
  `cbViewInjected` (fired after XHR HTML injection).
- **Selection:** `cbSelectionChange` (any change), `cbSystemSelectionChange` (server-driven only)
  (`:1168`, `:1178`).
- **Visibility:** `cbQuestionActivation`, `cbQuestionDeactivation`, `cbAnswerActivation`,
  `cbAnswerDeactivation` (`:1224-1258`).
- **Validation:** `cbValidationChange` (`:1193`), and — which a custom type can also **trigger** to
  drive its own message UI — `cbValidationMessageShown` / `cbValidationMessageCleared`
  (`:1110`, `:1123`).
- **Pricing:** `cbPricingChange` (`:1060`).
- **Required-selection gates:** `cbRequiredPageSelectionsMissing`/`Made`,
  `cbRequiredProductSelectionsMissing`/`Made` (`:1071-1093`).
- **Page nav:** `cbPageSwitchStart`, `cbPageSwitchEnd` (`:373`, `:381`).

### 7.4 Server-side seams

- **`postMakeSelection(&$response)` — extend the response.** A global function (define it in a
  `system_overrides/` file); `sendResponse()` calls it with the response **by reference** so you can
  add keys to the `makeSelection` JSON (`controllers/configuratorpage.php:132`). The keys can be
  anything, **including a server-rendered HTML fragment** — pair it with a `serverResponseReceived`
  listener that reads your key and injects it. This is the canonical way to push extra state through
  the per-selection round-trip. Real-world example (Beta-Calco):

  ```php
  // system_overrides/postMakeSelection.php — runs after every makeSelection
  function postMakeSelection(&$response) {
      ob_start();
      KenedoView::getView('ConfigboxViewBcconfiguratorcontrols')->display();
      $response['htmlConfiguratorControls'] = ob_get_clean();   // a custom key
  }
  ```
  ```js
  // custom JS — consume the custom key on every response
  cbj(document).on('serverResponseReceived', function (e, response) {
      if (response.htmlConfiguratorControls) {
          cbj('.sticky-wrapper-controls').replaceWith(response.htmlConfiguratorControls);
      }
  });
  ```
  So while the *built-in* keys are instructions rather than markup (§6), a customization is free to add
  markup — the "no HTML" rule is about the core payload, not a hard limit.
- **A custom controller task** — as the `lof` example does, POST to your own
  `controller/task` via `server.makeRequest`, do type-specific work, then feed the result back into
  the standard flow with `sendSelectionToServer` (so rules/pricing still re-run for the stored value).
  New controllers go in `data/customization/controllers/` (see
  `customization/com_configbox_overriding_controllers_and_models.md`).
- **Custom template / view class** for the type's markup — a `question_<type>` template override or a
  `ConfigboxViewQuestion_<Type>` view (`customization/com_configbox_overriding_views_and_templates.md`).
- **A custom question model** — `ConfigboxQuestion<Type>` in `classes/question_types/` (or the
  customization dir) to control `applies()`, `isValidValue()`, `getOutputValue()`, `getSku()` (the
  SKU frozen onto the order line), storage normalization, etc.

> **`initEach` must be idempotent.** A question can be (re)initialized on every paint and every XHR
> view injection. Scope your work to the current question, and guard already-wired nodes with a marker
> class — the same rule as the view-asset engine's `…Each` init calls.

---

## 8. Gotchas & things to know

- **The response has no *question* HTML (by default).** Per-selection updates are JSON instructions;
  the client mutates the DOM. Don't expect `makeSelection` to return re-rendered questions — but note a
  `postMakeSelection` customization *can* add HTML (§7.4).
- **Selection sends aren't serialized.** Only debounced. A slow response for an older change can
  overwrite a newer one. Keep custom-type stores lightweight, or gate rapid changes yourself.
- **The `#configurator-data` blob and the response are both extensible.** Integrators add their own
  keys — a view override can add to the initial blob (Beta-Calco ships `isLoggedIn`,
  `originalProductId`, analytics ids, …), and `postMakeSelection` adds to each response. Read the blob
  with `getConfiguratorData(key)`; read response keys in a `serverResponseReceived` handler.
- **Custom question modules compose with core, they don't replace it.** A customization's configurator
  JS is a *separate* AMD module that **depends on** `configbox/configurator` (e.g. Beta-Calco's
  `configbox/custom/configurator`), so the core `processServerResponse`/`updateSelection` still run and
  the custom module layers extra behavior on top — it does not shadow the core module.
- **`checkbox` renders one answer; `images` flips to a checkbox for one answer.** Type behavior isn't
  a pure function of the control — it depends on the answer count too.
- **Value addressing varies by type.** Choice types have a real input `name` (`question-<id>` /
  `choice-<id>`); free-value types carry their value only in a CSS-id-addressed field. Read each type
  by its own selector.
- **Upload is the odd one out.** It bypasses `sendSelectionToServer`, builds its own `FormData` XHR,
  and triggers `serverResponseReceived` by hand. Its payload keys are maintained inline — keep them in
  sync with the standard path if you touch it.
- **Every type on a page must be registered**, and a registered object must implement all nine
  required methods or registration throws. Stub the ones you don't need.
- **The `slider` template includes a `descriptions` sub-template** (`question_slider/tmpl/default.php:14`,
  file `question_slider/tmpl/descriptions.php`) — a type-specific sub-template that has no base
  counterpart. Custom slider overrides should account for it.

---

## See also

- `customization/com_configbox_assets_and_amd.md` — the AMD loader, the `custom_questions.js`
  auto-entry-point, and the view-asset engine (`initConfiguratorPage…` init calls).
- `customization/com_configbox_overriding_views_and_templates.md` — overriding a `question_<type>`
  template or the shared `question/` sub-templates (the 5-slot precedence chain).
- `customization/com_configbox_custom_properties.md` — the admin-form side of question/answer fields
  (Kenedo properties), distinct from the frontend question *types* here.
- `technical/com_configbox_rule_engine.md` — `applies()` / `getInconsistencies()` / `getAutoSelectItems()`
  internals (the encoded rules engine).
- `technical/com_configbox_calculation_engine.md` — `getDynamicValidationValues()` and calculated
  min/max (the encoded calc engine).
- `functional/com_configbox_configurator_building.md` — the admin view of building
  Product → Pages → Questions → Options.
- `assets/debug/JsonResponses.min.js` — the JSDoc reference for the `makeSelection` response shape
  (`JsonResponses.configuratorUpdates`).
- Key source: `assets/javascript/configurator.js`, `assets/javascript/questions.js`,
  `assets/javascript/server.js`, `views/configuratorpage/view.html.php`,
  `views/question/view.html.php`, `views/question_*/tmpl/default.php`,
  `controllers/configuratorpage.php`, `helpers/encoded/<v>/configurator.php` (encoded — see §5),
  `classes/ConfigboxQuestion.php`,
  `classes/ConfigboxAnswer.php`, `classes/ConfigboxConfiguration.php`,
  `models/cartposition.php` (`getPricing`, `getPageItemVisibility`, `getDynamicValidationValues`,
  `getMissingSelections`).
