Skip to main content
Custom steps let you insert your own UI into the Dromo import flow — between any two built-in steps — without breaking the standard modal experience. Your page loads inside an <iframe> embedded in Dromo’s modal chrome, and communicates back via postMessage.

The import flow

The default step order is:
UPLOAD → HEADER_SELECT → COLUMN_MATCH → SELECT_MATCH → REVIEW
Custom steps can be inserted after any of these steps (except REVIEW):
insertAfterInserted after
"HEADER_SELECT"Header row detection
"COLUMN_MATCH"Column mapping
"SELECT_MATCH"Select field matching — just before Review

Quick start

Register a custom step before calling open(). The url is the page Dromo will load inside the iframe.
import DromoUploader from "dromo-uploader-js";

const dromo = new DromoUploader(licenseKey, fields, settings, user);

dromo.registerCustomStep({
  id: "terms",
  insertAfter: "SELECT_MATCH",
  label: "Terms & Conditions",
  url: "https://your-app.com/dromo-custom-step.html",
});

dromo.open();

Driving navigation from the iframe

Your iframe page posts messages to window.parent to control navigation.
MessageEffect
{ type: "DROMO_NEXT" }Advance to the next step
{ type: "DROMO_BACK" }Return to the previous step
{ type: "DROMO_MESSAGE", payload: any }Trigger the onMessage callback
// Advance to the next step
window.parent.postMessage({ type: "DROMO_NEXT" }, "*");

// Go back
window.parent.postMessage({ type: "DROMO_BACK" }, "*");

// Send data to onMessage, then advance
window.parent.postMessage({ type: "DROMO_MESSAGE", payload: { accepted: true } }, "*");
window.parent.postMessage({ type: "DROMO_NEXT" }, "*");

Modifying the schema from a custom step

Use the onMessage callback to mutate the import schema in response to what the user does in your iframe. It receives the same uploader instance as step hooks, giving you access to addField, removeField, addRows, and more.
dromo.registerCustomStep({
  id: "add-fields",
  insertAfter: "SELECT_MATCH",
  label: "Add Fields",
  url: "https://your-app.com/add-fields.html",
  onMessage: async (uploader, payload) => {
    const { fields } = payload as { fields: Array<{ label: string; key: string; type: string }> };
    for (const field of fields) {
      await uploader.addField(field as any);
    }
  },
});
The iframe sends the payload:
window.parent.postMessage(
  { type: "DROMO_MESSAGE", payload: { fields: [{ label: "Department", key: "department", type: "string" }] } },
  "*"
);
window.parent.postMessage({ type: "DROMO_NEXT" }, "*");

Multiple custom steps

Register multiple steps at the same insertAfter position — they are presented in registration order:
dromo.registerCustomStep({ id: "consent",   insertAfter: "COLUMN_MATCH", url: "/consent.html" });
dromo.registerCustomStep({ id: "data-type", insertAfter: "COLUMN_MATCH", url: "/data-type.html" });
// Flow: … → COLUMN_MATCH → consent → data-type → SELECT_MATCH → REVIEW

Full example: dynamic field builder

The example below is a self-contained iframe page that lets the user define extra fields before Review, then sends them back via onMessage.

Step registration

dromo.registerCustomStep({
  id: "add-fields",
  insertAfter: "SELECT_MATCH",
  label: "Add Fields",
  url: "https://your-app.com/add-fields.html",
  onMessage: async (uploader, payload) => {
    const { fields } = payload as {
      fields: Array<{
        label: string;
        key: string;
        type: string;
        selectOptions?: { label: string; value: string }[];
      }>;
    };
    for (const field of fields) {
      await uploader.addField(field as any);
    }
  },
});

