3

Leveraging JS Proxies for the DOM

 3 years ago
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.
neoserver,ios ssh client

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)
Enter fullscreen modeExit fullscreen mode

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)
Enter fullscreen modeExit fullscreen mode

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]
Enter fullscreen modeExit fullscreen mode

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>
*/
Enter fullscreen modeExit fullscreen mode

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
}
Enter fullscreen modeExit fullscreen mode

You could use this function as-is and immediately see some benefits.

h("main", {}, 
    h("h1", {}, "Hello World")
)
Enter fullscreen modeExit fullscreen mode

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)
})
Enter fullscreen modeExit fullscreen mode

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({}, "")
Enter fullscreen modeExit fullscreen mode

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!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK