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

dom-importer

v1.0.2

Published

Scans the DOM and dynamically imports JavaScript modules when matching elements are found. Supports lazy loading, lifecycle hooks, and SPA-friendly DOM watching.

Readme

dom-importer

npm version npm downloads license bundle size

Scans a DOM tree and dynamically imports JavaScript modules when matching elements are found. Supports plain functions, lifecycle classes (mount / unmount), lazy loading via IntersectionObserver, import-cache deduplication, and SPA-friendly watch — all without depending on any framework.

  • Zero dependencies — no runtime deps, framework-agnostic
  • Dual ESM + CJS — works with Vite, webpack, esbuild, and require()
  • TypeScript — full types included, generic definitions for custom attributes
  • Tree-shakeable — import only what you use

Install

npm install dom-importer

Quick start — functional API

For one-shot page loads the scan helper is the simplest entry point. It accepts the same LoaderOptions as new DomImporter(options), so transformers, mount, init, onError, and rootNode are supported:

import { scan } from 'dom-importer';

scan(
  [
    { selector: 'carousel', load: 'carousel' },
    { selector: 'accordion', load: 'accordion' },
  ],
  {
    transformers: {
      selector: (name) => `[data-component="${name}"]`,
      load: (name) => () => import(`./components/${name}.js`),
    },
  }
);

Class-based API — recommended

Use DomImporter directly whenever you need watch, prefetch, lazy loading, onError, or shared import caching across multiple calls.

import { DomImporter } from 'dom-importer';

const loader = new DomImporter({
  transformers: {
    selector: (name) => `[data-component="${name}"]`,
    load: (name) => () => import(`./components/${name}.js`),
  },
  onError: (err, def) => console.error('Failed to load', def.selector, err),
});

await loader.scan([
  { selector: 'carousel', load: 'carousel' },
  { selector: 'accordion', load: 'accordion' },
]);

watch() — SPA / dynamic DOM

Runs an initial scan and then observes the root node for newly inserted elements, initializing matching components as they appear. Returns a stop function.

const stopWatching = loader.watch([
  { selector: 'carousel', load: 'carousel' },
  { selector: 'tooltip', load: 'tooltip' },
]);

// Later — e.g. before navigating away:
stopWatching();
// Or stop all observers at once:
loader.stop();

prefetch() — warm the import cache

Pre-loads modules during idle time without mounting anything. Subsequent scan or watch calls will use the cached promise.

loader.prefetch([
  { selector: 'modal', load: 'modal' },
  { selector: 'video-player', load: 'video-player' },
]);

lazy: true — viewport-triggered loading

Add lazy: true to any definition to defer loading until the element enters the viewport (IntersectionObserver).

loader.scan([
  { selector: 'video-player', load: 'video-player', lazy: true },
  { selector: 'carousel',     load: 'carousel' },   // eager (default)
]);

clearCache()

Clears the internal import cache, forcing fresh imports on the next scan, watch, or prefetch call.

loader.clearCache();

How modules are initialized

After a matching element is found and the module is loaded, the library dispatches initialization in this order:

1. Per-element mount callback (options.mount) — custom element-level control

Executes for each matched element. Use this for module-specific initialization logic.

May return a cleanup function which will be automatically called when the element is removed from the DOM (when using watch).

const loader = new DomImporter({
  mount: (mod, element, props, def) => {
    new (mod as any).Tabs(element, props);
    
    // Optional: return a cleanup function
    return () => {
      // cleanup code
    };
  },
});

await loader.scan([
  { selector: '.tabs', load: () => import('./tabs.js') },
]);

Adding a class or attribute after load

The mount callback already receives the element and the full definition, so marking loaded elements is straightforward:

// Add a class to every loaded element across all definitions
const loader = new DomImporter({
  mount(mod, element) {
    element.classList.add('is-loaded');
  },
});

To be more selective — for example only marking certain definitions — use a custom attribute on the definition:

import type { ModuleDefinition } from 'dom-importer';

type MyDef = ModuleDefinition<{ loadedClass?: string }>;

const loader = new DomImporter<MyDef>({
  mount(mod, element, props, def) {
    if (def.loadedClass) {
      element.classList.add(def.loadedClass);
    }
  },
});

loader.scan([
  { selector: '.carousel', load: () => import('./carousel.js'), loadedClass: 'carousel--ready' },
  { selector: '.tooltip',  load: () => import('./tooltip.js') },   // no class added
]);

Same idea for attributes:

import type { ModuleDefinition } from 'dom-importer';

type MyDef = ModuleDefinition<{ loadedAttr?: string }>;

const loader = new DomImporter<MyDef>({
  mount(mod, element, props, def) {
    if (def.loadedAttr) {
      element.setAttribute(def.loadedAttr, '');
    }
  },
});

loader.scan([
  { selector: '.carousel', load: () => import('./carousel.js'), loadedAttr: 'data-loaded' },
]);

