<bmx-configurator>
Configure, price, quote: a product's choices - options, extras, quantities, yes-or-no, text - with rules that keep every combination valid, and the price, the bill of materials and the quote worked out as the buyer goes.
15 properties · 2 events · 12 methods · 6 parts
Example
Choose a 1U chassis after picking a graphics card to see what the rules do. Seller mode shows the discount, approval and the bill of materials.
Show markup
<bmx-configurator id="ex-cf" mode="seller" submit-label="Add to quote" lang="en-GB"></bmx-configurator>
<p class="note" id="ex-cf-out" role="status" style="margin: 12px 0 0">Choose a 1U chassis after picking a graphics card to see what the rules do. Seller mode shows the discount, approval and the bill of materials.</p>
<script type="module">
await customElements.whenDefined('bmx-configurator');
const cfg = document.getElementById('ex-cf');
const out = document.getElementById('ex-cf-out');
cfg.model = {
name: 'Edge server',
description: 'A short-depth server for branch offices and labs. Every combination you can choose here can be built.',
sku: 'ES-100',
currency: 'GBP',
groups: [
{ id: 'chassis', label: 'Chassis', section: 'Hardware', required: true, default: 'tower', options: [
{ id: 'tower', label: 'Tower', description: 'Quiet, for an office corner.' },
{ id: 'rack1u', label: 'Rack 1U', description: 'One rack unit; no room for a graphics card.', price: 300, sku: 'CH-1U' },
{ id: 'rack2u', label: 'Rack 2U', description: 'Two rack units, room for one full-size card.', price: 450, sku: 'CH-2U', leadTime: 10, badge: 'Popular' },
] },
{ id: 'cpu', label: 'Processor', section: 'Hardware', required: true, default: 'silver', options: [
{ id: 'silver', label: '16 cores', description: '2.1 GHz' },
{ id: 'gold', label: '32 cores', description: '2.4 GHz', price: 900 },
{ id: 'platinum', label: '64 cores', description: '2.0 GHz, needs 128 GB or more', price: 2400 },
] },
{ id: 'ram', label: 'Memory', section: 'Hardware', type: 'number', min: 32, max: 512, step: 32, default: 64, included: 64, unitPrice: 6, unit: 'GB', sku: 'RAM-1G' },
{ id: 'gpu', label: 'Graphics', section: 'Hardware', default: 'none', options: [
{ id: 'none', label: 'None' },
{ id: 'a2', label: 'Entry inference card', price: 1800, sku: 'GPU-A2' },
{ id: 'l40', label: 'Large inference card', description: 'Needs redundant power.', price: 7500, bom: [{ sku: 'GPU-L40', description: 'Large inference card' }, { sku: 'CAB-PWR', description: 'Power cable', quantity: 2 }] },
{ id: 'h100', label: 'Training card', disabled: true, description: 'Allocation only.' },
] },
{ id: 'psu', label: 'Power', section: 'Hardware', required: true, default: 'single', options: [
{ id: 'single', label: 'Single supply' },
{ id: 'redundant', label: 'Redundant supplies', price: 250, bom: [{ sku: 'PSU-800', description: '800 W supply', quantity: 2 }] },
] },
{ id: 'colour', label: 'Front bezel', section: 'Finish', default: 'graphite', options: [
{ id: 'graphite', label: 'Graphite', color: '#3b3f46' },
{ id: 'silver', label: 'Silver', color: '#c9ccd1' },
{ id: 'blue', label: 'Blue', color: '#2457c5', price: 20 },
] },
{ id: 'extras', label: 'Extras', section: 'Finish', type: 'multiple', max: 3, options: [
{ id: 'wifi', label: 'Wi-Fi', price: 40, sku: 'NET-WIFI' },
{ id: 'tpm', label: 'Security chip', price: 25, sku: 'SEC-TPM' },
{ id: 'rails', label: 'Rack rails', price: 60, sku: 'RAIL-KIT' },
{ id: 'lock', label: 'Lockable bezel', price: 30, sku: 'BZ-LOCK' },
] },
{ id: 'tag', label: 'Asset tag', section: 'Finish', type: 'text', max: 12, description: 'Printed on the front, up to 12 characters.' },
{ id: 'os', label: 'Operating system', section: 'Service', default: 'linux', options: [
{ id: 'linux', label: 'Linux' },
{ id: 'windows', label: 'Windows Server', recurring: 45 },
] },
{ id: 'onsite', label: 'Next-day on-site support', section: 'Service', type: 'boolean', unitRecurring: 30 },
],
rules: [
{ kind: 'requires', when: 'chassis = "rack1u"', then: 'gpu = "none"', message: 'A 1U chassis has no room for a graphics card.' },
{ kind: 'requires', when: 'gpu = "l40"', then: 'psu = "redundant"', message: 'The large card needs redundant power.' },
{ kind: 'requires', when: 'cpu = "platinum"', then: 'ram >= 128', message: '64 cores need at least 128 GB of memory.' },
{ kind: 'excludes', when: 'chassis <> "tower"', then: 'HAS(extras, "wifi")', message: 'Wi-Fi is fitted to towers only.' },
{ kind: 'hide', target: 'extras.rails', when: 'chassis = "tower"' },
{ kind: 'price', when: 'COUNTOF(extras) >= 3', amount: -20, label: 'Extras bundle' },
{ kind: 'bom', when: 'chassis <> "tower"', line: { sku: 'CAB-KIT', description: 'Rack cable kit', quantity: '=IF(chassis = "rack2u", 2, 1)' } },
],
pricing: { base: 1500, period: 'month', tiers: [{ from: 5, percent: 5 }, { from: 20, percent: 10 }], maxDiscount: 15, approvalAbove: 10, approvalTotal: 50000 },
quantity: { min: 1, max: 500 },
};
const show = quote => (out.textContent = `${quote.quantity} × ${quote.name}: £${quote.total.toLocaleString('en-GB')}${quote.recurringTotal ? ` + £${quote.recurringTotal.toLocaleString('en-GB')} a month` : ''}${quote.complete ? '' : ' (not finished)'}.`);
cfg.addEventListener('bmxChange', event => show(event.detail.quote));
cfg.addEventListener('bmxSubmit', event => (out.textContent = `Added to the quote: ${event.detail.quote.bom.length} parts, £${event.detail.quote.total.toLocaleString('en-GB')}.`));
</script>
<bmx-configurator id="cfg" mode="seller" submit-label="Add to quote"></bmx-configurator>
<script>
cfg.model = {
name: 'Rack server', currency: 'GBP', pricing: { base: 1500, maxDiscount: 15, approvalAbove: 10 },
groups: [
{ id: 'chassis', label: 'Chassis', required: true, default: 'tower',
options: [{ id: 'tower', label: 'Tower' }, { id: 'rack1u', label: 'Rack 1U', price: 300 }] },
{ id: 'gpu', label: 'Graphics', default: 'none',
options: [{ id: 'none', label: 'None' }, { id: 'l40', label: 'L40', price: 7500 }] },
],
rules: [{ kind: 'requires', when: 'chassis = "rack1u"', then: 'gpu = "none"',
message: 'A 1U chassis has no room for a graphics card.' }],
};
cfg.addEventListener('bmxSubmit', e => addToBasket(e.detail.quote));
</script>
Rules are formulas in the library's spreadsheet language with group ids as
names: what a choice requires, what it excludes, what it hides, price
adjustments and bill-of-materials lines. Options the other choices rule out
are marked with the reason; choosing one anyway shows what else would
change, and does it on request. Answers that only one value can satisfy are
set and marked "set by a rule". Prices follow every change: options, units
above what is included, adjustments, volume tiers and the seller's
discount, with approval limits. The model is plain JSON, and
dist/node/bmx-configurator.mjs runs the same rules and prices on a server,
so a quote can be checked before it is trusted. Nothing is sent anywhere.
Properties
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
autoResolve |
auto-resolve |
boolean |
false |
Choosing an unavailable option makes the other changes it needs at once, rather than asking first. |
currency |
currency |
string |
— | ISO 4217. Default: the model's, then USD. |
discount |
discount |
number |
0 |
The seller's discount in percent, held to the model's most. Two-way. |
hidePrices |
hide-prices |
boolean |
false |
Hide the prices (configure only). |
label |
label |
string |
— | The configurator's accessible name. Default: the model's name. |
layout |
layout |
'steps' | 'single' |
— | Sections as steps one at a time, or every group on one page. Default: steps when groups name sections. |
locale |
locale |
string |
— | A BCP 47 locale for numbers and money. Default: the page's lang, then the reader's. |
mode |
mode |
BmxConfiguratorMode |
'buyer' |
A buyer sees prices; a seller also sees the discount, approval and the bill of materials. |
model |
model |
BmxConfigModel | string |
{ groups: [] } |
The product model: groups, options, rules and pricing (object or JSON). |
name |
name |
string |
— | The field's name in a form; the form receives { selection, quantity, discount } as JSON. |
quantity |
quantity |
number |
1 |
How many are being configured. Two-way. |
readonly |
readonly |
boolean |
false |
Shows the configuration without letting it change. |
strings |
strings |
Partial<BmxConfiguratorStrings> | string |
— | Words to show instead of the English ones, as an object or JSON. |
submitLabel |
submit-label |
string |
— | Shows a submit button with this text; bmxSubmit carries the quote. |
value |
value |
BmxConfigSelection | string |
{} |
What the buyer chose, by group id (object or JSON). Two-way. |
Events
| Event | Detail | Description |
|---|---|---|
bmxChange |
BmxConfiguratorChangeDetail |
Fired after every change, with the selection, every answer, the quote and what else changed. |
bmxSubmit |
{ quote: BmxConfigQuote; selection: BmxConfigSelection; } |
Fired by the submit button, with the quote. |
Methods
| Method | Signature | Description |
|---|---|---|
check |
check() => Promise<BmxConfigProblem[]> |
Problems with the model: ids, options, defaults, rule formulas, and whether anything can be configured. |
focusGroup |
focusGroup(group: string) => Promise<boolean> |
Shows a group (opening its step) and moves focus to it. |
getBomCsv |
getBomCsv() => Promise<string> |
The bill of materials as CSV. |
getQuote |
getQuote() => Promise<BmxConfigQuote> |
The quote as it stands: answers, price lines, totals, bill of materials, approval and what is missing. |
getResolution |
getResolution() => Promise<BmxConfigResolution> |
Every group's answer, where each came from, what is available and what is missing. |
getSelection |
getSelection() => Promise<BmxConfigSelection> |
What the buyer chose (the answers given, not those defaulted or set by rule). |
propose |
propose(group: string, value: BmxConfigValue) => Promise<{ selection: BmxConfigSelection; changes: BmxConfigChange[]; } | null> |
What choosing a value would change, without choosing it; null when no configuration has it. |
redo |
redo() => Promise<boolean> |
Redoes the last undone change. |
reset |
reset() => Promise<void> |
Back to the selection, quantity and discount the configurator started with. |
select |
select(group: string, value: BmxConfigValue, options?: { force?: boolean; }) => Promise<{ chosen: boolean; changes: BmxConfigChange[]; }> |
Chooses as the buyer would. When the value is unavailable, the other changes it needs are made too (force, the default here) or only reported. Resolves with whether it was chosen and what else changed. |
setSelection |
setSelection(selection: BmxConfigSelection, options?: { quantity?: number; discount?: number; }) => Promise<void> |
Replaces the selection (and optionally the quantity and discount). |
undo |
undo() => Promise<boolean> |
Undoes the last change. |
CSS shadow parts
| Part | Description |
|---|---|
group |
A group of choices. |
header |
The product's name and description. |
option |
An option; also option-unavailable, option-chosen. |
steps |
The list of sections. |
submit |
The submit button. |
summary |
The summary, prices and actions. |
CSS custom properties
| Property | Description |
|---|---|
--bmx-configurator-card-width |
The narrowest an option card is drawn. |
--bmx-configurator-chosen |
The colour that marks a chosen option. |
--bmx-configurator-sticky-top |
How far from the top of the page the summary stays when scrolling. |
--bmx-configurator-summary-width |
The width of the summary beside the choices. |