A lightweight module loader that lets DOM elements request their own JavaScript dependencies.
- What is DOMule?
- The Problem It Solves
- Performance Comparison
- Who Should Use DOMule
- Why Not Use...
- Architecture
- Quick Start
- Writing Modules
- Core Concepts
- Inter-Module Communication (module API)
- Using Third-Party Modules
- Migration Guide (v2.x → v3.0)
- Troubleshooting
- DOM... what?
- Notes
DOMule enables any DOM element to request JavaScript modules, so the page loads scripts only for the elements that actually require them. This eliminates massive amounts of overhead in loading scripts that you don't (always) need.
Write <div data-requires="module.mjs"> and DOMule imports that module, calls its init() function, and passes the
element to it. If multiple elements require the same module, all requiring elements are passed. Optionally, you can even
defer loading the module until the element is visible; just add data-require-lazy="true".
Zero build step. Pure ES6 modules.
Most JavaScript frameworks solve the problem backwards: scripts search the DOM for elements to enhance. Scripts that are there. Always. On each load, regardless of context. Dead weight, adding insult to injury by polluting the global namespace.
DOMule inverts this — elements declare what they need, so scripts only load when actually needed.
This eliminates three common problems:
- Wasted bytes: Scripts loading even when their target elements aren't on the page, bloating the payload
- Manual orchestration: Writing and maintaining selectors, event binding, initialization order, etc.
- Build complexity: Bundlers, tree-shaking, code-splitting configs
DOMule's contract:
- An
elementsaysdata-requires="module.mjs" module.mjsis loaded- Its
init()function executes, receiving theelementas an argument (or every element that requiredmodule.mjs, if multiple request it).
No build step. No framework lock-in. Just load what you need, when you need it.
| Traditional Multi-Script Approach | DOMule Approach |
|---|---|
| HEAD | HEAD |
<script src="jquery-3.7.1.min.js"></script> (87KB) |
<script type="module" src="entrypoint.mjs"></script> (2KB) |
<script src="bootstrap.bundle.min.js"></script> (59KB) |
|
<script src="lodash.min.js"></script> (73KB) |
|
<script src="app-utils.js"></script> (24KB) |
|
| BODY | BODY |
<div class="slider">...</div> |
<div class="slider" data-requires="./slider.mjs">...</div> |
<div class="gallery">...</div> |
<div class="gallery" data-requires="./gallery.mjs">...</div> |
FOOTER (i.e. just before </body>) |
|
<script src="slider.js"></script> (31KB) |
(loads automatically: slider.mjs, 15KB) |
<script src="gallery.js"></script> (42KB) |
(loads automatically: gallery.mjs, 18KB) |
<script src="video-player.js"></script> (56KB) |
(not present on page, doesn't load) |
<script src="map.js"></script> (38KB) |
(not present on page, doesn't load) |
| TOTAL TRANSFER | TOTAL TRANSFER |
| 410KB | 35KB |
| Page Type | Elements Present | Traditional (always loads) | DOMule (loads only needed) | Bytes Saved |
|---|---|---|---|---|
| Homepage | Slider only | 422KB | 17KB (entrypoint + slider) | 96% reduction |
| Blog Post | None (just text) | 422KB | 2KB (entrypoint only) | 99.5% reduction |
| Gallery Page | Gallery + Lightbox | 422KB | 35KB (entrypoint + gallery) | 92% reduction |
| Contact Page | Map only | 422KB | 40KB (entrypoint + map) | 90% reduction |
| Video Page | Video player below fold | 422KB | 2KB initially, then 58KB on scroll | 86% reduction |
| Dashboard | All features | 422KB | 127KB (loads all modules) | 70% reduction |
| Aspect | Traditional | DOMule |
|---|---|---|
| Scripts parsed on load | All 10 scripts, always | 1 entrypoint, then 0-6 modules as needed |
| Memory footprint | jQuery + Bootstrap + Lodash + all feature code = ~2.8MB heap | Only loaded modules = ~400KB-1.2MB heap |
| Parse/compile time | 180-250ms (all scripts) | 15-60ms (only needed modules) |
| Code executed unnecessarily | 60-80% never runs on any given page | 0% - only loaded code runs |
| Main thread blocking | 250ms+ (all scripts parse/execute) | 50-100ms (staggered, async) |
Traditional (everything loads):
Time → 0ms ────────────────────────────────────────────── 850ms
│
HTML ████ (20ms)
jquery │ ██████████ (95ms, blocks rendering)
bootstrp │ ██████ (65ms, blocks rendering)
lodash │ ████████ (80ms, blocks rendering)
utils │ ███ (30ms, blocks rendering)
slider │ ████ (40ms)
gallery │ █████ (50ms)
video │ ██████ (60ms, UNUSED)
map │ ████ (45ms, UNUSED)
analytics│ ██ (15ms)
│
DOMContentLoaded fires at: 850ms
DOMule (conditional loading):
Time → 0ms ──────────────── 180ms
│
HTML ████ (20ms)
entrypt │ ██ (10ms, non-blocking)
slider │ ███ (25ms, async, needed)
gallery │ ████ (30ms, async, needed)
│
DOMContentLoaded fires at: 180ms
Assuming typical user journey: Homepage → Blog → Gallery → Contact → 6 more blogs
| Approach | Total Bytes Transferred | Parse/Compile Time | Cache Efficiency |
|---|---|---|---|
| Traditional | 4,220KB (422KB × 10) | 2,500ms | Low (bundle invalidation) |
| DOMule | 287KB (varied by page) | 340ms | High (module-level) |
| Savings | 93% less bandwidth | 86% less CPU | Granular cache hits |
| Factor | Traditional | DOMule |
|---|---|---|
| Build step | Required (webpack/rollup) | None |
| Bundle configuration | Manual optimization needed | Automatic, DOM-driven |
| Add new feature | Edit build config → rebuild → test | Write module → add data-requires |
| Remove feature | Edit imports → rebuild → test | Remove element (script auto-excluded) |
| Code splitting | Manual import() statements |
One attribute: data-require-lazy="true" |
| Time to first byte | Wait for bundle rebuild (5-15s) | Instant (no build) |
- 96% reduction on typical pages (homepage with one feature)
- 850ms → 180ms DOMContentLoaded time
- Zero rebuild time during development
- 93% less bandwidth over typical 10-page session
- 86% less parse/compile time across pages
Rule of thumb: If >50% of your JS doesn't run on most pages, DOMule pays for itself in bandwidth alone.
Ideal for:
- Content-heavy sites where most pages use 20% of available JS features
- Theme developers wanting to offer optional JS features without bloat
- Escaping the confines of Wordpress' JavaScript ecosystem
- Server-rendered pages with progressive enhancement
- Teams without (or avoiding) build pipelines
- Projects where "just attach behavior to elements" covers 80% of JS needs
- Sites with optional heavy features (galleries, maps, forms) that shouldn't penalize every page
- Anyone concerned with optimizing their page load times and willing to squeeze every last KB out of it
Not ideal for:
- Large SPAs with complex state management (use React/Vue/Svelte)
- Applications requiring SSR hydration
- Projects already invested in a bundler workflow
- Teams needing component-level reactivity
Simple test: If your mental model is "this element needs this script" rather than "this app needs this state tree," DOMule fits.
What it does: Controllers attach to data-controller attributes with lifecycle callbacks, targets, and actions.
Key difference: Stimulus is a full framework for organizing behavior. DOMule is just a loader—it imports modules and
calls init(), then gets out of the way. No lifecycle, no targets, no framework opinions. If you need structure, use
Stimulus. If you just need "load this when that appears," DOMule is lighter.
What it does: Declarative reactivity via data attributes (x-data, x-show, x-model).
Key difference: Alpine handles UI state and interactivity. DOMule handles module loading. Alpine is always present on the page; DOMule modules only load when needed. You'd use Alpine for reactive components, DOMule for loading the scripts that power them (or not loading them if they're not on the page).
What it does: Asynchronous module loading with dependency management via define() and require().
Key difference: RequireJS uses programmatic, script-driven loading (require(['module'], callback)). DOMule uses
DOM-driven loading (data-requires="module"). RequireJS requires wrapping everything in AMD syntax. DOMule uses
native ES6 modules. RequireJS solves "load dependencies in order." DOMule solves "only load what's on the page."
What it does: Build-time analysis to split bundles and lazy-load chunks via import().
Key difference: Webpack decides splits during build based on static analysis. DOMule decides at runtime based on viewport visibility. Webpack requires a build step and toolchain. DOMule runs in the browser with zero build. Webpack optimizes bundles. DOMule optimizes page load by not loading scripts for absent elements.
The pattern no one else solves: "Load this script only if this element exists and is visible, then pass all matching elements to one initialization function." That specific intersection is DOMule's territory.
DOMule uses a three-tier module architecture:
System infrastructure required for DOMule to function:
core.scanner.mjs– Discovers elements withdata-requiresattributescore.loader.mjs– Orchestrates module imports and initializationcore.events.mjs– Manages lifecycle events (docReady, resize, scroll, etc.)core.registry.mjs- Keeps track of modules, and handles module api.core.log.mjs– Provides colored console logging (enabled via?debug=true)core.telemetry.mjs– Performance tracking, budget monitoring, metrics dashboard
Reusable helpers used by core and available to modules:
util.observe.mjs– Visibility/resize detection (isVisible, IntersectionObserver wrappers)util.debounce.mjs– Debounce/throttle functions for eventsutil.color.mjs– Color generation and manipulationutil.dom.mjs– DOM manipulation utilitiesutil.format.mjs– String/phone/URL formattingutil.math.mjs– Math utilities, bezier curvesutil.perf.mjs– FPS counter, performance trackingutil.iteration.mjs– Array/object iteration helpersutil.mediainfo.mjs– CSS media query wrapper (prefers-reduced-motion, etc.)util.ensure-visibility.mjs– Viewport scrolling with header detectionutil.polyfills.mjs– Compatibility patches (auto-imports, no init needed)
Here's where your modules live. There are some examples in the repository (e.g. hnl.breakpoints.mjs,
hnl.baseline-grid.mjs), and you can use _template.mjs to get started with writing your own.
Place them somewhere in your project, presumably something like /assets/js/domule/.
Heads-up!
Location of the entrypoint does not really matter, but there's one caveat: module paths, as requested by DOM elements in their
data-requiresare resolved relative to this entrypoint. So if your entrypoint is in/assets/js/entrypoint.mjs, and you have a module in/assets/js/modules/mymodule.mjs, you would usedata-requires="./modules/mymodule.mjs".See Path Resolution for more info.
For the sake of this example, I will assume the entrypoint is at /assets/js/entrypoint.mjs, and domule lives in
/assets/js/domule/.
// entrypoint.mjs
import events from './domule/core.events.mjs'; //make sure this path is correct
import {loadModules} from './domule/core.loader.mjs'; //make sure this path is correct
events.docReady(() => {
// Handle all dynamic module imports
loadModules({
'assets': 'https://cdn.example.com/js/' // Optional: path aliases
}, () => {
console.log('All modules initialized');
});
});<head>
<script type="module" src="entrypoint.mjs"></script>
</head><!-- Basic usage -->
<div data-requires="./modules/mymodule.mjs"></div>
<!-- Using path aliases (if configured) -->
<div data-requires="%assets%mymodule.mjs"></div>
<!-- Multiple modules -->
<div data-requires="./modules/slider.mjs,./modules/analytics.mjs"></div>
<!-- Lazy loading (only loads when visible) -->
<!-- Loose: viewport only (default) -->
<div data-requires="./gallery.mjs" data-require-lazy="true">
<!-- or: ("loose" and "true" do the same)-->
<div data-requires="./gallery.mjs" data-require-lazy="loose">
<!-- Strict: viewport + rendered visibility -->
<div data-requires="./modal.mjs" data-require-lazy="strict">/**
* example-module.mjs
* Example module that fades in elements when they become visible
*/
import {isVisible} from "./util.observe.mjs";
import events from "./core.events.mjs";
import {logger} from "./core.log.mjs";
/**
* Module name (used for logging)
*/
export const NAME = 'fadeInModule';
/**
* Called automatically when module is loaded
* @param {NodeList} elements - All elements with data-requires="./example-module.mjs"
* @returns {string|boolean|undefined} Optional status message
*/
export function init(elements) {
// Check visibility on scroll/resize
events.addListener('docShift', () => {
elements.forEach(element => {
isVisible(element, (visible) => {
if (visible) {
element.classList.add('fade-in');
logger.log(NAME, 'Element became visible');
}
});
});
});
return `Initialized ${elements.length} element(s)`;
}Use _template.mjs as a starting point:
import {isVisible} from "./util.observe.mjs";
import events from "./core.events.mjs";
import {logger} from "./core.log.mjs";
export const NAME = 'exampleModule';
export function init(elements) {
// Your code here
// Optional: listen for events
events.addListener('docShift', () => {
elements.forEach(element => {
// Do something on scroll/resize
});
});
// Optional: return status
return 'Module ready';
}DOMule supports multiple ways to specify module paths:
Standard ES6 module paths relative to your entrypoint:
<div data-requires="./modules/mymodule.mjs"></div>
<div data-requires="../vendor/library.mjs"></div>data-requires are resolved relative to where your entrypoint lives, not relative to the
HTML file. This is standard ES6 module behavior.
Example:
/
├── index.html
├── js/
│ ├── entrypoint.mjs ← Your entrypoint
│ └── modules/
│ └── slider.mjs ← Your module
<!-- In index.html -->
<div data-requires="./modules/slider.mjs"></div>
<!-- ✅ Resolves to: js/modules/slider.mjs (relative to entrypoint) -->
<div data-requires="../modules/slider.mjs"></div>
<!-- ❌ Would look for: modules/slider.mjs (outside js/) -->Define reusable path aliases for cleaner imports:
loadModules({
'assets': 'https://cdn.example.com/js/',
'vendor': 'https://unpkg.com/'
});Use with %name% syntax:
<div data-requires="%assets%mymodule.mjs"></div>
<div data-requires="%vendor%[email protected]/lodash.min.js"></div>Load from CDNs or external sources:
<div data-requires="https://cdn.example.com/module.mjs"></div>If a global SITE_NONCE variable exists, it's appended automatically for Content Security Policy compliance:
<script>const SITE_NONCE = 'your-nonce-here';</script>
// DOMule adds: ?nonce=your-nonce-hereDOMule has a "minified-first" strategy on module importing; it automatically tries loading minified versions (
.min.mjs)
first, and falls back to unminified versions if a minified version is not found.
So for <div data-requires="./module.mjs"></div>, DOMule will first try to fetch module.min.js,
falling back to module.mjs if that fails.
The "attempt-then-fallback" pattern is superior to probing for a minified version first, because:
- Fewer requests - Probe = 2 requests (HEAD + GET), fallback = 1-2 requests (GET + optional retry)
- Simpler code - No HEAD request handling, just catch/retry
- Better caching - Browser caches GET responses, not HEAD
- Race-free - No timing issues between probe and actual import
One request per module is the cost of doing business. The fallback only fires when minified is genuinely missing, which should be rare in production (assuming you deploy both versions).
Enabling debug mode completely disables the "minified-first" import behaviour and always loads unminified
modules (i.e.: the exact path definition in data-requires).
Use data-require-lazy to defer loading until elements are visible:
<!-- Only loads when video scrolls into view -->
<video data-requires="./videoplayer.mjs" data-require-lazy="true">
<source src="video.mp4">
</video>"true"or"loose"- Standard IntersectionObserver (viewport intersection only)"strict"- Adds obstruction checking (viewport + rendered visibility + transition/animation detection)
Use "strict" mode for elements that:
- Animate from
opacity: 0to visible - Are revealed via
display: none → block - Appear from off-canvas (hidden by parent overflow)
- Show/hide via modals or overlays
How it works:
- Module is not loaded initially
- Visibility watcher monitors the element
- When visible, module loads and
init()is called - Watcher is removed to prevent memory leaks
Uses IntersectionObserver when available (Chrome 61+, Safari 12.1+, Firefox 60+), falls back to scroll events.
Elements get automatic CSS classes tracking module load progress:
| State | Class | When |
|---|---|---|
| Pending | .module-pending |
Awaiting import |
| Loading | .module-loading |
Import started (lazy only) |
| Loaded | .module-loaded |
Init complete |
| Error | .module-error |
Failed |
Also available as data-requires-state attribute for JavaScript access.
Example - Show spinner during load:
<div data-requires="./gallery.mjs">
<div class="loader">Loading...</div>
<div class="content"></div>
</div>.module-pending .loader {
display: block;
}
.module-pending .content {
display: none;
}
.module-loaded .loader {
display: none;
}
.module-loaded .content {
display: block;
}Example - Lazy load indicator:
<img data-requires="./lightbox.mjs" data-require-lazy="true">img.module-pending {
opacity: 0.3;
}
img.module-loading {
animation: pulse 1s infinite;
}
img.module-loaded {
opacity: 1;
}Elements with multiple modules (e.g., data-requires="a.mjs,b.mjs") transition to loaded only when all succeed.
The event handler (core.events.mjs) provides:
docReady– DOM content loadeddocLoaded– All resources loaded (including images)imgsLoaded– All non-lazy images loadeddocShift– Combined resize/scroll/visibility changestartResize,resize,endResize– Window resize phasesbodyResize– Body element dimension changesstartScroll,scroll,endScroll– Scroll phasesdocBlur,docFocus– Document visibility changesbreakPointChange– Responsive breakpoint changes (xs,sm,md,lg,xl,xxl,xxxl)*
* Uses Bootstrap 5's breakpoint system, both for naming and cutoff (pixel) values.
Usage:
import events from './core.events.mjs';
events.addListener('resize', (e) => {
console.log('Window resized');
});
// Or use shorthand
events.docReady(() => {
console.log('DOM ready');
});Modules can clean up and unregister themselves when no longer needed:
export const NAME = 'gallery';
let observer, animationId, listeners = [];
export function init(elements) {
// Setup
observer = new IntersectionObserver(/*...*/);
animationId = requestAnimationFrame(animate);
elements.forEach(el => {
el.addEventListener('click', handleClick);
listeners.push({el, handler: handleClick});
});
}
export function destroy() {
// Clean up resources
observer?.disconnect();
cancelAnimationFrame(animationId);
listeners.forEach(({el, handler}) => {
el.removeEventListener('click', handler);
});
// Unregister from module system
ModuleRegistry.unregister(NAME);
}
// Module decides when to self-destruct
someCondition && destroy();Note: ModuleRegistry.unregister() rejects pending promises (i.e. ModuleRegistry.waitFor())
Enable comprehensive logging with ?debug=true:
https://yoursite.com/page.html?debug=true
Debug mode provides:
- Detailed module loading logs
- Color-coded console output
- Error stack traces
- Cache-busting (forces fresh downloads) - unless specifically overriden using
&cache=true - Performance timing
Example output:
[core][loader] Importing module.mjs...
[myModule] Imported.
[myModule] Initializing for 3 element(s).
[myModule] Initialized, module said: Ready
DOMule provides a module registry for coordinated behavior between modules without tight coupling or global state pollution.
Modules sometimes need to interact:
- A gallery loads images → lightbox needs to know they changed
- Analytics needs to track when video player starts
- Form validation needs to check if CAPTCHA module is ready
Without coordination, you'd resort to:
- Global variables (namespace pollution)
- DOM events (coarse, no type safety)
- Manual initialization order (brittle)
Registry provides:
- Discovery: check if modules are loaded
- Waiting: block until dependencies are ready
- Access: get module exports safely
The api() function:
- Single standardized entry point per module
- Module defines what actions it supports
- Other modules call
moduleA.api('actionName', ...args)
Besides an init() export, a module providing coordination
(gallery.mjs in the example above) simply exports an additional api() function:
export const NAME = 'gallery';
let images = [];
let changeCallbacks = [];
/**
* Initialize module, as per the module standard
*/
export function init(elements) {
images = loadImages(elements);
return `Loaded ${images.length} images`;
}
/**
* Additional (and optional) public API for inter-module coordination
*/
export function api(action, ...args) {
switch (action) {
case 'getImages':
return images;
case 'getCount':
return images.length;
case 'onChange':
// Register callback for changes
changeCallbacks.push(args[0]);
break;
case 'addImage':
images.push(args[0]);
changeCallbacks.forEach(cb => cb(images));
break;
default:
logger.warn(NAME, `Unknown action: ${action}`);
}
}The module that needs to coordinate with this module (lightbox.mjs in the example above) can then simply do:
import {ModuleRegistry} from './core.registry.mjs';
export const NAME = 'lightbox';
export function init(elements) {
// Wait for gallery module (handles async)
ModuleRegistry.waitFor('gallery')
.then(gallery => {
// Guaranteed: gallery exists AND has api()
const images = gallery.api('getImages');
initLightbox(elements, images);
// React to changes
gallery.api('onChange', (newImages) => {
updateLightbox(newImages);
});
})
.catch(error => {
// Timeout, module error, or no api()
logger.warn(NAME, `Gallery unavailable: ${error.message}`);
// Degrade gracefully - work standalone
});
}ModuleRegistry.waitFor(name, timeout)
Waits for module to load and verify it has an api() interface.
Parameters:
name(string) - Module name (from NAME export, or filename if no NAME)timeout(number) - Max wait time in ms (default:30000)
Returns: A Promise that resolves with module exports
Rejects when:
- Module doesn't load within timeout
- Module loads but has no
api()function - Module
init()throws error (module has failed to initialize)
// Promise chain style
ModuleRegistry.waitFor('gallery')
.then(gallery => {
gallery.api('getImages');
})
.catch(error => {
console.warn('Gallery unavailable:', error);
});
// Async/await style
try {
const gallery = await ModuleRegistry.waitFor('gallery', 5000);
const images = gallery.api('getImages');
} catch (error) {
console.warn('Gallery unavailable:', error);
}ModuleRegistry.isLoaded(name)
Check if module is loaded successfully (synchronous):
if (ModuleRegistry.isLoaded('gallery')) {
const gallery = ModuleRegistry.get('gallery');
gallery.api('getImages');
}ModuleRegistry.get(name)
Get module exports (or null if not loaded):
const gallery = ModuleRegistry.get('gallery');
if (gallery && typeof gallery.api === 'function') {
gallery.api('getImages');
}ModuleRegistry.getElements(name)
Get all elements that required this module:
const galleryElements = ModuleRegistry.getElements('gallery');
// → [div.gallery, section.photos, ...]
//alternatively:
ModuleRegistry.getElements('gallery').forEach(el => {
// Do something with each element
});ModuleRegistry.getAll()
Get all registered modules (debug helper).
console.log(ModuleRegistry.getAll());
// → [{name: 'gallery', state: 'loaded', elementCount: 2}, ...]Load module if available, degrade if not:
export function init(elements) {
ModuleRegistry.waitFor('analytics', 2000)
.then(analytics => {
// Track events through analytics
trackWithAnalytics(analytics);
})
.catch(() => {
// Analytics not available, skip tracking
console.log('Running without analytics');
});
}Wait for multiple modules in parallel:
export async function init(elements) {
try {
const [gallery, lightbox] = await Promise.all([
ModuleRegistry.waitFor('gallery'),
ModuleRegistry.waitFor('lightbox')
]);
// Both available
coordinateModules(gallery, lightbox);
} catch (error) {
console.warn('Dependencies missing:', error);
}
}Check without waiting:
export function init(elements) {
// Initialize immediately
setupFeature(elements);
// Check if optional module is present
if (ModuleRegistry.isLoaded('theme')) {
const theme = ModuleRegistry.get('theme');
applyTheme(theme.api('getColors'));
}
}Let other modules react to your events:
// In video-player.mjs
let playCallbacks = [];
export function api(action, ...args) {
switch (action) {
case 'onPlay':
playCallbacks.push(args[0]);
break;
}
}
function handlePlay() {
playCallbacks.forEach(cb => cb({currentTime, duration}));
}
// In analytics.mjs
ModuleRegistry.waitFor('video-player')
.then(player => {
player.api('onPlay', (data) => {
trackEvent('video_play', data);
});
});DOMule works with any ES6 module, not just custom ones:
<!-- Load Bootstrap components -->
<div data-requires="bootstrap/js/src/collapse.js">
<button data-bs-toggle="collapse" data-bs-target="#demo">
Toggle
</button>
<div class="collapse" id="demo">Content</div>
</div>
<!-- Load from CDN -->
<div data-requires="https://cdn.jsdelivr.net/npm/[email protected]/+esm">
<!-- Your content -->
</div>Third-party modules without an init() function simply execute their top-level code.
Version 3.0 introduced a new module naming scheme. All old imports still worked but logged deprecation warnings. As of version 3.1, old imports will no longer work, as the backward-compatible module shims have been removed. This is earlier than originally planned (v4.0) to reduce maintenance overhead.
| v2.x | v3.x | Type |
|---|---|---|
hnl.domscanner.mjs |
core.scanner.mjs |
Core |
hnl.dynamicimports.mjs |
core.loader.mjs |
Core |
hnl.eventhandler.mjs |
core.events.mjs |
Core |
hnl.logger.mjs |
core.log.mjs |
Core |
hnl.helpers.mjs |
util.observe.mjs, util.dom.mjs, etc. |
Utils |
hnl.debounce.mjs |
util.debounce.mjs |
Util |
hnl.colortool.mjs |
util.color.mjs |
Util |
| Old Export | New Export |
|---|---|
import {hnlLogger} |
import {logger} |
import eventHandler |
import events |
import {dynImports} |
import {loadModules} |
| Deprecated (v3.x) | New Name (v3.1+) | Removal |
|---|---|---|
dynImports() |
loadModules() |
v4.0.0 |
The old dynImports() function still works in v3.x with a deprecation warning in debug mode.
Before:
import {hnlLogger} from 'nok-2025-v1/assets/js/modules/modules/hnl.logger.mjs';
import {isVisible} from 'nok-2025-v1/assets/js/modules/modules/hnl.helpers.mjs';
import eventHandler from 'nok-2025-v1/assets/js/modules/modules/hnl.eventhandler.mjs';
hnlLogger.log('Example', 'Message');
eventHandler.docReady(() => {
});After:
import {logger} from './core.log.mjs';
import {isVisible} from './util.observe.mjs';
import events from './core.events.mjs';
import {loadModules} from './core.loader.mjs'; // was {dynImports}, pre v3.1
logger.log('Example', 'Message');
events.docReady(() => {
loadModules(() => {
console.log('All modules loaded');
});
});Function Renames:
dynImports()→loadModules()(deprecated, removed in v4.0)
Backward Compatibility:
// Old code still works (with warning in debug mode)
import {dynImports} from './core.loader.mjs';
dynImports(() => console.log('loaded'));
// New recommended usage
import {loadModules} from './core.loader.mjs';
loadModules(() => console.log('loaded'));- v3.0.0: Old imports work through shims, console warnings appear
- v3.1.0: Old paths removed (breaking change)
- v3.1.0:
dynImports()deprecated,loadModules()introduced - v4.0.0:
dynImports()removed (planned)
Core features require ES6 module support. Optional features degrade gracefully:
- IntersectionObserver (lazy loading) - Chrome 61+, Safari 12.1+
- ResizeObserver (body resize) - Chrome 64+, Safari 13.1+
Check:
- Browser console for errors (enable
?debug=true) - Path is correct (remember
./becomes./../) - Module is valid ES6 with
exportstatements - No syntax errors
Verify:
- Function is named
init(case-sensitive) - Function signature:
export function init(elements){} - Check console for initialization errors
Common causes:
- Element has
display: none;orvisibility: hidden; - Typo: use
data-require-lazy="true"(notdata-requires-lazy- note the superflous 's')
Optimize:
- Use lazy loading for below-fold content
- Minimize number of small modules (bundle related functionality)
- Disable debug mode in production
- Use browser cache
Great question.
DOM + Module + Mule.
- The DOM declares what's needed;
- Modules provide it;
- The mule hauls it — but only what's actually required. Like the animal, it's stubborn about not carrying dead weight.
This is a personal repository for maintaining the module system. Feel free to use it, but note: modules may change without prior notice.
Version: 3.1.0
License: MIT
Author: Klaas Leussink / hnldesign
