v2.6.0

<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

PropertyAttributeTypeDefaultDescription
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

EventDetailDescription
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

MethodSignatureDescription
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

PartDescription
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

PropertyDescription
--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.