Skip to content

Instantly share code, notes, and snippets.

@sukima
Last active March 14, 2022 03:13
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sukima/5b3b889f31a2d0bef041b8b203c55937 to your computer and use it in GitHub Desktop.
Save sukima/5b3b889f31a2d0bef041b8b203c55937 to your computer and use it in GitHub Desktop.
Very small DOM micro-lib using Proxy
/*******************************************/
/* Version 1.0.0 */
/* License MIT */
/* Copyright (C) 2022 Devin Weaver */
/* https://tritarget.org/cdn/simple-dom.js */
/*******************************************/
/**
* This micro lib is a compact and simple method to interact with the DOM. It
* facilitates the query mechanisms that querySelector and querySelectorAll use
* but in a compact fluent API. It also makes adding (and removing) events
* easy.
*
* Tests: https://tritarget.org/cdn/tests/simple-dom-test.js
*
* ## Query single element
*
* Queries are performed by property lookups. It will use querySelector()
* first. If that doesn't find anything it will default to getElementById().
*
* Calling methods/properties on the Element will be proxied. If however you
* need access to the actual Element use `.element`.
*
* @example
* ```js
* import $ from 'https://tritarget.org/cdn/simple-dom.js';
*
* // By ID
* $.anElementId.dataset.foo;
* $['#anElementId']dataset.foo;
*
* // By CSS selection
* $['.foobar'].classList.add('list-item');
* $['ul li'].classList.add('first-list-item');
* $['button[data-action]'].on.click(() => console.count('button action'));
* doSomethingWithRawElement($.foobar.element);
* ```
*
* ## Query many elements
*
* Selecting multiple can be opt-in with the `.all` property.
*
* Calling methods/properties on the NodeList will be proxied. If however you
* need access to the actual NodeList use `.elements`.
*
* @example
* ```js
* $.all['ul li'].classList.add('list-item');
* $.all['button'].on.click(() => { … });
* doSomethingWithRawNodeList($.all['.foobar'].elements);
* ```
*
* ## Events
*
* Events can be attach with the `.on` property followed by the event name as
* a function with the callback passed in. When attaching events it will return
* a teardown function.
*
* @example
* ```js
* let off = $.on.keyup(() => { … });
* off();
*
* $.button.on.click(() => { … });
* $.all['.btn'].on.customEvent(() => { … });
* ```
*/
const proxies = new WeakSet();
function attachEvents(el, eventNames, fn, options) {
eventNames.forEach((e) => el.addEventListener(e, fn, options));
return () =>
eventNames.forEach((e) => el.removeEventListener(e, fn, options));
}
function eventable(...elements) {
return new Proxy({}, {
get(_, prop) {
let eventNames = prop.split(',');
return (fn, options) => {
let detachables = elements.map(
e => attachEvents(e, eventNames, fn, options)
);
return () => detachables.forEach(i => i());
};
},
});
}
function wrapper(fn) {
return subject => {
if (proxies.has(subject)) { return subject; }
let result = fn(subject);
proxies.add(result);
return result;
};
}
function domAll(element) {
const queryWrap = wrapper(prop => {
return new Proxy([...element.querySelectorAll(prop)].map(dom), {
get(target, prop) {
switch (prop) {
case 'elements': return target.map(i => i.element);
case 'on': return eventable(...target.map(i => i.element));
}
return Reflect.get(target, prop);
},
set(target, prop, value) {
return Reflect.set(target, prop, value);
},
});
});
return wrapper(() => new Proxy(queryWrap, {
get(_, prop) {
return queryWrap(prop);
},
}))();
}
function dom(element) {
const queryWrap = wrapper(prop => {
return prop instanceof Node
? dom(prop)
: dom(element.querySelector(prop) ?? document.getElementById(prop));
});
return wrapper(() => new Proxy(queryWrap, {
get(_, prop) {
switch (prop) {
case 'element': return element;
case 'all': return domAll(element);
case 'on':
return eventable(element === document ? document.body : element);
}
if (Reflect.has(element, prop)) {
let thing = Reflect.get(element, prop);
return typeof thing === 'function'
? (...args) => thing.call(element, ...args)
: thing;
}
return queryWrap(prop);
},
set(_, prop, value) {
return Reflect.set(element, prop, value);
},
}))();
}
export default dom(document);
@sukima
Copy link
Author

sukima commented Mar 12, 2022

Example usage

import $ from 'https://tritarget.org/cdn/simple-dom.js';

$['#menu-open'].on.click(() => app.trigger('OPEN_MENU'));
$['#menu-close'].on.click(() => app.trigger('CLOSE_MENU'));

let elem = $.createElement('div');
$.body.appendChild(elem);

doSomethingWithDomNode($['.select-by-class'].element);

$['#my-list'].all['.list-items'].elements.forEach(doSomethingWithDomNode);

let teardownAllButtonEvents = $.all.button.on.click(({ target }) => console.log(`clicked ${target.name}`));
teardownAllButtonEvents();

let teardownKeyboardTrap = $.on.keyup(event => );
teardownKeyboardTrap();

$['.open'].classList.remove('open');

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment