const $id = new Proxy({}, {
// get element from cache, or from DOM
get: (tgt, k, r) => (tgt[k] || ((r = document.getElementById(k)) && (tgt[k] = r))),
// prevent programming errors
set: () => $throw(`Attempt to overwrite id cache key!`)
});
It's nice to be able to refer to elements by property name, so:
<div id="thing"></div>
Is reachable with:
$id.thing
And, since the underlying structure is just an object, you can still enumerate it with Object.keys, which can sometimes be a useful debugging aid and general catalog of accessed elements.
Anyways.. Proxy is a wildly underappreciated and used class in JavaScript.
I built a Proxy-based microlib for making fluent REST calls just on a lark a couple years ago. It was never anything production-ready, but it was so handy that I used it in all of my JS projects until I moved away from webdev. The API was basically:
const api = new Proxy({route: baseRoute}, handler); // handler is the microlib's export
const result = await api.get.some.route.invoke(); // GET {baseRoute}/some/route
invoke() is just how it finally fires the call. I didn't feel like spending too much time making it automatic, the benefits just weren't large enough to justify compared to just calling invoke().
The native call does usually include a cache and will use it optimistically; however, the strategies around invalidating this cache are varied and some surprisingly basic DOM actions can trigger it. Then browsers usually fallback to the full DOM query case.
The cache eliminates this variability, which prior to flexbox, could be very useful in highly nested site designs particularly in mobile contexts.
My fault, I tend to "rapid fire" sometimes. Yours was next to another reply on the identical subject and that caused me to mistake the meaning of the comma in your sentence.
Reading it more slowly, I see it now, and, thank you!
A call to `$id.toString` actually yields the native unbound function whether or not there is such an element on the page. So get is not invoked in this case.
const myDiv = div(
{id: 'container', className: 'my-class'},
h1('Hello World'),
p('This is a dynamically generated paragraph.')
);
document.body.appendChild(myDiv);
That's completely unnecessary these days with template strings. It's gonna be much faster as well to use browser's native parsing.
const div = document.createElement('div');
let text = 'This is a dynamically generated paragraph';
div.innerHTML = `
<div id="container" class="my-class">
<h1>Hello world</h1>
<p>${text}</p>
</div>
`;
document.body.append(...div.children);
Generally, I would recommend avoiding directly setting the innerHTML property, since it's vulnerable to XSS and other injection attacks. If you do, make sure you HTML-escape each variable you interpolate.
Here's a way to do that with a tagged template function (named it `htmlFragment` to make it super clear what it's for):
// a couple of functions to help
const sanitizeHTML = (unsafeStr) => {
const div = document.createElement('div');
div.textContent = unsafeStr;
return div.innerHTML;
};
const htmlFragment = (fragments, ...variables) => {
const result = variables.map((variable, i) => fragments[i] + sanitizeHTML(variable));
result.push(fragments[fragments.length-1]);
return result.join('');
};
// updated your code here
const div = document.createElement('div');
let text = 'This is a dynamically generated paragraph';
div.innerHTML = htmlFragment`
<div id="container" class="my-class">
<h1>Hello world</h1>
<p>${text}</p>
</div>
`;
document.body.append(...div.children);
Unfortunately, to my knowledge there isn't yet a close-to-the-metal solution for templating and data binding in HTML/JS, although several proposals are currently being discussed.
One big difference and why I've been experimenting with JSX[1] is that in that example, if the `text` comes from an untrusted source it can lead to XSS, which TinyJS prevents (I checked)!
This is exactly what I was thinking. I'm always trying to have fewer third-party dependencies in my codebase no matter how tiny, especially if it's solving problems that already have platform/system native solutions.
When using TypeScript the types for querySelectorAll are a bit hairy to map but by defining the consts like above the types "just work".
for (const tag of $$<HTMLParagraphElement>("p")) ...
I don't use Array.from in the result of $$ because sometimes creating an Array is not necessary. The NodeList returned can be iterated directly or converted to an array later if really needed:
[...$$("p")].map(p => ...)
Since I use TypeScript I lean on TSX for building HTML. I use preact's render-to-string package to convert it to a string [1].
You didn’t need a library to do this. Just alias document.querySelector and document.querySelectorAll to something shorter. Polluting the global namespace with functions to document.createElement on every possible html tag is not a good idea.
may I ask what's the point of `const tagNames =`? removing it would make no difference, as far as I can tell - just make sure the previous line ends with a semicolon (or add it at the same line as the opening square bracket)
You could make it even smaller by removing all the element names, and just have it be passed in as an argument.
Would also reduce the amount of functions you need to declare, though function count is likely not a problem for modern JS engines.
I remember when we would throw this into our utilities.js scripts back in like 2007. We didn't have querySelector yet, though. We'd also walk to school with no shoes, in the snow, uphill, both ways.
I wrote my first jQuery lite selector clone a bit ago, any feedback would be appreciated, especially since I don't see the need to separate out querySelector & querySelectorAll
Calling those $ and $$ selector helper functions new is a bit disingenuous. They’ve been present as Chrome dev tools shortcuts for years. Let alone JQuery, I’ve also spent years seeing little articles about this neat trick on everything from medium to dev.to.
I recommend the author remove that or note inspiration from the README to avoid detracting from the rest of the work. I realize there’s more here than the traditional document.querySelector.bind(document) assignment.
I recently built a toy Golang SSR project using Zepto, a jQuery-like library, and felt like I was 17 again (**EDIT: it's unmaintained - don't use it). Also of note is that "Highlander II" takes place in the year 2024 [1]. It's a sign! Everything old is new again! `$` is immortal!
Anyways.. Proxy is a wildly underappreciated and used class in JavaScript.