Iframe page (add-fields.html)

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>Add Fields</title>
  <style>
    body { font-family: sans-serif; margin: 0; padding: 32px 24px; background: #f9fafb; }
    h2   { margin: 0 0 4px; font-size: 18px; }
    .subtitle { margin: 0 0 24px; color: #6b7280; font-size: 14px; }
    .card { background: #fff; border: 1px solid #e5e7eb; border-radius: 8px; padding: 16px; margin-bottom: 16px; }
    .row  { display: flex; gap: 8px; }
    .col  { display: flex; flex-direction: column; gap: 4px; flex: 1; }
    label { font-size: 12px; color: #6b7280; font-weight: 500; }
    input, select { padding: 7px 10px; border: 1px solid #d1d5db; border-radius: 6px; font-size: 14px; width: 100%; }
    .btn { padding: 8px 16px; border-radius: 6px; font-size: 14px; font-weight: 600; cursor: pointer; border: none; }
    .btn:disabled { opacity: 0.4; cursor: default; }
    .btn-primary   { background: #4f46e5; color: #fff; }
    .btn-secondary { background: #fff; border: 1px solid #d1d5db; color: #374151; }
    .btn-sm { padding: 6px 12px; font-size: 13px; }
    #fields-list { list-style: none; margin: 0 0 8px; padding: 0; display: flex; flex-direction: column; gap: 6px; }
    #fields-list li { display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; background: #fff; border: 1px solid #e5e7eb; border-radius: 6px; font-size: 13px; }
    #fields-list li button { background: none; border: none; cursor: pointer; color: #9ca3af; }
    #fields-empty { font-size: 13px; color: #9ca3af; font-style: italic; }
    .nav { display: flex; gap: 12px; margin-top: 24px; }
  </style>
</head>
<body>
  <h2 id="title">Add Custom Fields</h2>
  <p class="subtitle">Define extra fields to add to the import schema, then click Continue.</p>

  <div class="card">
    <div class="row">
      <div class="col">
        <label for="field-label">Label</label>
        <input id="field-label" type="text" placeholder="e.g. Department" />
      </div>
      <div class="col" style="max-width:140px">
        <label for="field-type">Type</label>
        <select id="field-type">
          <option value="string">String</option>
          <option value="select">Select</option>
        </select>
      </div>
    </div>
    <div style="margin-top:12px">
      <button id="add-field-btn" class="btn btn-primary btn-sm" onclick="addField()" disabled>
        Add field
      </button>
    </div>
  </div>

  <div class="card">
    <p id="fields-empty">No fields added yet.</p>
    <ul id="fields-list"></ul>
  </div>

  <div class="nav">
    <button class="btn btn-secondary"
      onclick="window.parent.postMessage({ type: 'DROMO_BACK' }, '*')">
      Back
    </button>
    <button class="btn btn-primary" onclick="onContinue()">Continue</button>
  </div>

  <script>
    const pendingFields = [];

    function refreshAddBtn() {
      document.getElementById("add-field-btn").disabled =
        !document.getElementById("field-label").value.trim();
    }

    function addField() {
      const lbl = document.getElementById("field-label").value.trim();
      if (!lbl) return;
      pendingFields.push({
        label: lbl,
        key: lbl.toLowerCase().replace(/\s+/g, "_"),
        type: document.getElementById("field-type").value,
      });
      document.getElementById("field-label").value = "";
      renderFields();
      refreshAddBtn();
    }

    function removeField(i) {
      pendingFields.splice(i, 1);
      renderFields();
    }

    function renderFields() {
      const list  = document.getElementById("fields-list");
      const empty = document.getElementById("fields-empty");
      list.innerHTML = "";
      empty.style.display = pendingFields.length ? "none" : "block";
      pendingFields.forEach((f, i) => {
        const li = document.createElement("li");
        li.innerHTML = `<span><strong>${f.label}</strong> — ${f.type}</span>
          <button onclick="removeField(${i})">āœ•</button>`;
        list.appendChild(li);
      });
    }

    function onContinue() {
      window.parent.postMessage(
        { type: "DROMO_MESSAGE", payload: { fields: pendingFields } },
        "*"
      );
      window.parent.postMessage({ type: "DROMO_NEXT" }, "*");
    }

    document.getElementById("field-label").addEventListener("input", refreshAddBtn);
    document.getElementById("field-label").addEventListener("keydown", (e) => {
      if (e.key === "Enter") addField();
    });
  </script>
</body>
</html>
What happens when the user clicks Continue:
1

Send data

The iframe posts DROMO_MESSAGE with { fields: [...] } to window.parent.
2

onMessage callback runs

Dromo calls onMessage(uploader, payload). The callback iterates the fields and calls uploader.addField() for each one.
3

Advance

The iframe posts DROMO_NEXT. Dromo moves to the next step (Review).
For a complete configuration reference, see the Custom Steps reference.