Configurator Questions & Answers, in Detail
- Version
- 3.x
- Updated
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:
-
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. -
The DOM is the state — sort of. Each question wrapper carries its current value in
data-selection/data-output-valueand its type indata-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. -
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. -
The response is instructions, not (question) HTML.
makeSelectionreturns 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 viapostMakeSelection— 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
makeSelectionrequest payload (§5), and the full response schema (§6) in this document were confirmed by driving a real, heavily-customized production configurator (Beta-Calco) — including thepostMakeSelectionseam (§7.4), the deprecated-registerQuestionalias, 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):
- page → product via the assignments helper:
ConfigboxAssignmentsHelper::getProductIdForPage($this->pageId)(:342). - Load the product and page models; abort if either is unpublished.
- product → pages —
$pageModel->getPages()for tab/button nav andpageSequence. - page → questions — again from the assignments helper (
:503):The list order is the question ordering. The helper's per-request memo uses the canonical CB4 key$questionIds = ConfigboxAssignmentsHelper::getPageQuestionIds($this->pageId);page_to_question(helpers/assignments.php:204-214); the legacy whole-catalogConfigboxCacheHelper::getAssignments()blob still exists for customizations, with the oldpage_to_elementname kept only as a deprecated alias — its own comment says to move custom code to thequestion_*keys (helpers/cache.php:607-618). New code uses the helper methods, not the blob. - The core render loop (
:513-523) — one view per question, keyed by type:Each question's HTML string is captured toforeach ($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;}$this->questionsHtmlkeyed by question id. - 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'sskucolumn, orNULL(no selection, no matching answer, empty column). UnlikegetOutputValue()the argument is required —NULLmeans "nothing selected", not "look it up". Its one core caller isConfigboxModelOrderRecord::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) — forcalculatedmode callConfigboxCalculation::calculate(...).isValidValue($value)(:398) — upload extension/MIME/size, else min/max range. Returnstrueor an error message string.getInitialValue()(:366) — default answer ordefault_valueprefill.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, elsenull.getSelections($includeSim = true)(:320) — merges simulated over real; this is the map the rule engine evaluates against.setSelection($questionId, $value)(:285) — firesConfigboxQuestion::onBeforeSetSelection()(upload handling), sets/unsets the value (anullvalue 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
| 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 inputname— 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">whentitle_display == 'heading', plus the description icon/inline block perdesc_display_method(1 = inline, 2 = popover, 3 = modal).question_decoration.php— the element image (<img class="question-decoration …">) whenel_imageis set.question_edit_buttons.php/answer_edit_buttons.php— quick-edit buttons whenConfigboxPermissionHelper::canQuickEdit().validation_feedback.php— a static<div class="validation-feedback">placeholder; the live message goes into the inline.validation-message-targetinstead.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:
- View class —
'ConfigboxViewQuestion_'.ucfirst($type)in the page loop (configuratorpage/view.html.php:515).checkbox→ConfigboxViewQuestion_Checkbox. - View file —
KenedoView::getView()derives the view name from the class (external/kenedo/classes/KenedoView.php:195):ConfigboxViewQuestion_Checkbox→ dirviews/question_checkbox/, checking the customization dir first. - Template — the base view's overridden
renderView()(views/question/view.html.php:360-405) tries five paths in order:- Joomla template override (
…/templates/<yourtemplate>/html/com_configbox/question_<type>/) data/customization/templates/question_<type>/<template>.phpdata/customization/templates/question/<template>.php(the base question dir)views/question_<type>/tmpl/<template>.php(the type's own template — normally wins)views/question/tmpl/<template>.php(base fallback)
- Joomla template override (
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, depscbj,configbox/server,cbj.bootstrap) — the engine. Returns a singleton object of methods. Its two entry points areinitConfiguratorPage(once: attach all document-delegated handlers and custom-event listeners,:15) andinitConfiguratorPageEach(each paint:initQuestions(), image preloading, sticky block, popovers, modals,:91).configbox/questions(questions.js, depscbj,configbox/configurator) — defines twelve per-type handler objects and, at the bottom, registers each withconfigurator.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) anddata-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-questionon the wrapper;.applying-answer/.non-applying-answeron#answer-<id>.display_while_disableddecides hide vs grey (hide-non-applying/grey-out-non-applying). - Selected option:
.selectedon the.answer, and#answer-input-<id>.prop('checked', …). - Validation:
.has-erroron the.form-group; message HTML into.validation-message-target. - Global config:
#configurator-data[data-json]viagetConfiguratorData/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 —
changeon the input; toggle.selected; send the answer id (checkbox sendsval()when checked,''when unchecked). - textbox / textarea —
keyupdebounced 400 ms (questions.js:540,637). - slider — range and text box mirror each other; store on
change, or on the text input'sinputdebounced 700 ms (:1189-1213); only sends if the value actually changed. - colorpicker — Spectrum's
changesends immediately;movesends debounced 400 ms. - calendar — the datepicker's
onSelectsends the picked date. - choices —
changeon the radio sends the choice text;keyupon the custom field sends when it changes (and checks its radio). - upload — drag/drop or browse builds a
FormDataand its ownXMLHttpRequestwith a progress bar; on completion it firesserverResponseReceivedwith the parsed JSON manually — it does not go throughsendSelectionToServer. 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, firingcbViewInjectedso the view-asset engine re-runsinitConfiguratorPageEachand 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:
- Set up — cart-position model,
ConfigboxConfiguration::getInstance(), validatecart_position_idandquestionExists($questionId); on failure set$response['error']and return. - Record
requestedChange({questionId, selection, outputValue}) andoriginalValue(the pre-change selection, for reverting). - Guard (only if selection is non-empty):
$question->applies()(rules — elseerror: 'Question does not apply') and$question->isValidValue($selection)(validation — else the error message). - Inconsistencies —
ConfigboxRulesHelper::getInconsistencies(...)simulates the change against the rules. If any require confirmation and the client hasn't confirmed, returninconsistencies- a
confirmationTextand stop (the two-phase confirm, see §6).
- a
- Resolve inconsistencies — for each, either
setSelection(...)a replacement (→configurationChanges.add) or clear it (→configurationChanges.remove). - Persist the requested change —
$configuration->setSelection($questionId, $selection),unsetSimSelections(). (Upload questions get their full file info folded intorequestedChangehere.) - Auto-selects / prefills —
ConfigboxRulesHelper::getAutoSelectItems(...)→ moreconfigurationChanges.add. - Validation values —
getDynamicValidationValues($pageId)→validationValues(recomputed min/max for calculated bounds). - Item visibility —
getPageItemVisibility($pageId)→itemVisibility(which questions/answers now show/hide). - Missing selections —
getMissingSelections(...)for page and product →missingPageSelections/missingProductSelections. - 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
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 asystem_overrides/file);sendResponse()calls it with the response by reference so you can add keys to themakeSelectionJSON (controllers/configuratorpage.php:132). The keys can be anything, including a server-rendered HTML fragment — pair it with aserverResponseReceivedlistener 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 makeSelectionfunction postMakeSelection(&$response) {ob_start();KenedoView::getView('ConfigboxViewBcconfiguratorcontrols')->display();$response['htmlConfiguratorControls'] = ob_get_clean(); // a custom key}// custom JS — consume the custom key on every responsecbj(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
lofexample does, POST to your owncontroller/taskviaserver.makeRequest, do type-specific work, then feed the result back into the standard flow withsendSelectionToServer(so rules/pricing still re-run for the stored value). New controllers go indata/customization/controllers/(seecustomization/com_configbox_overriding_controllers_and_models.md). -
Custom template / view class for the type's markup — a
question_<type>template override or aConfigboxViewQuestion_<Type>view (customization/com_configbox_overriding_views_and_templates.md). -
A custom question model —
ConfigboxQuestion<Type>inclasses/question_types/(or the customization dir) to controlapplies(),isValidValue(),getOutputValue(),getSku()(the SKU frozen onto the order line), storage normalization, etc.
initEachmust 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…Eachinit 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
makeSelectionto return re-rendered questions — but note apostMakeSelectioncustomization 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-datablob and the response are both extensible. Integrators add their own keys — a view override can add to the initial blob (Beta-Calco shipsisLoggedIn,originalProductId, analytics ids, …), andpostMakeSelectionadds to each response. Read the blob withgetConfiguratorData(key); read response keys in aserverResponseReceivedhandler. - 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'sconfigbox/custom/configurator), so the coreprocessServerResponse/updateSelectionstill run and the custom module layers extra behavior on top — it does not shadow the core module. checkboxrenders one answer;imagesflips 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 ownFormDataXHR, and triggersserverResponseReceivedby 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
slidertemplate includes adescriptionssub-template (question_slider/tmpl/default.php:14, filequestion_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, thecustom_questions.jsauto-entry-point, and the view-asset engine (initConfiguratorPage…init calls).customization/com_configbox_overriding_views_and_templates.md— overriding aquestion_<type>template or the sharedquestion/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 themakeSelectionresponse 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).