Plugin

importly.js

Drop-in CSV / spreadsheet header mapper for <input type="file">. The user picks a file, importly suggests how their columns map to your fields, they fix anything wrong, and the form posts a clean mapping JSON alongside the file. importly builds the map — your backend does the import.

npm install @goboldlyforward/importly

Try it

A real form, a real mapping

Drop a CSV or spreadsheet — or click Load sample to import a deliberately messy roster. importly will auto-suggest matches; fix any it got wrong, then hit Continue to see what the form would post.

Form payload


    

Install & usage

Drop it in

One stylesheet, one script, one attribute, one JSON field schema. No build step. Plugin auto-mounts on DOMContentLoaded.

<link rel="stylesheet" href="path/to/importly.css">
<script src="path/to/importly.js"></script>

<form action="/leads/import" method="post" enctype="multipart/form-data">
  <input type="file" name="leads"
         accept=".csv,.xlsx,.xls"
         data-importly
         data-fields='[
           {"key":"first_name","label":"First name","required":true},
           {"key":"last_name","label":"Last name","required":true},
           {"key":"email","label":"Email","required":true},
           {"key":"phone","label":"Phone"},
           {"key":"company","label":"Company"}
         ]'>
  <button>Continue</button>
</form>

The plugin keeps the original <input> in the DOM and syncs its files list to whatever the user drops or picks. A sibling <input type="hidden" name="leads_mapping"> is added automatically and updated on every mapping change, so a normal form post carries both the file and the mapping JSON.

Override the hidden field name with data-mapping-name="my_map" if you need it.

XLSX / XLS support is provided by SheetJS, loaded lazily from a CDN the first time a non-CSV file is dropped. Pure-CSV usage never hits the network.

Field schema

Declare what your backend expects

Pass an array of field objects via data-fields. Only key is required; everything else shapes the UI.

{
  "key":         "email",        // identifier your backend uses — required
  "label":       "Email",        // shown to the user — defaults to key
  "required":   true,            // missing required fields show a warning pill
  "type":        "email",        // optional tag rendered next to the label
  "description": "Primary contact"  // shown under the label
}

importly's auto-suggest is intentionally simple: normalize both sides (lowercase, strip punctuation, collapse whitespace), match exact, then match by substring. first_name finds "First Name", "FirstName", "FIRST_NAME", and "first name " — that's about it. The user is the source of truth; auto-suggest just saves clicks.

JavaScript API

For when auto-init isn't enough

// Manual init (after injecting new file inputs)
Importly.initAll(scope);          // scan a subtree for [data-importly]
Importly.init(inputElement);      // mount one input

// Live instance
const inst = Importly.instances.get(inputElement);
inst.getMapping();                // -> { first_name: "First Name", ... }
inst.setMapping({ email: "E-mail Address" });
inst.reset();                     // back to the dropzone

// Events bubble from the original input
input.addEventListener('importly:parsed', (e) => {
  console.log(e.detail.headers, e.detail.rowCount, e.detail.mapping);
});
input.addEventListener('importly:mapping-changed', (e) => {
  console.log(e.detail.mapping);
});
input.addEventListener('importly:reset', () => { ... });
input.addEventListener('importly:error', (e) => alert(e.detail.error.message));