v2.0.0

<bmx-data-importer>

CSV, TSV, Excel and JSON in; clean, checked rows out - all in the browser. The reader drops a file (or pastes rows), matches its columns to the page's fields, fixes what is wrong in a grid, and imports.

10 properties · 3 events · 6 methods · 8 parts

Example

Try a sample file or drop your own CSV, Excel or JSON file below. Nothing leaves this page.

The sample uses semicolons, day-first dates and decimal commas, and has a few rows to fix.

Show markup
<div class="row" style="display: block">
  <p style="margin: 0 0 12px">
    <bmx-button id="ex-di-sample" variant="soft">Try a sample file</bmx-button>
    <span class="note">or drop your own CSV, Excel or JSON file below. Nothing leaves this page.</span>
  </p>
  <bmx-data-importer id="ex-di" label="Import customers" style="max-inline-size: 64rem"></bmx-data-importer>
</div>
<p class="note" id="ex-di-out" role="status">The sample uses semicolons, day-first dates and decimal commas, and has a few rows to fix.</p>

<script type="module">
  await customElements.whenDefined('bmx-data-importer');
  const importer = document.getElementById('ex-di');
  const out = document.getElementById('ex-di-out');
  importer.fields = [
    { key: 'id', label: 'Customer ID', type: 'integer', required: true, unique: true, aliases: ['Cust no', 'Account'] },
    { key: 'name', label: 'Company', required: true, transform: 'trim', aliases: ['Customer', 'Organisation'] },
    { key: 'email', label: 'Email', type: 'email', unique: true },
    { key: 'country', label: 'Country', options: [{ value: 'GB', label: 'United Kingdom', aliases: ['UK', 'Britain', 'England'] }, { value: 'IE', label: 'Ireland' }, { value: 'FR', label: 'France' }, { value: 'DE', label: 'Germany' }] },
    { key: 'since', label: 'Customer since', type: 'date', aliases: ['Joined', 'Start date'] },
    { key: 'limit', label: 'Credit limit', type: 'number', min: 0, description: 'In pounds; 0 or more.' },
    { key: 'active', label: 'Active', type: 'boolean', defaultValue: true },
  ];
  const sample = [
    'Cust no;Customer;E-mail address;Country;Joined;Credit limit;Active;Notes',
    '1001;Acme Engineering;sales@acme.example;UK;05/03/2024;2.500,00;yes;Key account',
    '1002;Blue Harbour Ltd;info@blueharbour.example;Ireland;20/11/2023;1.250,50;yes;',
    '1003;Quayside Logistics;ops@quayside;France;31/01/2022;750;no;Email bounced',
    '1004;  Northwind Traders ;accounts@northwind.example;Britain;14/02/2021;-50;yes;',
    '1002;Blue Harbour (old);info@blueharbour.example;Ireland;01/06/2019;0;no;Duplicate',
    '1005;Fenwick & Sons;hello@fenwick.example;Germany;31/02/2024;3.000;y;',
    ';Unnamed lead;lead@example.example;Spain;;;;',
    '1006;Moorside Dairy;orders@moorside.example;England;07/07/2020;12.000;1;',
  ].join('\r\n');
  document.getElementById('ex-di-sample').addEventListener('click', () => importer.loadText(sample, 'customers-export.csv'));
  importer.addEventListener('bmxImportComplete', e => {
    out.textContent = `Imported ${e.detail.rows.length} rows (skipped ${e.detail.skipped}, removed ${e.detail.removed}). First: ${JSON.stringify(e.detail.rows[0])}`;
  });
</script>
<bmx-data-importer id="importer"></bmx-data-importer>
<script>
  importer.fields = [
    { key: 'id', label: 'Customer ID', type: 'integer', required: true, unique: true },
    { key: 'name', label: 'Name', required: true, aliases: ['Company'] },
    { key: 'email', label: 'Email', type: 'email', unique: true },
    { key: 'country', options: [{ value: 'GB', label: 'United Kingdom', aliases: ['UK'] }] },
  ];
  importer.importHandler = rows => api.saveCustomers(rows);
</script>

Reading: text files decoded (UTF-8, UTF-16, Windows-1252) and split by a sniffed delimiter to RFC 4180, a piece at a time; Excel workbooks with every sheet, formulas calculated and dates as dates; JSON records flattened. The heading row is found, or not.

Matching: each field is given the column whose name is nearest its key, label or aliases, helped by whether the values suit its type; the reader changes any choice. Dates are read day-first or month-first and numbers with either decimal mark, detected per column from its values.

Review: every row is read into the fields' types and checked - required, type, range, length, pattern, list values (by label or alias), the page's own validate, and repeats of unique fields. Problems are marked in a grid of only the rows in view; the reader edits a cell in place, removes rows, removes every row with problems or every duplicate, and undoes. Import hands the page the records (importHandler, then bmxImportComplete), with the rows that still have problems left out if the reader chooses.

Nothing is uploaded anywhere: the file never leaves the page.

Properties

PropertyAttributeTypeDefaultDescription
accept accept string '.csv,.tsv,.txt,.xlsx,.json,text/csv,text/plain,application/json,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' The file types offered by the file picker.
allowSkip allow-skip boolean true May the reader import while rows still have problems, leaving those rows out.
dateOrder date-order 'auto' | BmxImportDateOrder 'auto' How dates written with numbers are read: auto decides per column from its values, then the locale.
fields fields BmxImportField[] | string [] The fields the page wants, as objects or JSON. Empty: every column is a field, named by its heading.
importHandler property only BmxImportHandler — The page's own import, awaited before bmxImportComplete.
label label string — The importer's accessible name. Default: "Import data".
locale locale string — The locale for detecting date order. Default: the page's.
maxFileSize max-file-size number 50 * 1024 * 1024 The largest file read, in bytes. Default 50 MB.
maxRows max-rows number 1000000 The most rows read. Default 1,000,000.
strings strings Partial<BmxDataImporterStrings> | string — Words to show instead of the English ones, as an object or JSON.

Events

EventDetailDescription
bmxImportComplete BmxImportCompleteDetail Fired when the rows have been imported.
bmxImportError BmxImportErrorDetail Fired when a file cannot be read or the import fails.
bmxImportFile BmxImportFileDetail Fired before a file is read. Cancel it to refuse the file.

Methods

MethodSignatureDescription
getProblems getProblems() => Promise<{ line: number; field: string; message: string; }[]> The problems as they stand in review.
getRows getRows() => Promise<Record<string, unknown>[]> The rows as they stand in review, as records.
loadFile loadFile(file: Blob, name?: string) => Promise<boolean> Reads a file, as if dropped. Resolves whether it was read.
loadRows loadRows(rows: unknown[], name?: string) => Promise<boolean> Takes rows already in hand: an array of records, or of arrays (the first may be the heading row).
loadText loadText(text: string, name?: string) => Promise<boolean> Reads text: CSV, TSV or JSON. Resolves whether it was read.
reset reset() => Promise<void> Back to the start, forgetting the file.

CSS shadow parts

PartDescription
cell A review cell; also cell-problem.
done The finished message.
drop The drop area.
footer The row of Back and the next action.
grid The review grid.
mapping The table matching fields to columns.
steps The list of steps.
summary The counts above the review grid.

CSS custom properties

PropertyDescription
--bmx-data-importer-cell-width The width of each field's column in review.
--bmx-data-importer-height The height of the review grid.
--bmx-data-importer-line-width The width of the row-number column in review.
--bmx-data-importer-problem The colour that marks a problem.