The same mount callback also handles cleanup when used with watch: return a function and it will be called when the element leaves the DOM.

2. Global init callback (options.init) — batch-level control

Executes once per definition with all matched elements. Use this for cross-cutting initialization logic. If it returns true, the regular mount flow is skipped for that definition.

const loader = new DomImporter({
  init: (mod, elements, def) => {
    // Initialize all matched elements for this definition
    elements.forEach((element) => {
      console.log('Initializing', element);
    });

    // return true to skip mount callback/mount class/default function
  },
});

3. Class with mount() — lifecycle component

If module.default is a class with a mount method, it is instantiated once per element. Data attributes are automatically parsed and passed as the second argument. If unmount() exists, a MutationObserver will call it when the element leaves the DOM.

// components/carousel.ts
import type { Props } from 'dom-importer';

export default class Carousel {
  private el!: Element;

  mount(element: Element, props: Props) {
    this.el = element;
    console.log(props.title);   // from data-title="..."
    console.log(props.values);  // from data-values="[1,2,3]" → parsed as array
  }

  unmount() {
    // cleanup: remove listeners, destroy timers, etc.
  }
}
<div data-component="carousel"
     data-title="Featured"
     data-values="[1,2,3]"
     data-config='{"autoplay":true}'>
</div>

4. Class with constructor parameters — fallback (not recommended)

If module.default is a class without a mount() method, it will be instantiated with (element, props) passed to the constructor. This is a fallback; the mount() method pattern is preferred for clarity.

// components/tabs.ts
import type { Props } from 'dom-importer';

export default class Tabs {
  constructor(element: Element, props: Props) {
    console.log(props.label);
  }

  unmount() {
    // cleanup
  }
}

Note: The mount() method pattern is recommended because it makes the initialization contract explicit. If you export a class without mount(), consider adding it or exporting a plain function instead.

5. Plain function — simple components

If module.default is a plain function (no mount method), it is called once per element with (element, props). May return a cleanup function.

// components/tooltip.ts
import type { Props } from 'dom-importer';

export default function (element: Element, props: Props) {
  element.setAttribute('aria-label', props.label as string);
  
  // Optional: return a cleanup function
  return () => {
    element.removeAttribute('aria-label');
  };
}

When once: true, the function is called once for all matched elements as fn(elements) — no props argument in that case.


CSS / SCSS imports

CSS and SCSS files are supported, just pass a dynamic import of the stylesheet and the library will import it for its side effect. Since a stylesheet has no callable default export, the initialization step is simply skipped.

import { scan } from 'dom-importer';

scan([
  // JS component — normal dispatch
  { selector: '[data-component="carousel"]', load: () => import('./carousel.js') },

  // CSS — imported for its side effect, no init call
  { selector: '[data-component="carousel"]', load: () => import('./carousel.css') },
]);

This also composes naturally with transformers:

import type { ModuleDefinition } from 'dom-importer';

type MyDef = ModuleDefinition<{ withStyles?: boolean }>;

const loader = new DomImporter<MyDef>({
  transformers: {
    selector: (name) => `[data-component="${name}"]`,
    load: (name, def) =>
      def.withStyles
        ? () => import(`./styles/${name}.css`)
        : () => import(`./components/${name}.js`),
  },
});

loader.scan([
  { selector: 'carousel', load: 'carousel' },
  { selector: 'carousel', load: 'carousel', withStyles: true },
]);

Bundlers (Vite, webpack, esbuild) apply the stylesheet as a side effect of the import, the same as a top-level import './carousel.css'.


Data-attribute prop parsing

Props are parsed automatically from data-* attributes before being passed to mount(). Keys become camelCase and values are JSON-parsed when possible:

| HTML attribute | Prop key | Prop value | |-------------------------|------------|-------------------| | data-title="Hello" | title | 'Hello' | | data-count="42" | count | 42 | | data-active="true" | active | true | | data-values="[1,2,3]" | values | [1, 2, 3] | | data-cfg='{"x":1}' | cfg | { x: 1 } | | data-my-label="Hi" | myLabel | 'Hi' |

You can also call parseDataProps manually:

import { parseDataProps } from 'dom-importer';

const props = parseDataProps(document.querySelector('.carousel')!);
// { title: 'Featured', values: [1, 2, 3] }

Custom component lifecycle (mount/unmount)

For components that need setup/teardown logic, export a class with mount and optionally unmount methods:

import type { MountableInstance, Props } from 'dom-importer';

export default class implements MountableInstance {
  mount(element: Element, props: Props): void {
    // Initialize your component
  }

  unmount(): void {
    // Clean up when element is removed
  }
}

The loader automatically:

  • Calls mount(element, props) once per matched element
  • Parses data-* attributes and passes them as props
  • Calls unmount() when elements are removed from the DOM (when using watch)

Example: React adapter

Keep your React components pure and handle React mounting with options.init:

