Leveraging JS Proxies for the DOM
source link: https://dev.to/phamn23/leveraging-js-proxies-for-the-dom-3ppm
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
The Problem
A recurring problem for many front-end developers is choosing what framework to use. Maybe your mind skipped to React, or the new star, Vue. Or maybe you're into Ember and Mithril. No one cares about Angular though. We all know it's a bloated relic living somewhere in the Great Pacific Garbage Patch.
It's strange how we always skip over to create-[framework]-app
or another boilerplate template without noticing the extreme amounts of overhead. Relatively simple side or personal projects don't require a framework at all. Choosing the vanilla JS option is considerably more responsible (we're not killing the client's poor Nokia browser with our 10 GB library) and requires no extensive bundler configuration. The browser was built for JavaScript, so use JavaScript.
Frameworks were created to boost productivity, modularize elements into reusable components, provide a novel way of manipulating data, ensure faster rendering through the virtual DOM, and supply a well supported developer toolset. We're missing out on a lot if we pick vanilla. Using native JS APIs is also an absolute nightmare. Who wants to write document.querySelectorAll
50 times?
Regardless, there isn't a need to re-invent the wheel. Although it may seem cool to have a functioning SPA, what you're really doing is writing another hundred lines of code or importing a heavy library with extensive polyfills just to rewrite the JS history API. It's not like the user cares if the url changed without refreshing the page. It's "smooth", but not if the page can't even load because of all of the crap you packed into it. Even Webpack can't save your file sizes now.
Creating Elements
There are several ways to tackle vanilla JS's lack of maintainability and ease of use. You could use this simple function I described in an earlier post on jQuery.
const $ = (query) => document.querySelectorAll(query)
However, querying elements is not the only tool we need as developers. Oftentimes, it's creating the elements that's the problem.
// create a div element
const div = document.createElement("div")
div.classList.add("test")
// create a paragraph element & fill it with "Hello World!"
const p = document.createElement("p")
p.textContent = "Hello World!"
// append nodes to div and then to the body element
div.appendChild(p)
document.body.appendChild(div)
Vanilla JS gets really ugly. Really fast. Feeling the itch to go back to React yet?
Proxies
Here's where the proxies come in. Proxies in JS allow you to "intercept and redefine fundamental operations for that object". As a bonus, it's supported by all the major browsers. Obviously, now that IE is dead, we don't have to worry about it anymore. Kinda like Angular!
I highly recommend reading the first few paragraphs of the MDN docs I linked above.
You can create proxies with the built-in Proxy
class. It takes two arguments: a target object and a handler function that indicates how the target should be manipulated.
I like to think proxies are useful for "listening" to when a property in an object is accessed or changed. For example, you could extend arrays to support negative indexes, similar to Python.
export const allowNegativeIndex = (arr) => new Proxy(arr, {
get(target, prop) {
if (!isNaN(prop)) {
prop = parseInt(prop, 10)
if (prop < 0) {
prop += target.length
}
}
return target[prop]
}
})
allowNegativeIndex([1, 2, 3])[-1]
DOM Manipulation
I randomly stumbled upon this code snippet when I was scrolling through my Twitter feed. I can't explain how genius this is.
Using a proxy to create elements! While this clearly applies to Hyperapp (a "tiny framework for building hypertext applications"), there's no reason why this couldn't apply to vanilla JS.
Imagine writing this instead of document.createElement
.
document.body.appendChild(div({},
h1({ id: "test" }, "Hello World"),
p({}, "This is a paragraph")
))
/*
<div>
<h1 id="test">Hello World</h1>
<p>This is a paragraph</p>
</div>
*/
It doesn't require JSX or a fancy framework, and using functions based on the literal HTML5 tag actually makes a lot of sense.
The Code
You can find a working demo on Codepen and Replit.
First we need to have some logic to easily create elements. I'll call it h
. h
should accept three arguments: an HTML tag, a list of attributes/event listeners that should be applied to the element, and an array of children that should be appended to the element.
const h = (tag, props={}, children=[]) => {
// create the element
const element = document.createElement(tag)
// loop through the props
for(const [key, value] of Object.entries(props)) {
// if the prop starts with "on" then add it is an event listener
// otherwise just set the attribute
if(key.startsWith("on")) {
element.addEventListener(key.substring(2).toLowerCase(), value)
} else {
element.setAttribute(key, value)
}
}
// loop through the children
for(const child of children) {
// if the child is a string then add it as a text node
// otherwise just add it as an element
if(typeof child == "string") {
const text = document.createTextNode(child)
element.appendChild(text)
} else {
element.appendChild(child)
}
}
// return the element
return element
}
You could use this function as-is and immediately see some benefits.
h("main", {},
h("h1", {}, "Hello World")
)
This is much more developer friendly, but we can still make it better with proxies. Let's create a proxy called elements
. Every time we access a property from elements
, we want to return our newly created h
function using the property as the default tag.
const elements = new Proxy({}, {
get: (_, tag) =>
(props, ...children) =>
h(tag, props, children)
})
Now we can write stuff that looks kinda like HTML directly in our vanilla JS. Amazing isn't it?
const { button, div, h1, p } = elements
document.body.appendChild(div({},
h1({ id: "red" }, "Hello World"),
p({ class: "blue" }, "This is a paragraph"),
button({ onclick: () => alert("bruh") }, "click me")
))
// this also works but destructuring is cleaner
// elements.h1({}, "")
State Management
Proxies also have a set
method, meaning you can trigger an action (ie: a re-render) when a variable is changed. Sound familiar? I immediately thought of state management. In a brief attempt to marry proxies with web components, I went on to build a library called stateful components. Proxy-based state (Vue) and "functional" elements (Hyperapp) aren't a new idea. If you're looking for something a little more fleshed out you should give Hyperapp a go. I know this article railed on frameworks a lot, but that doesn't mean I don't recognize their utility and purpose in a given context.
Closing
I hope you enjoyed this short article. A lot of thanks to Matej Fandl for discovering this awesome hack, and I look forward to seeing what you build with proxies!
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK