How to hook into the DOM using Vanilla JavaScript!
Tags: javascript, webdev, beginners, programming
Published: 2022-02-26 21:04:08

An element. A very simple element. It's there. Right in the DOM tree. But we want to hook into it. We want to use simple methods to control what it renders. To control when it updates.

If you're a web developer, then you might be familiar with React Hooks. I've also written articles on React Hooks Flow. But this is not about them.

Sure. There's a similarity. They're hooks in React because they let stateless functions use the Class Components abilities like states and lifecycles.

Here we're going to write logic that saves the value or the state of a DOM element and updates it as the state changes. Then this is not about React. But about an interesting way to interact with the DOM. From pure JavaScript!

***

## What are we going to do?

Think of a simple counter app. There are a few elements on the screen to let the user interact with it. 

It displays a big number. Which demonstrates the current count.

You click a button and it increments the number. Clicking another one results in decrement. The third button lets you reset the counter to zero.

We're going to create this app. But we're going to do so in a different way. First, we'll write some helper classes to allow us with hooking into the DOM. Then we're going to use them to construct the app logic.

This is how we're going to use those helper classes:

```js
const count = new StateHook("count", 0);

new RenderHook(() => document.getElementById("counter"))
  .use(count)
  .modify((el) => (el.innerText = `${count.value}`));

document.getElementById("incrementBtn")
  .addEventListener("click", () => count.update(count.value + 1));

document.getElementById("decrementBtn")
  .addEventListener("click", () => count.update(count.value - 1));

document.getElementById("resetBtn")
  .addEventListener("click", () => count.update(0));

```
That's it. Of course, we need to write the HTML part, which is short. And we've to create those helper objects.

This piece of code might seem strange. Even unfamiliar. And that's okay. Because we're going to understand everything step by step.

In the end, you've got a mini helper library that you can extend or use to create new projects.

If you're still in doubt whether this article is for you or not, then let me show you what topics it covers.

***

## What aspects of JS are we going to work with?

* DOM manipulation. A very simple example of it.
* Classes in JS and their different aspects. Like the public and local properties, inheritance, and chaining.
* The EventTarget instance. This is the main part. To be able to replicate the React Hook Flow order, we have to work with events.
* Understanding how React applications look under the hood.

If these seem interesting to you, let's move along.

***

## Creating the project

Only three files. I don't want to waste your time with `npm` and `CSS styling`. Create a file and name it `index.html`. The two other files are scripts. We'll name them: `hooks.js` and `scripts.js`.

Paste the following boilerplate into `index.html`:

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <script src="hooks.js"></script>
    <script src="scripts.js"></script>
    <title>Vanilla Hooks</title>
  </head>
  <body>
    <main>
      <div id="root">
        <div class="counter">
          <div class="counter__number">
            <p class="number" id="counter">Loading...</p>
          </div>
          <div class="counter__actions">
            <button id="incrementBtn" class="actions__button">
              + Increment
            </button>
            <button id="decrementBtn" class="actions__button">
              - Decrement
            </button>
            <button id="resetBtn" class="actions__button">
              0 Reset
            </button>
          </div>
        </div>
      </div>
    </main>
  </body>
</html>
```
This HTML structure creates a `<p>` tag and three `button`s. The `<p>` tag handles displaying the counter's current value and each of the buttons has a different role.

Now let's write some JS code.

## The Hooks
We named one of the hooks files `hooks.js`. This is the file where our app's core logic is going to live. We'll write some helper classes. which are able of listening to events and cause updates in the `DOM` according to these events.

### EventTarget
This is how the Mozilla docs explain `EventTargets` in JavaScript ([read more here](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget)):
>The EventTarget interface is implemented by objects that can receive events and may have listeners for them. In other words, any target of events implements the three methods associated with this interface.

But why do we need to use them?

An `EventTarget` interface allows us to create objects which can dispatch events. This means that in any other part of the code you can attach listeners to the events the `EventTarget` dispatches.

One main parameter in handling `DOM` changes is to register specific values as `state`s. Whenever these values change, the hooks should reflect them in the DOM.

Then let's start with writing a state hook.

### The State Hook
We aim to write a reactive interface for our application. This means that what the hooked elements render in the DOM updates in reaction to changes in our states.

We're going to use `EventTarget`s to write a `State class`. This class will hold the state's current value and handle its updates. When we try to change the state value, the class instance will `dispatch` an `update event`.

We attach an `eventListener` to the `state instance`. And fire callbacks when it dispatches the `update event`. 

Let's write the code:
```js
class StateHook extends EventTarget {
  #_value = null;
  constructor(value) {
    super();
    this.#_value = value;
  }

  get value() {
    return this.#_value;
  }

  set value(newValue) {
    return null;
  }

  update(newValue) {
    this.#_value = newValue;
    const updateEvent = new CustomEvent("update");
    this.dispatchEvent(updateEvent);
  }
}
```

Let's inspect the code line by line. In the first line, we declare a JS class. We use the `extends` keyword to declare that this class inherits from `EventTarget` class.

This way our `State Class`' instances will own the `dispatchEvent` and `addEventListener` methods. We can use them to handle `state change events`.

In the first line inside the class we define a `private instance property` named `_value`. When a variable inside a class' enclosing tags starts with the `#` character it becomes a private property. This means that the only way to assign its value is from inside the class enclosing tags.

