npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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

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 onComplete callback, 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-wc

Register

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 entirely

Override 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.body as 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-editor

See @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.