v3.0.0

<bmx-bom-editor>

A multi-level bill of materials, edited in the page: the structure as an indented tree grid, the parts master, the summarised parts list for a build, where each part is used, and what changed against another version.

28 properties · 4 events · 23 methods · 9 parts

Example

Try it with your own BOM files in confidence: they are opened by your browser, on your device. Nothing is uploaded, no copy reaches us or anyone else, and nothing is kept once you close this tab.

Rolling up...

Show markup
<bmx-bom-editor id="ex-bom" label="Coastline e-bike, rev D" name="bom" quantity="25" compare-label="Rev C" description="Cost, weight and lead time roll up as you edit, for the build quantity and the day chosen. Try changing a quantity, dragging a line into another assembly, or pasting rows from a spreadsheet."></bmx-bom-editor>
<p class="note" id="ex-bom-out" role="status" style="margin: 12px 0 0">Rolling up...</p>

<script type="module">
  await customElements.whenDefined('bmx-bom-editor');
  const editor = document.getElementById('ex-bom');
  const out = document.getElementById('ex-bom-out');

  // A made-up e-bike. Both wheels use the same wheel assembly, so it is listed once.
  const part = (id, name, type, more = {}) => ({ id, name, type, ...more });
  const bike = {
    name: 'Coastline e-bike',
    currency: 'GBP',
    parts: [
      part('CB-100', 'Coastline e-bike', 'make', { revision: 'D', cost: 38, leadTime: 2 }),
      part('FRM-20', 'Frame, aluminium 6061, size M', 'buy', { cost: 142, weight: 2.1, leadTime: 42, supplier: 'Northgate Tube' }),
      part('FRK-20', 'Fork, rigid', 'buy', { cost: 38, weight: 0.9, leadTime: 28 }),
      part('WHL-700', 'Wheel assembly 700c', 'make', { cost: 9, leadTime: 1 }),
      part('RIM-700', 'Rim 700c, 32 hole', 'buy', { cost: 17.5, weight: 0.52, leadTime: 21 }),
      part('SPK-292', 'Spoke 292 mm, stainless', 'buy', { cost: 0.31, weight: 0.007, leadTime: 14 }),
      part('HUB-F', 'Hub, front', 'buy', { cost: 14, weight: 0.31, leadTime: 21 }),
      part('TYR-700', 'Tyre 700 x 40c', 'buy', { cost: 11.8, weight: 0.48, leadTime: 10 }),
      part('DRV-250', 'Drive unit 250 W', 'make', { cost: 6, leadTime: 2 }),
      part('MOT-250', 'Hub motor 250 W', 'buy', { cost: 118, weight: 2.9, leadTime: 56, supplier: 'Eastwind Motors' }),
      part('CTL-36', 'Controller 36 V', 'buy', { cost: 34, weight: 0.21, leadTime: 35 }),
      part('BAT-418', 'Battery 36 V 418 Wh', 'buy', { cost: 186, weight: 2.6, leadTime: 49 }),
      part('BAT-500', 'Battery 36 V 500 Wh', 'buy', { cost: 214, weight: 2.8, leadTime: 49 }),
      part('HRN-1', 'Wiring harness', 'buy', { unit: 'm', cost: 2.4, weight: 0.06, leadTime: 14 }),
      part('KIT-FX', 'Fastener kit', 'phantom'),
      part('BLT-M5', 'Bolt M5 x 12', 'buy', { cost: 0.06, weight: 0.004, leadTime: 7 }),
      part('BLT-M5-SS', 'Bolt M5 x 12, stainless', 'buy', { cost: 0.11, weight: 0.004, leadTime: 7 }),
      part('NUT-M5', 'Nut M5, nyloc', 'buy', { cost: 0.03, weight: 0.002, leadTime: 7 }),
      part('PNT-B', 'Paint, harbour blue', 'buy', { unit: 'l', cost: 31, weight: 1.3, leadTime: 5 }),
      part('DWG-CB', 'General arrangement drawing'),
    ],
    lines: [
      { id: 'L1', parent: 'CB-100', part: 'FRM-20', quantity: 1, findNumber: '10' },
      { id: 'L2', parent: 'CB-100', part: 'FRK-20', quantity: 1, findNumber: '20' },
      { id: 'L3', parent: 'CB-100', part: 'WHL-700', quantity: 2, findNumber: '30' },
      { id: 'L4', parent: 'WHL-700', part: 'RIM-700', quantity: 1, findNumber: '10' },
      { id: 'L5', parent: 'WHL-700', part: 'SPK-292', quantity: 32, scrap: 2, findNumber: '20' },
      { id: 'L6', parent: 'WHL-700', part: 'HUB-F', quantity: 1, findNumber: '30' },
      { id: 'L7', parent: 'WHL-700', part: 'TYR-700', quantity: 1, findNumber: '40' },
      { id: 'L8', parent: 'CB-100', part: 'DRV-250', quantity: 1, findNumber: '40' },
      { id: 'L9', parent: 'DRV-250', part: 'MOT-250', quantity: 1, findNumber: '10' },
      { id: 'L10', parent: 'DRV-250', part: 'CTL-36', quantity: 1, findNumber: '20' },
      { id: 'L11', parent: 'DRV-250', part: 'HRN-1', quantity: 1800, unit: 'mm', findNumber: '30' },
      // The bigger battery replaces the smaller one from the new year.
      { id: 'L12', parent: 'CB-100', part: 'BAT-418', quantity: 1, findNumber: '50', effectiveTo: '2026-12-31' },
      { id: 'L13', parent: 'CB-100', part: 'BAT-500', quantity: 1, findNumber: '50', effectiveFrom: '2027-01-01' },
      { id: 'L14', parent: 'CB-100', part: 'KIT-FX', quantity: 1, findNumber: '60' },
      { id: 'L15', parent: 'KIT-FX', part: 'BLT-M5', quantity: 8, alternates: [{ part: 'BLT-M5-SS' }] },
      { id: 'L16', parent: 'KIT-FX', part: 'NUT-M5', quantity: 8 },
      { id: 'L17', parent: 'CB-100', part: 'PNT-B', quantity: 0.35, findNumber: '70' },
      { id: 'L18', parent: 'CB-100', part: 'DWG-CB', quantity: 1, reference: true },
    ],
  };

  // Rev C, last month: 28 spokes and a shorter harness.
  const revC = structuredClone(bike);
  revC.parts.find(p => p.id === 'CB-100').revision = 'C';
  revC.lines.find(l => l.id === 'L5').quantity = 28;
  revC.lines.find(l => l.id === 'L11').quantity = 1500;

  editor.bom = bike;
  editor.compareWith = revC;

  const money = new Intl.NumberFormat('en-GB', { style: 'currency', currency: 'GBP' });
  const report = async () => {
    const r = await editor.getRollup();
    out.textContent = r.unitCost === null
      ? 'Add parts with costs to see the roll-up.'
      : `${money.format(r.unitCost)} a bike, ${money.format(r.totalCost)} for ${r.quantity}; ${r.totalWeight.toFixed(1)} kg in all; ${r.leadTime} days from order to the first bike.`;
  };
  editor.addEventListener('bmxChange', report);
  await report();
