Appearance
Better DOM API
Convenience wrappers that unify querying, traversal, and event handling across Document, DocumentFragment, and Element. The goal is a smaller, more memorable API surface without pulling in a full library.
When to Use
- Rapid prototyping where jQuery is overkill but raw DOM is verbose
- Projects that want
query/queryAllon every node type - Code that handles space-separated event names or needs automatic listener cleanup
- Code that needs EventEmitter-style
on/off/emiton any EventTarget (WebSocket, BroadcastChannel, custom targets)
The Pattern
Unified query methods
Call query and queryAll on any node type — Document, DocumentFragment, or Element — without checking which prototype supports querySelector:
javascript
const nav = document.query("nav");
const links = nav.queryAll("a");
links.forEach((link) => {
link.classes.add("nav-link");
});Shorthand properties
Use el.text, el.html, el.classes, and node.parent instead of the longer built-in names:
javascript
const heading = document.query("h1");
heading.text = "Welcome";
heading.classes.add("hero");
const container = heading.parent;Attribute helper
Chain attr calls to set multiple attributes without breaking the expression — attr reads with one argument and writes with two:
javascript
Element.create("input")
.attr("type", "email")
.attr("placeholder", "you@example.com");EventEmitter bridge for EventTarget
Shim on, off, emit, and once onto EventTarget.prototype so every event-capable object in the browser gets an EventEmitter-style API:
javascript
// Custom EventTarget
const bus = new EventTarget();
bus.on("update", (e) => console.log(e.detail));
bus.emit("update", { count: 1 });
// One-shot listener
bus.once("ready", (e) => bootstrap(e.detail));
// Works on any EventTarget subclass
const channel = new BroadcastChannel("app");
channel.on("message", (e) => console.log(e.data));
// Cleanup
channel.off("message", handler);Because on and off are direct aliases for addEventListener and removeEventListener, all options still work — capture, signal, { once: true }, etc.:
javascript
button.on("click", handler, { once: true });
controller.signal.on("abort", teardown);Elements, documents, and the window get the richer on/off defined below — those override this bridge because they sit further down the prototype chain than EventTarget.
Event helpers with cleanup
on accepts space-separated event names and returns a removal function. off mirrors the same API. These override the EventTarget bridge above for elements, documents, and the window — adding multi-event binding and automatic cleanup:
javascript
const cleanup = button.on("click touchstart", handleActivate);
// Later, remove both listeners at once
cleanup();Static factory methods
Create elements, text nodes, and fragments without going through document methods:
javascript
const frag = DocumentFragment.create();
const heading = Element.create("h2");
heading.text = "Hello";
frag.append(heading);
document.query("#app").append(frag);Implementation
Implementing unified query methods
Alias each built-in selector method onto all three prototypes so the same names work everywhere:
javascript
Document.prototype.query = Document.prototype.querySelector;
Document.prototype.queryAll = Document.prototype.querySelectorAll;
Document.prototype.find = Document.prototype.querySelectorAll;
DocumentFragment.prototype.query = DocumentFragment.prototype.querySelector;
DocumentFragment.prototype.queryAll =
DocumentFragment.prototype.querySelectorAll;
DocumentFragment.prototype.find = DocumentFragment.prototype.querySelectorAll;
Element.prototype.query = Element.prototype.querySelector;
Element.prototype.queryAll = Element.prototype.querySelectorAll;
Element.prototype.find = Element.prototype.querySelectorAll;Implementing shorthand properties
Define getters and setters on the built-in prototypes:
javascript
// el.classes instead of el.classList
Object.defineProperty(Element.prototype, "classes", {
get() {
return this.classList;
},
});
// el.html instead of el.innerHTML
Object.defineProperty(Element.prototype, "html", {
get() {
return this.innerHTML;
},
set(value) {
this.innerHTML = value;
},
});
// el.text instead of el.innerText
Object.defineProperty(Element.prototype, "text", {
get() {
return this.innerText;
},
set(value) {
this.innerText = value;
},
});
// node.parent instead of node.parentNode
Object.defineProperty(Node.prototype, "parent", {
get() {
return this.parentNode;
},
});Implementing attr()
Return this from the setter path to enable chaining, and alias removeAttr to the built-in removeAttribute:
javascript
Element.prototype.attr = function attr(name, value) {
if (arguments.length === 1) {
return this.getAttribute(name);
}
this.setAttribute(name, value);
return this;
};
Element.prototype.removeAttr = Element.prototype.removeAttribute;Implementing the EventTarget bridge
Alias the verbose built-in names and add emit (via CustomEvent) and once (via the { once: true } option):
javascript
EventTarget.prototype.on = EventTarget.prototype.addEventListener;
EventTarget.prototype.off = EventTarget.prototype.removeEventListener;
EventTarget.prototype.emit = function emit(type, detail) {
return this.dispatchEvent(new CustomEvent(type, { detail }));
};
EventTarget.prototype.once = function once(type, listener) {
this.addEventListener(type, listener, { once: true });
};emit always creates a CustomEvent with a detail payload — fine for custom events, but for synthetic native events like click you still need dispatchEvent(new MouseEvent("click")). Methods like removeAllListeners, listeners, and listenerCount are deliberately excluded — they would require a shadow listener registry, turning a zero-cost alias bridge into a stateful wrapper.
Implementing event helpers
Split space-separated event names and attach a listener for each. Return a function that removes all of them at once:
javascript
Element.prototype.on =
Document.prototype.on =
Window.prototype.on =
function on(events, ...rest) {
const el = this;
events.split(/\s+/g).forEach((event) => {
el.addEventListener(event, ...rest);
});
return function remove() {
el.off(events, ...rest);
};
};
Element.prototype.off =
Document.prototype.off =
Window.prototype.off =
function off(events, ...rest) {
const el = this;
events.split(/\s+/g).forEach((event) => {
el.removeEventListener(event, ...rest);
});
};Implementing static factories
Each class gets a static method that delegates to the corresponding document.create* call:
javascript
Element.create = function (...args) {
return document.createElement(...args);
};
Text.create = function (...args) {
return document.createTextNode(...args);
};
DocumentFragment.create = function () {
return document.createDocumentFragment();
};Trade-offs
| Approach | Pros | Cons |
|---|---|---|
| Better DOM API | Uniform, terse, no deps | Mutates prototypes |
| jQuery | Battle-tested, huge ecosystem | Large bundle |
| Native DOM only | Zero overhead | Verbose, inconsistent API |
Prototype mutation warning. Extending built-in prototypes can conflict with future platform APIs or third-party code. This pattern works best in controlled environments — prototypes, internal tools, and scripts you own end-to-end. Avoid it in shared libraries.