@updog/data-editor-wc
v0.1.22
Published
Client-side CSV importer and spreadsheet editor SDK as a Web Component. Drop into Vue, Angular, Svelte, or vanilla JS. Import CSV, Excel, JSON; edit 1M+ rows entirely in the browser.
Downloads
2,939
Maintainers
Readme
@updog/data-editor-wc
Client-side CSV importer and spreadsheet editor SDK, shipped as a Web Component for Vue, Angular, Svelte, and vanilla JS. Your users import files, match columns to your schema, fix errors, and submit clean data. Edits happen inline, in the browser, at 1M+ rows.
What is @updog/data-editor-wc
@updog/data-editor-wc is a commercial Web Component that embeds a complete data import wizard and spreadsheet editor into any frontend stack. Same engine as the React SDK; framework-agnostic packaging.
- Import CSV, Excel (XLSX), TSV, JSON, and XML — column auto-matching, value mapping, multi-source merging, validation
- Edit 1M+ rows with undo/redo, find & replace, copy/paste, fill handle, sorting, filtering, and bulk transforms
- Submit clean data through a single
onCompletecallback, classified into insert / update / delete
Everything runs client-side. Files are parsed, validated, and edited in the user's browser — no upload server, no data residency, no DPA required.
Using React? The same SDK ships as a first-class React component —
@updog/data-editor— with typed props, hooks-friendly integration, and a smaller bundle (shares your app's React).
Requirements
- Any modern evergreen browser (Chrome, Firefox, Safari, Edge).
- An API key — this is a commercial SDK. Sign up at updog.tech to get one.
Installation
npm install @updog/data-editor-wcRegister
import "@updog/data-editor-wc";
import "@updog/data-editor-wc/styles.css";Defines <updog-editor> globally. Import once, typically at your app entry.
How you wire the element into a framework (Angular's CUSTOM_ELEMENTS_SCHEMA, Vue's compilerOptions.isCustomElement, Svelte's svelte:element, etc.) depends on your stack. Once registered, the rest of this guide is the same.
Quick Start
<button id="open-btn">Open Editor</button>
<updog-editor id="editor"></updog-editor>
<script type="module">
import "@updog/data-editor-wc";
import "@updog/data-editor-wc/styles.css";
const editor = document.getElementById("editor");
editor.configure({
apiKey: "your-license-key",
primaryKey: "id",
columns: [
{
id: "name",
title: "Full Name",
size: 200,
validators: [{ type: "required", message: "Name is required" }],
},
{
id: "email",
title: "Email",
size: 250,
validators: [
{ type: "required", message: "Email is required" },
{ type: "email", message: "Invalid email" },
],
},
{
id: "role",
title: "Role",
editor: { type: "select", options: ["Admin", "Editor", "Viewer"] },
},
],
loadData: async (onChunk) => {
const res = await fetch("/api/employees");
onChunk(await res.json());
},
onComplete: async (result) => {
for (const source of result.sources) {
const inserts = source.rows.filter((r) => r.isNew && !r.isDeleted && r.isValid);
const updates = source.rows.filter((r) => !r.isNew && r.isChanged && !r.isDeleted && r.isValid);
const deletes = source.rows.filter((r) => r.isDeleted && !r.isNew);
await persist(source.sourceId, { inserts, updates, deletes });
}
editor.hide();
},
});
document.getElementById("open-btn").addEventListener("click", () => editor.show());
editor.addEventListener("close", () => editor.hide());
</script>Setting Props
Two ways to configure the element: HTML attributes for primitives, JS properties for everything else.
HTML attributes
Use for primitive values. The element observes these and re-renders on change.
| Attribute | Maps to | Type |
|---|---|---|
| api-key | apiKey | string |
| primary-key | primaryKey | string |
| locale | locale | string |
| variant | variant | "editor" | "uploader" |
| mode | mode | "modal" | "inline" |
| open | open | boolean (presence = true) |
| rtl | rtl | boolean (presence = true) |
| readonly | readonly | boolean (presence = true) |
<updog-editor api-key="your-key" primary-key="id" variant="uploader" readonly></updog-editor>JS properties
Everything else — objects, functions, arrays — must be set as a JS property or via configure().
const editor = document.querySelector("updog-editor");
editor.columns = [...];
editor.loadData = async (onChunk) => { ... };
editor.onComplete = async (result) => { ... };configure(props)
Set multiple properties at once. Useful during setup. Accepts every prop except onClose. Property updates are batched — the editor re-renders once per microtask regardless of how many properties you set.
editor.configure({
apiKey: "your-key",
primaryKey: "id",
columns: [...],
loadData: async (onChunk) => onChunk(await fetchRows()),
onComplete: async (result) => { await save(result); },
});Inline Mode
Render the editor directly in the DOM without a modal overlay:
<updog-editor mode="inline" api-key="your-key"></updog-editor>In inline mode, show() / hide() and the open attribute don't apply. The close event is not fired.
Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| apiKey | string | Yes | — | Your Updog license key. Validated on each open. |
| columns | DataEditorColumn[] | Yes | — | Column definitions. |
| primaryKey | string | Yes | — | Column ID that uniquely identifies each row. |
| mode | "modal" | "inline" | No | "modal" | Rendering mode. |
| open | boolean | Modal only | — | Controlled modal visibility. Equivalent to show() / hide(). |
| loadData | (onChunk) => Promise<void> | No | — | Async data loader. Stream rows in chunks, optionally tagged by source. |
| onComplete | (result) => void \| Promise<void> | No | — | Called on submit. See Submission Result. |
| variant | "editor" | "uploader" | No | "editor" | Initial view. "uploader" opens the import wizard first. |
| translations | DataEditorTranslations | No | — | Partial i18n overrides. |
| locale | string | No | "en" | BCP 47 locale tag. |
| rtl | boolean | No | false | Right-to-left layout. |
| readonly | boolean | No | false | Hide all editing UI. |
| enableDeleteRow | "all" | "new" | false | No | false | Row deletion policy. |
| enableAddRow | boolean | No | true | Show the "Add row" button. |
| enableCreateColumn | boolean | No | true | Allow creating columns for unmatched CSV headers during import. |
| importFormats | DataEditorFormat[] | No | all | Allowed import formats. [] disables import. |
| exportFormats | DataEditorFormat[] | No | all | Allowed export formats. [] disables export. |
| remoteSources | RemoteSource[] | No | — | Custom import buttons (Google Sheets, S3, etc.) rendered on the upload step. |
| rowHeight | number | No | 34 | Row height in pixels. |
| headerHeight | number | No | 36 | Header height in pixels. |
| server | DataEditorServer | No | — | Server-delegated mode: SDK renders, your backend handles queries and mutations. |
| chat | DataEditorChat | No | — | Bring-your-own AI chat panel. |
| onColumnMatch | (headers, columns) => ... | No | — | Override import column matching. |
| onValueMatch | (valuesToMatch) => ... | No | — | Override import value matching for select columns. |
| synonyms | Record<string, string[]> | No | — | Extra synonyms for column auto-matching. |
| sampleData | Record<string, unknown>[] | No | — | Rows used in the "Download Example" file. |
| localStorage | false | { licenseGrant?: boolean } | No | { licenseGrant: true } | What the SDK caches in localStorage. |
| onError | (error: UpdogError) => void | No | — | Called on internal errors. Use for logging or Sentry. |
| className | string | No | — | CSS class on the wrapper element. |
Methods
| Method | Description |
|---|---|
| show() | Opens the editor modal. Equivalent to setting open to true. No-op in mode="inline". |
| hide() | Closes the editor modal. Equivalent to setting open to false. No-op in mode="inline". |
| configure(props) | Bulk-set any props. See configure(props). |
Events
close
Fires when the user closes the modal (X button or Escape key). Replaces the React onClose prop — the WC dispatches a bubbling CustomEvent instead. Not fired in mode="inline".
editor.addEventListener("close", () => editor.hide());Column Configuration
editor.columns = [
{
id: "email",
title: "Email",
size: 260,
// Declarative validators. See "Built-in Validators" for the full list.
validators: [
{ type: "required", message: "Email is required" },
{ type: "email", message: "Enter a valid email" },
],
// Flag duplicates in this column as errors.
unique: true,
// Revalidate these columns when this one changes.
dependentFields: ["confirmEmail"],
// Cell editor. Default is "text".
editor: { type: "select", options: ["US", "UK", "DE"] },
// Display-only formatting. Does not mutate stored data.
formatter: (value) => value.toLowerCase(),
// Runs when rows are uploaded to the editor. Mutates the stored value.
transformer: (value) => String(value).trim(),
// Sidebar filter control.
filter: { type: "select" },
// Lock cells in this column. `"default"` locks only default-source rows.
locked: "default",
},
];Cell editors: { type: "text" } (default), { type: "date", minDate?, maxDate? }, { type: "select", options: string[] }, { type: "number", decimalPlaces?, decimalSeparator?, thousandsSeparator?, allowChars? }.
Column filters: { type: "select", label?, placeholder?, options?, multiple? }, { type: "number-range", label? }, { type: "date-range", label? }.
Built-in Validators
Validators are declarative objects passed in the validators array on each column. The SDK ships a TS interpreter for the client; Updog Scale ships a matching Go interpreter for server mode. Both runtimes are pinned to the same fixture set, so a column definition produces identical pass/fail results regardless of where it runs.
| type | Fields | Behavior |
|---|---|---|
| "required" | message? | Value must be non-empty. |
| "email" | message? | Value must look like an email address. |
| "numeric" | message? | Value must parse as a number. Commas are stripped before parsing. |
| "regex" | pattern, flags?, message? | Value must match the regular expression. Empty values are skipped. |
| "range" | min?, max?, message? | Value must be numerically within [min, max]. Either bound is optional. |
| "oneOf" | values: string[], message? | Value must be one of the listed strings. |
| "date" | format?: "YYYY-MM-DD" \| "DD/MM/YYYY", message? | Value must parse as a date in the given format. When format is omitted, either format is accepted. |
editor.columns = [
{ id: "name", title: "Name", validators: [{ type: "required", message: "Required" }] },
{
id: "email",
title: "Email",
validators: [
{ type: "required", message: "Required" },
{ type: "email", message: "Invalid email" },
],
},
{ id: "salary", title: "Salary", validators: [{ type: "numeric", message: "Must be a number" }] },
{
id: "status",
title: "Status",
validators: [{ type: "oneOf", values: ["active", "inactive"], message: "Invalid status" }],
},
{
id: "startDate",
title: "Start",
validators: [{ type: "date", format: "YYYY-MM-DD", message: "Use YYYY-MM-DD" }],
editor: { type: "date" },
dependentFields: ["endDate"],
},
];A ValidationError with level: "error" flags the cell in the grid but does not block submission — invalid rows are delivered to onComplete tagged via isValid: false.
Custom validators
For cross-field checks or anything not covered by the built-ins, wrap a (value, row) => ValidationError | null function in { type: "function", fn }. Function validators are client-mode only — they're dropped (with a one-time warning) when the SDK serializes the schema for server mode. Use dependentFields on the column to trigger re-validation when another column changes.
const endAfterStart = (value, row) => {
if (!value || !row.startDate) return null;
if (String(value) < String(row.startDate)) {
return { level: "error", message: "End must be on or after start" };
}
return null;
};
editor.columns = [
{
id: "endDate",
title: "End",
editor: { type: "date" },
validators: [
{ type: "date", message: "Invalid date" },
{ type: "function", fn: endAfterStart },
],
},
];The only level is "error". Return null to indicate the value is valid.
Data Loading
loadData is called once when the editor opens. Call onChunk one or more times to stream rows. The grid renders each chunk without blocking.
// Single source
editor.loadData = async (onChunk) => {
for (let page = 0; page < totalPages; page++) {
const rows = await fetch(`/api/employees?page=${page}`).then((r) => r.json());
onChunk(rows);
}
};
// Multiple sources — each chunk tagged with a source
editor.loadData = async (onChunk) => {
onChunk(await fetchSalesforce(), { source: "Salesforce", done: true });
onChunk(await fetchHubSpot(), { source: "HubSpot", deletable: true, done: true });
};ChunkSourceOptions: { source: string; id?: string; deletable?: boolean; done?: boolean }. Sources are auto-registered on first encounter. Chunks without options go to "Existing Data". When loadData resolves, any source still loading is finalized automatically.
Submission Result
onComplete(result) fires when the user submits. May return a promise.
type DataEditorResult = {
sources: {
sourceId: string;
sourceName: string;
rows: {
row: Record<string, unknown>;
isNew: boolean; // imported or manually added this session
isChanged: boolean; // cell values differ from origin
isDeleted: boolean; // marked for deletion
isValid: boolean; // passes all validators
}[];
}[];
counts: { new: number; changed: number; deleted: number; invalid: number };
};Pristine backend rows (unchanged, not deleted, not new) are omitted from sources[].rows — they're no-ops. The flags are orthogonal: a row can be new, changed, and deleted at once. You own the routing rules:
editor.onComplete = async (result) => {
for (const source of result.sources) {
const inserts = source.rows.filter((r) => r.isNew && !r.isDeleted && r.isValid);
const updates = source.rows.filter((r) => !r.isNew && r.isChanged && !r.isDeleted && r.isValid);
const deletes = source.rows.filter((r) => r.isDeleted && !r.isNew);
await persist(source.sourceId, { inserts, updates, deletes });
}
};If you never tagged sources via loadData, you'll get one entry with sourceId: "backend". result.counts is an aggregate for summary UI — route with the per-row flags, not counts.
Import Configuration
The import wizard guides users through uploading a file, mapping columns to your schema, and resolving value mismatches.
Formats
Supported: "csv", "tsv", "xlsx", "json", "xml".
editor.importFormats = ["csv", "xlsx"]; // only CSV and XLSX
editor.exportFormats = []; // disable export entirelyOverride column matching
editor.onColumnMatch = async (headers, columns) => {
const mappings = await myMatchingService.match(headers, columns);
return mappings; // { csvHeader: columnId | null }
};Return a map of { csvHeader: columnId | null }. Entries set to null or omitted fall back to built-in matching.
Override value matching
For select columns, override imported-value → option mapping:
editor.onValueMatch = async (valuesToMatch) => {
// valuesToMatch = { country: { importedValues: ["espana", "fr"], options: ["Spain", "France"] } }
return {
country: { espana: "Spain", fr: "France" },
};
};Values set to null skip auto-matching. Unmapped values fall back to built-in fuzzy matching.
Synonyms
Extra aliases for the built-in column matcher:
editor.synonyms = {
productSku: ["sku", "article_no", "item_code"],
firstName: ["first", "given_name", "fname"],
};Remote sources
Render custom import buttons for third-party sources. You own auth and picker logic; the SDK renders the button and processes the result.
editor.remoteSources = [
{
id: "google-sheets",
label: "Google Sheets",
icon: "<svg>...</svg>",
fetch: async () => {
const data = await myGoogleSheetsLib.pick();
return data.rows; // Record<string, unknown>[] — or return a File
},
},
];Return a File to parse via the standard CSV/XLSX/JSON/XML pipeline, or return structured records to skip parsing.
Error Handling
Log SDK-internal errors through onError. The SDK recovers gracefully — use this for monitoring, not recovery.
editor.onError = (error) => {
Sentry.captureException(error.originalError ?? error, {
tags: { code: error.code, source: error.source },
});
};Error codes: PARSE_ERROR, RENDER_ERROR, TRANSFORM_ERROR, VALIDATION_ERROR, WORKER_ERROR, COMMAND_ERROR, OPERATION_ERROR.
Styling
<updog-editor> renders in light DOM by design — no Shadow DOM isolation. This is intentional: you can override any editor style with CSS targeting descendants of updog-editor.
CSS variables
The editor reads colors from CSS custom properties. Override them on :root or on the element itself:
updog-editor {
--updog-grid-cell-bg-idle: #ffffff;
--updog-grid-header-bg-idle: #f8f9fa;
}See the Styling reference for the full variable list.
Class names
Internal elements use BEM-style class names prefixed with updog__. Target them for deep customization:
updog-editor .updog__button--primary { background: purple; }Global CSS interaction
Because there is no Shadow DOM, your page's global CSS applies inside the editor. If you have aggressive resets (e.g., button { all: unset }), scope them away from updog-editor descendants:
body *:not(updog-editor *) { /* your global rules */ }Notes
- Fonts are inherited from your page. The editor uses
var(--updog-font-family)which defaults to system fonts. - In modal mode, the editor portals to
document.bodyas a full-screen overlay. In inline mode, it renders in place with no overlay. - Multiple
<updog-editor>instances on the same page are fully independent.
Using React?
Use the first-class React SDK instead:
npm install @updog/data-editorSee @updog/data-editor.
FAQ
Does my user's data leave the browser? No. Parsing, validation, mapping, and editing all run client-side. Your code controls when (and whether) data is submitted to your own backend.
Which frameworks does the Web Component support? Vue, Angular, Svelte, vanilla JS, and any other framework that renders HTML. For React, install @updog/data-editor instead.
Is there a row limit? No hard cap. The grid is engineered for 1M+ rows using canvas rendering and virtual scrolling.
Are there per-import or per-row fees? No. Pricing is flat per production domain. See updog.tech/#pricing.
Can I white-label? Yes, on every plan including the free tier. Strip all Updog branding via CSS theming.
Which file formats are supported? CSV, Excel (XLSX), TSV, JSON, and XML on both import and export.
License
Commercial — see LICENSE for the full terms of the Updog SDK Commercial License v1.0. Contact [email protected] for enterprise licensing. Third-party dependencies bundled with this package are listed in THIRD_PARTY_NOTICES.txt.
