Add a CSV importer to Angular in 10 minutes
Angular apps use the framework-agnostic JavaScript SDK, wrapped in a service so the rest of your application never touches the uploader directly.
npm install dromo-uploader-js
Define the schema and the service together. The service constructs an uploader on demand, opens it, and resolves a promise when the user finishes. Running the callback through NgZone keeps change detection predictable, which matters more than it sounds and is covered in the gotchas below.
// src/app/import/dromo-import.service.ts
import { Injectable, NgZone, inject } from "@angular/core";
import DromoUploader, { type IDeveloperField } from "dromo-uploader-js";
import { environment } from "../../environments/environment";
@Injectable({ providedIn: "root" })
export class DromoImportService {
private zone = inject(NgZone);
private readonly employeeFields: IDeveloperField[] = [
{ label: "Employee ID", key: "employee_id", type: "string", validators: [{ validate: "required" }, { validate: "unique" }] },
{ label: "Work email", key: "work_email", type: "email", validators: [{ validate: "required" }] },
{ label: "Hire date", key: "hire_date", type: "date" },
{
label: "Department",
key: "department",
type: "select",
selectOptions: [
{ label: "Engineering", value: "eng" },
{ label: "Sales", value: "sales" },
{ label: "Operations", value: "ops" },
],
},
];
openEmployeeImport(userId: string): Promise<Record<string, unknown>[]> {
return new Promise((resolve) => {
const uploader = new DromoUploader(
environment.dromoLicenseKey,
this.employeeFields,
{ importIdentifier: "employees", developmentMode: !environment.production },
{ id: userId }
);
uploader.onResults((rows) => this.zone.run(() => resolve(rows)));
uploader.open();
});
}
}
The component that triggers the import stays small. It injects the service and an HttpClient, awaits the rows, and posts them.
// src/app/import/import-button.component.ts
import { Component, inject, signal } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { DromoImportService } from "./dromo-import.service";
@Component({
selector: "app-import-button",
standalone: true,
template: `
<button type="button" (click)="start()">Import employees</button>
@if (imported() > 0) {
<p>{{ imported() }} employees imported.</p>
}
`,
})
export class ImportButtonComponent {
private importer = inject(DromoImportService);
private http = inject(HttpClient);
imported = signal(0);
async start() {
const rows = await this.importer.openEmployeeImport("user_42");
this.http.post("/api/employees/bulk", rows).subscribe(() => this.imported.set(rows.length));
}
}
That is the whole integration: a service, a component, and an endpoint that trusts the rows because every validator has already run.
How it fits the Angular model
Wrapping the SDK in an injectable is the idiomatic move. Dependency injection gives you one place to read the license key from environment, one place to attach hooks, and a seam you can replace with a fake in unit tests. Components ask for a result and stay ignorant of how the modal works.
Standalone components need no module registration, as shown above. Projects still on NgModules import nothing special either; the SDK is a plain class, so it works wherever a service can be provided. Nothing in the uploader requires CommonModule or a specific Angular version beyond what the SDK's TypeScript targets.
The uploader renders its own modal outside your component tree, so ViewEncapsulation does not interfere with its styles and your styles do not leak into it. Appearance is set through the SDK's style overrides rather than through Angular stylesheets.
Angular gotchas Dromo handles for you
Callbacks outside the zone. Third-party widgets fire completion events from code Angular did not patch, so the view does not update when the import finishes. Developers spend an hour wondering why the row count stays at zero until they click somewhere. The zone.run wrapper in the service above fixes it; on zoneless applications, writing the result into a signal triggers the update on its own.
ExpressionChangedAfterItHasBeenCheckedError. Opening a modal and mutating bound state during the same change detection pass throws this in development. Because the uploader is opened from an event handler and reports back asynchronously, the two never collide.
Web Workers under the CLI. Parsing a large file without freezing the UI means a worker, and the Angular CLI's worker configuration trips up most teams the first time. Dromo processes the file inside the widget, and for uploads that are still too big for one sitting, the user can download an annotated Excel workbook, fix it offline, and resume.
Memory on large uploads. Keeping a hundred thousand parsed rows in a component property and rendering them with *ngFor will stall the browser. The review grid is virtualized inside the uploader and your component only ever receives the final array.
Reactive Forms and file inputs. FormControl does not bind to <input type="file">, so custom importers end up with a ViewChild and manual event handling. There is no file input in your template at all.
Dromo vs building it with PapaParse
| Build it yourself | Dromo | |
|---|---|---|
| Column mapping | A mapping component with a mat-select per header, and no memory of last time |
Header-to-field suggestions that are remembered between imports |
| Validation | Validators duplicated between a Reactive Form and the backend | Type, regex, range, uniqueness, and allowed-value checks, plus hooks that call your API |
| Error correction UI | A data table with an editable cell renderer and custom error state | Inline highlighting in a review grid; users fix cells without leaving the flow |
| Large files | A worker, a streaming parser, and a virtual scroll strategy | Handled in the widget, with offline fix-and-resume for the worst files |
| Internationalization | @angular/localize extraction for every string in the wizard |
Thirty-plus languages included |
| Ongoing maintenance | New customer format, new bug ticket | Schema edits, often made by a PM without a release |
A team ingesting a fixed-format CSV from their own export job should reach for PapaParse and a worker. That is a solved problem and it costs nothing. The importer earns its keep once the spreadsheets are authored by customers, because then the hard part is the human in the loop, not the parser.
Ship it to your users
Every plan includes white labeling at no additional charge. Fonts, colors, and copy are configurable so the modal matches an Angular Material theme or a custom design system, and no Dromo mark appears.
More than thirty languages are supported out of the box. If your app already sets a locale with LOCALE_ID, pass the same value to the uploader and the entire flow follows it.
Private Mode runs the complete import in the browser and retains nothing on Dromo's infrastructure. It is a settings change, not an architecture change, and the service above needs no edits beyond that flag.

