Documentation Index Fetch the complete documentation index at: https://developer.dromo.io/llms.txt
Use this file to discover all available pages before exploring further.
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.
JavaScript
React
Angular
Angular template
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.
Message Effect { 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 : 32 px 24 px ; background : #f9fafb ; }
h2 { margin : 0 0 4 px ; font-size : 18 px ; }
.subtitle { margin : 0 0 24 px ; color : #6b7280 ; font-size : 14 px ; }
.card { background : #fff ; border : 1 px solid #e5e7eb ; border-radius : 8 px ; padding : 16 px ; margin-bottom : 16 px ; }
.row { display : flex ; gap : 8 px ; }
.col { display : flex ; flex-direction : column ; gap : 4 px ; flex : 1 ; }
label { font-size : 12 px ; color : #6b7280 ; font-weight : 500 ; }
input , select { padding : 7 px 10 px ; border : 1 px solid #d1d5db ; border-radius : 6 px ; font-size : 14 px ; width : 100 % ; }
.btn { padding : 8 px 16 px ; border-radius : 6 px ; font-size : 14 px ; 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 : 1 px solid #d1d5db ; color : #374151 ; }
.btn-sm { padding : 6 px 12 px ; font-size : 13 px ; }
#fields-list { list-style : none ; margin : 0 0 8 px ; padding : 0 ; display : flex ; flex-direction : column ; gap : 6 px ; }
#fields-list li { display : flex ; justify-content : space-between ; align-items : center ; padding : 8 px 12 px ; background : #fff ; border : 1 px solid #e5e7eb ; border-radius : 6 px ; font-size : 13 px ; }
#fields-list li button { background : none ; border : none ; cursor : pointer ; color : #9ca3af ; }
#fields-empty { font-size : 13 px ; color : #9ca3af ; font-style : italic ; }
.nav { display : flex ; gap : 12 px ; margin-top : 24 px ; }
</ 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:
Send data
The iframe posts DROMO_MESSAGE with { fields: [...] } to window.parent.
onMessage callback runs
Dromo calls onMessage(uploader, payload). The callback iterates the fields and calls uploader.addField() for each one.
Advance
The iframe posts DROMO_NEXT. Dromo moves to the next step (Review).