// components/Carousel.tsx — pure React component
export default function Carousel({ title, values }: { title: string; values: number[] }) {
  return (
    <div className="carousel">
      <h2>{title}</h2>
      <p>Items: {values.join(', ')}</p>
    </div>
  );
}
// main.ts
import { DomImporter } from 'dom-importer';
import { createRoot } from 'react-dom/client';
import { createElement } from 'react';
import type { ModuleDefinition } from 'dom-importer';

type MyDef = ModuleDefinition<{ isReact?: boolean }>;

const loader = new DomImporter<MyDef>({
  transformers: {
    selector: (name) => `[data-component="${name}"]`,
    load: (name) => () => import(`./components/${name}.js`),
  },
  mount: (mod, element, props, def) => {
    if (!def.isReact) return;

    const root = createRoot(element);
    root.render(createElement(mod.default as any, props));
    
    // Return cleanup function
    return () => {
      root.unmount();
    };
  },
});

const definitions: MyDef[] = [
  {
    selector: 'video-player',
    load: 'video-player',
    isReact: true,
  },
  { selector: 'tooltip', load: 'tooltip' },
];

await loader.watch(definitions);
<!-- Data attributes are parsed into props -->
<div data-component="carousel" data-title="Featured" data-values="[1,2,3]"></div>

Custom attributes on definitions

Pass extra fields in any definition and read them inside load or selector transformers:

import type { ModuleDefinition } from 'dom-importer';

type MyDef = ModuleDefinition<{
  isReact?: boolean;
  priority?: 'high' | 'low';
  version?: number;
}>;

const loader = new DomImporter<MyDef>({
  transformers: {
    selector: (name) => `[data-component="${name}"]`,
    load: (name, def) => {
      const path = def.priority === 'high' ? './critical' : './components';
      return () => import(`${path}/${name}.js`);
    },
  },
});

loader.scan([
  { selector: 'carousel', load: 'carousel', isReact: true, priority: 'high' },
  { selector: 'tooltip', load: 'tooltip', priority: 'low' },
]);

API reference

new DomImporter(options?)

The primary API. Holds shared state: import cache and active observer instances.

| Method | Returns | Description | |-------------------------|-----------------|-----------------------------------------------------------------------------| | scan(definitions) | Promise<void> | Find matching elements and load + init modules. | | watch(definitions) | () => void | scan + observe DOM for new elements. Returns a per-observer stop fn. | | prefetch(definitions) | Promise<void> | Warm the import cache without mounting. | | stop() | void | Disconnect all active MutationObserver and IntersectionObserver instances. | | clearCache() | void | Clear the import deduplication cache. |

scan(definitions, options?) — functional helper

Creates a DomImporter(options), runs scan, and discards it. Suitable for simple one-shot page loads. options is LoaderOptions<T>, so all loader options are supported. Returns Promise<void>.

prefetch(definitions, options?) — functional helper

Creates a DomImporter and runs prefetch. Returns Promise<void>.


ModuleDefinition<T>

| Property | Type | Default | Description | |------------|---------------------------------------------------|---------|------------------------------------------------------------------------------------------------------| | selector | string | – | CSS selector or short name resolved by transformers.selector. | | load | string \| () => Promise<any> | – | Module path/URL or short name resolved by transformers.load. Prefer a function for bundler support.| | once | boolean | false | Load and call the module only once even if multiple elements match. | | lazy | boolean | false | Defer loading until the element enters the viewport (IntersectionObserver). | | ...T | any | – | Any extra fields — forwarded to transformer functions and callbacks as the last argument. |

LoaderOptions<T>

| Property | Type | Default | Description | |----------------|-------------------------------------------------------------|------------|------------------------------------------------------------------------| | rootNode | Document \| Element | document | Root node to query. Useful for scoped subtrees. | | transformers | Transformers<T> | – | Functions that resolve selector and load strings. | | mount | (module: unknown, element: Element, props: Props, def: ModuleDefinition<T>) => void \| (() => void) | – | Per-element mount callback. Executes for each matched element. May return a cleanup function. | init | (module: unknown, elements: Element[], def: ModuleDefinition<T>) => boolean \| void | – | Global init callback per definition (all matched elements). Return true to skip regular mount flow for that definition. | | onError | (error: unknown, def: ModuleDefinition<T>) => void | – | Error handler. If omitted, errors are logged to console.error. |

Transformers<T>

| Property | Type | Description | |------------|---------------------------------------------------------------|-------------------------------------------------------| | selector | (value: string, def: ModuleDefinition<T>) => string | Converts a short name to a full CSS selector. | | load | (value: string, def: ModuleDefinition<T>) => string\|LoadFn | Converts a short name to a URL or import function. |

MountableInstance

interface MountableInstance {
  mount(element: Element, props: Props): void;
  unmount?(): void;
}

parseDataProps(element)

Parses all data-* attributes of an element into a Props object (Record<string, unknown>) with camelCase keys and JSON-parsed values where applicable.


License

MIT