Skip to main content
Version: 3.x

Configurator Questions & Answers, in Detail

Version
3.x
Updated
View markdown

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):
    $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:
    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):

<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):

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 cbrequires 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 isSource
selection (data-selection)the machine value the server storesConfigboxConfiguration::getInstance()->getSelection($id)
outputValue (data-output-value)the human-readable label$question->getOutputValue()

What selection actually contains depends on the type:

Type familyselection is…
checkbox / radiobuttons / dropdown / imagesthe chosen answer id (integer)
choicesthe chosen choice text
textbox / textarea / slider / colorpicker / ralcolorpickerthe raw text / number / colour string
calendara normalized datetime string
uploada 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:

<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

TypeControlNotes
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 answerEach in a .radio wrapper; loops all answers.
dropdown (question_dropdown)radio inputs inside a custom dropdownSame radio markup, wrapped in .configbox-dropdown-trigger + .configbox-dropdown. Not a native <select>.
images (question_images)image buttonsradios, 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

TypeControlField id / nameValue carried
choices (question_choices)radios from the question's choices (newline-split), plus an optional free-text field when a choice is literally customname="choice-<id>", class .configbox-choice-field (+ .configbox-choice-custom-field), data-choicethe choice text
slider (question_slider)<input type="range" class="form-range"> mirrored to a text box#cb-slider-<id> + .wrapper-input inputa 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-inputcolour 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-fileJSON 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). checkboxConfigboxViewQuestion_Checkbox.
  2. View fileKenedoView::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) cbrequires configbox/questions and, conditionally, configbox/custom/custom_questions when server.config.requireCustomQuestionJs is true. Then, for each .question on the page:

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 / dropdownchange on the input; toggle .selected; send the answer id (checkbox sends val() when checked, '' when unchecked).
  • textbox / textareakeyup 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.
  • choiceschange 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 navigationswitchPage (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):

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:

$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. InconsistenciesConfigboxRulesHelper::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 / prefillsConfigboxRulesHelper::getAutoSelectItems(...) → more configurationChanges.add.
  8. Validation valuesgetDynamicValidationValues($pageId)validationValues (recomputed min/max for calculated bounds).
  9. Item visibilitygetPageItemVisibility($pageId)itemVisibility (which questions/answers now show/hide).
  10. Missing selectionsgetMissingSelections(...) 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 keyClient action
errorshowValidationError(requestedChange.questionId, error) and stop
confirmationText (+ originalValue)window.confirm(...); if OK, re-send with confirmed=true; if cancelled, updateSelection(...originalValue..., 'system') to revert
requestedChangeupdateSelection(questionId, selection, outputValue, 'user')
validationValuesprocessValidationUpdate(...) → per-question cbValidationChange
itemVisibility ({questions:{qid:bool}, answers:{qid:{aid:bool}}})processItemVisibility(...)cbQuestionActivation/Deactivation, cbAnswerActivation/Deactivation
configurationChanges (add/remove)processAutomaticSelections(...)updateSelection(..., 'system')
pricingtrigger cbPricingChange (also carries each question's outputValue/showInOverview, refreshing the selections overview)
missingPage/ProductSelectionsstore, 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

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

FunctionPurpose
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):

    // 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
    }
    // 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 modelConfigboxQuestion<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.mdapplies() / getInconsistencies() / getAutoSelectItems() internals (the encoded rules engine).
  • technical/com_configbox_calculation_engine.mdgetDynamicValidationValues() 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).