</script>
<bmx-bom-editor label="Pump 100, rev C" name="bom" quantity="25"></bmx-bom-editor>
<script>
  editor.bom = { currency: 'GBP', parts: [...], lines: [...] };
  editor.compareWith = revisionB;
</script>

A part is listed once and every assembly that uses it sees the same part, so changing a sub-assembly changes it everywhere it is used (the details panel says how many places). Cost, weight and cumulative lead time are rolled up as you type, for the build quantity and the day chosen (effectivity), with the critical path of lead time marked. Lines carry quantities in their own unit (converted to the part's stock unit), scrap, find numbers, reference designators, effectivity dates, reference-only lines and alternates. Problems - parts missing, cycles, units that do not convert, designators that do not match the quantity - are listed and marked on their rows.

Files come from ERP and PLM exports - indented or parent-child CSV, JSON, a nested JSON tree - and go out as JSON, CSV in each layout, a change list and a printable PDF. Rows pasted from a spreadsheet become lines. Nothing leaves the page. In a form, name receives the BOM as JSON.

Structure keyboard: arrows move (Right opens, then steps into the cells; Left steps back and closes), Enter or F2 edits the cell, typing starts an edit, Insert adds a component, Delete deletes the line, Alt+Up/Down moves it, Alt+Right puts it into the line above, Alt+Left takes it out of its assembly, Ctrl+D duplicates, Ctrl+C copies, Ctrl+V pastes rows, Shift+F10 opens actions.

Properties

PropertyAttributeTypeDefaultDescription
bom bom BmxBom | string | null null The bill of materials: { parts, lines } as an object or JSON (a nested tree or a CSV text are read too). Two-way: bmxChange reports every edit.
columns columns string[] | string — The structure's columns, in order, as an array, JSON or a comma list (part, find, name, qty, ..., and attr:<key> for your own). Two-way: the Columns menu changes it.
compareLabel compare-label string — What the other version is called.
compareWith compare-with BmxBom | string — Another version to compare with (an object, JSON or CSV text).
currency currency string — ISO 4217 code for costs. Default: the BOM's currency.
description description string — Help text below the editor.
disabled disabled boolean false Disable the editor.
effectiveDate effective-date string — The day lines must be in effect on (YYYY-MM-DD), all for every line. Default: today. Two-way.
errorText error-text string — An error supplied by the page.
expandDepth expand-depth number 2 How many levels start open.
hideIneffective hide-ineffective boolean false Leave out lines not in effect on the day (by default they are shown, dimmed).
hideLabel hide-label boolean false Hide the label visually while keeping it as the accessible name.
hideSummary hide-summary boolean false Hide the totals under the views.
hideToolbar hide-toolbar boolean false Hide the toolbar.
label label string — The field's label: names the whole control.
locale locale string — BCP 47 locale for numbers and dates. Default: the page's language.
messages messages Record<string, string> | string — Problem messages in place of the English ones, by code (missing-part, cycle, ...), as an object or JSON.
name name string — The field's name in a form: it receives the BOM as JSON.
panel panel boolean true Show the details panel. Two-way.
pdfStrings pdf-strings Partial<BmxBomPdfStrings> | string — Words for the printed PDF, as an object or JSON.
quantity quantity number 1 How many of the top assembly are built. Two-way.
readonly readonly boolean false Allow looking, finding, comparing and saving, but not changing.
requireValid require-valid boolean false Keep the form from submitting while any problem is an error.
root root string — The top assembly shown, by part number. Default: the BOM's root, else its first top assembly. Two-way.
src src string — An address to read the BOM from (the page's own server): JSON or CSV.
strings strings Partial<BmxBomEditorStrings> | string — Words to show instead of the English ones, as an object or JSON.
view view BmxBomEditorView 'structure' The view shown. Two-way.
views views BmxBomEditorView[] | string — The views offered, as an array, JSON or a comma list. Default: all, compare only with compare-with.

Events

EventDetailDescription
bmxChange BmxBomChangeDetail Fired after the BOM changes.
bmxEdit BmxBomEditDetail Fired before an edit. Cancel it to refuse the edit.
bmxSelect BmxBomSelectDetail Fired when the selected row or part changes.
bmxValidate BmxBomValidateDetail Fired after each check.

Methods

MethodSignatureDescription
addLine addLine(parent: string, line: Partial<Omit<BmxBomLine, "parent">> & { part: string; }, index?: number) => Promise<string | null> Add a line to an assembly (the part is added to the parts list when it is new). Returns the line's id, or null when refused.
collapseAll collapseAll() => Promise<void>
compare compare(other?: BmxBom | string) => Promise<BmxBomComparison | null> What differs from compare-with (or another BOM given here).
expandAll expandAll() => Promise<void>
export export(kind: BmxBomExportKind) => Promise<Blob> A file of the BOM: JSON, CSV in each layout, the change list against compare-with, or a PDF.
getBom getBom() => Promise<BmxBom> The BOM now.
getPartsList getPartsList(include?: "leaves" | "bought" | "all") => Promise<BmxBomFlatList> The summarised parts list for the build.
getProblems getProblems() => Promise<BmxBomProblem[]> The problems found by the last check.
getRollup getRollup() => Promise<{ root: string | null; quantity: number; unitCost: number | null; totalCost: number | null; costComplete: boolean; unitWeight: number | null; totalWeight: number | null; weightComplete: boolean; leadTime: number; }> The top assembly's roll-up for the build quantity and day.
getSelection getSelection() => Promise<BmxBomSelectDetail> The selected row.
load load(file: Blob | string, fileName?: string) => Promise<BmxBomProblem[]> Read a file (JSON, a nested JSON tree, or CSV in the indented, parent-child or parts layout), as an edit that can be undone. Returns what reading found.
redo redo() => Promise<boolean>
removeLine removeLine(id: string) => Promise<boolean> Delete a line.
replacePart replacePart(from: string, to: string) => Promise<number | null> Use another part wherever one is used. Returns how many lines changed (null when refused).
save save(kind?: BmxBomExportKind, fileName?: string) => Promise<void> Download a file of the BOM (see export).
select select(target: string) => Promise<boolean> Select a row by its key (L1/L5), a line id, or a part number (its first place), and show it. Returns whether it was found.
setBom setBom(bom: BmxBom | string, options?: { keepHistory?: boolean; fileName?: string; }) => Promise<void> Replace the BOM (an object, JSON, a nested tree or CSV text). With keepHistory, it can be undone.
setFocus setFocus() => Promise<void>
undo undo() => Promise<boolean>
updateLine updateLine(id: string, patch: Partial<Omit<BmxBomLine, "id" | "parent">>) => Promise<boolean> Change fields of a line. Returns whether it changed.
updatePart updatePart(id: string, patch: Partial<BmxBomPart>) => Promise<boolean> Change a part (a new id renumbers it everywhere). Returns whether it changed.
validate validate() => Promise<BmxBomProblem[]> Check now. Returns every problem.
whereUsed whereUsed(part: string) => Promise<BmxBomWhereUsed> Where a part is used.

Slots

SlotDescription
description Rich help text, in place of the description property.
label Rich label content, in place of the label property.

CSS shadow parts

PartDescription
description The help text under the field.
details The details panel.
grid Each view's grid.
label The label element.
menu The actions and columns menus.
problems The problems panel.
row A grid row; also row-selected, row-problem.
summary The totals under the views.
toolbar The bars of views, settings and commands.

CSS custom properties

PropertyDescription
--bmx-bom-editor-background The views' background.
--bmx-bom-editor-details-width The width of the details panel.
--bmx-bom-editor-height The height of the views' scrolling area.
--bmx-bom-editor-indent How far each level of the structure is indented.
--bmx-bom-editor-label-font-size The label's font size.
--bmx-bom-editor-radius Corner radius of the frame.
--bmx-bom-editor-row-height The height of one row.
--bmx-bom-editor-selected-background The selected row.
--bmx-bom-editor-support-font-size Font size of the description and error message.