This property is the one we use to store the state's latest value after each update. We defined it as a private property because we want it to be immutable like `React states`.

In the next line, we write the class `constructor`. It only takes one argument which we name `value`. This argument is the state's initial value.

We store the initial value in the class's `#_value` property. 

After the constructor we define a `get` and a `set` method for the `#_value` property. We name these methods as `value`, so that's the name we'll use later to access them.

Now we can access the state value by writing `instance.value` instead of  `instace._value`. The setter method returns null and does nothing. So that we can never write `instance._value = x`. Now it's immutable.

And in the end, we define the `update` method for the `state` instance. This method takes an argument which we named `newValue`. We assign this argument's value to the state's private 'value' property.

Then by writing `const updateEvent = new CustomEvent("update")` we create a custom event with the key 'update'. Custom events are like every other event. They take a name from you, and any `Event Target` can dispatch them.

In the last line of this method, we dispatch this event. Now we can attach listeners to the instances of this state. And make changes in the `DOM` using the new state value.

Then let's write the second hook. Which controls what the `DOM` renders, by listening to the state hook.

### The Render Hook

This hook has one simple task. We give it a function by which it can find a specific element. Then we give it specific `state`s which it can listen to their updates. Finally, it gets a function that we call `modifier`.

It calls the modifier the first time the `DOM` is ready and then each time the states' values change. It is the hook's task to keep track of the states and call the modifier when they change.

The modifier is a function that the hook calls every time the state changes. So we can use it to control what the element renders.

This is the how we can write it:
```js
class RenderHook {
  constructor(getElement) {
    this._getElement = getElement;
    this._modifier = null;
    window.addEventListener("load", () => this.render());
  }

  use(state) {
    state.addEventListener("update", (e) => {
      this.render();
    });
    return this;
  }

  modify(modifier) {
    this._modifier = modifier;
    return this;
  }

  render() {
    const theElement = this._getElement();
    if (!theElement) return;
    if (typeof this._modifier === "function") this._modifier(theElement);
}
```

`RenderHook` is a simple class. It doesn't inherit from `EventTarget`. Because we have no need for dispatching events from its instances.

It only takes a function as an argument and assigns its value to the `_getElement` property. Calling this function should return a `DOM Element`.

In the next line, we define the `_modifier` property which has an initial null value. It will hold the modifier function which can be set later using a method.

At the end of the constructor, we add a listener to `window's load event`. The instance's render method will run for the first time as soon as the `DOM` is loaded.

After the constructor, we define a `use` method. It accepts a `state` argument. The argument should be an instance of the `StateHook` class. Then we add a listener to its `update` event. Each time a state updates it calls the instace's `render` method.

At the end of this method, we return `this`. You might wonder why we do so. This way we're returning the current instance. This benefits us while calling this class' methods as we can use `chaining`. 

Chaining is a more declarative way of calling an instance's methods. To see the difference, look at the following example. It tries to add three different states to a `RenderHook` instance:
```js
const counterRender = new RenderHook(() => document.getElementById("counter"));
counterRender.use(counterState);
counterRender.use(timeState);
counterRender.use(styleState);
```

The code can be shorter and more concise by using chaining. Each time we call the `use` method it returns us a `RenderHook` instance. So we can attach each method call to the previous one. Resulting in the following code:
```js
new RenderHook(() => document.getElementById("counter"))
  .use(counterState)
  .use(timeState)
  .use(styleState);
```

Now our code looks clean ;)

Next comes the modify method. It takes a function. And assigns it to the current `instance's` `_modifier` `property`.

And the last method in the line is `render`. It's the base of this concept. It's the promised one. The one who does the final job.

You give it no arguments. Call it and it will proceed to update the `DOM`. To do so it uses what data you have provided using the other methods. 

First it calls the `_getElement` function. Then assigns the returned value to `theElement` variable. Then it checks if `theElement` is not `nullish`. That can happen in case the element has been removed from the `DOM`. 

It calls the `_modifier` function and passes `theElement` to it. And the modifier can proceed to do its job. Which could be updating the `DOM`.

And that's all!

***

## How it works.

Once more let's look at the final code I showed you at the beginning:
```js
const count = new StateHook("count", 0);

new RenderHook(() => document.getElementById("counter"))
  .use(count)
  .modify((el) => (el.innerText = `${count.value}`));

document.getElementById("incrementBtn")
  .addEventListener("click", () => count.update(count.value + 1));

document.getElementById("decrementBtn")
  .addEventListener("click", () => count.update(count.value - 1));

document.getElementById("resetBtn")
  .addEventListener("click", () => count.update(0));

```

Now it shouldn't seem confusing anymore. We define a state using the `StateHook`. Its initial value is 0. Then we create a `RenderHook`. We pass it the function to get the counter text element.

We tell it to use the `counter state` and start listening to its updates. And we give it a `modifier` which it should call each time the counter state is updated.

In the next three lines, we use simple JavaScript. We find the button elements in the `DOM` and attach listeners to them. Clicking the increment button increments the `count state`'s value using its `update` method.

We configure the two other buttons in a similar way.

Every time we call the `state's` update method it dispatches a `Custom Event`. This event's name is `update`. This dispatch invokes our `RenderHook's` render method. And in the end, our `modifier` updates the `text element's innerText`.

The End.

*(Cover photo by Vishal Jadhav on unsplash.)*