The "Best" Raw JS Approach to the DOM
Tags: javascript, webdev, beginners, tutorials
Published: 2021-11-17 17:00:46

_TL;DR: Create your elements entirely with JavaScript, rather than hard-coding in your HTML file._

Odds are, interacting with the DOM is the most common thing you'll use JavaScript for on the front-end. There are frameworks and libraries to facilitate this, but sometimes they aren't an option. In this article, I'll demonstrate the best vanilla JS approach to the DOM **in my experience**. I'll show a naïve approach, then a simple demonstration of another approach. The target audience of this article is intermediate-level developers, but I encourage beginners to take this approach, too.

_Disclaimer: "Best" is just my opinion. I welcome criticism, feedback, or questions in the comments._

## Introduction

Let's say you have some data - a list of 5 objects representing products, each with a name, price, and description. Your web app needs to 1) render them, and 2) update them.

_Note: to "render" means to display on the page_

## Naïve approach

A naïve approach would be to hard-code lots of HTML, use JS to search for certain elements, then add the data and event handlers to those elements.

```html
<form class="product">
    <input class="name" type="text"/>
    <input class="price" type="number"/>
    <input class="description" type="number"/>
    <button>Edit</button>
</form>

<!-- Repeat 4 more times... -->
```

```javascript
const products = [
  // 5 product objects...
];

const forms = document.querySelectorAll(".product");

for (let i = 0; i < forms.length; i++) {
  const nameTxt = forms[i].querySelector(".name");
  const priceTxt = forms[i].querySelector(".price");
  const descriptionTxt = forms[i].querySelector(".description");

  nameTxt.value = products[i].name;
  priceTxt.value = products[i].price;
  descriptionTxt.value = products[i].description;

  forms[i].onsubmit = (e) => {
    e.preventDefault();
    products[i].name = nameTxt.value;
    products[i].price = priceTxt.value;
    products[i].description = descriptionTxt.value;
  };
}
```

This is the approach that every beginner tutorial teaches. Sometimes it's sufficient, other times not. Its flaws eventually become problematic, and I found myself addressing them over and over until I took a different approach.

### Flaws

* ❌ Could be done with less code
It's hard to tell as a beginner, but this is very important. It could also be done without repeating code (i.e. the HTML).

* ❌ No data binding
What if you update a product's name somewhere else in your code? The page will still display the old name. This could cause problems.
_Note: to "bind" data means to sync data with the UI. In other words, the user typing in the textbox will immediately update the data, and vice versa._

* ❌ Not reusable
What if you need to render/update a product again on another page? It would take a bit of work to make this code easily reusable. 

* ❌ Naming things is hard
Spending time thinking of the best class and variable names? This approach necessitates that chore.

* ❌ Tight coupling
Struggling to remember the class names from your HTML file? Spending time adapting your JS to work on another page? This approach tightly couples your JS and HTML, worsening these problems.
_Note: tightly coupled code is 2+ pieces of code that are highly dependent on each other to work._

## "Best" approach

```html
<div id="product-section"></div>
```

```javascript
const products = [
  // 5 product objects...
];

function newProductList(products) {
  const list = newElement(`<div></div>`);

  for (let product of products) {
    list.append(newProductForm(product));
  }

  return list;
}

function newProductForm(product) {
  const form = newElement(`<form></form>`);
  form.append(
    newElement(`<input type="text" name="name" />`, { boundTo: product }),
    newElement(`<input type="number" name="price" />`, { boundTo: product }),
    newElement(`<input type="text" name="description" />`, { boundTo: product })
  );

  return form;
}

function newElement(html, options = {}) {
  const template = document.createElement("template");
  template.innerHTML = html.trim();
  const element = template.content.firstChild;

  if (options.boundTo) {
    const object = options.boundTo;
    element.value = object[element.name];
    element.oninput = () => {
      object[element.name] = element.value;
    };
  }

  return element;
}

// Only occurrence of HTML <-> JS coupling
const productSection = document.querySelector("#product-section");
productSection.append(newProductList(products));
```

This approach may be harder to understand at first, but it's worth the investment. Aside from the convenience of `newElement`, the main point is to _identify elements that are coupled to your data, and make them "components" that are created entirely with JS_.

### Explanation
`newElement` is our function, which takes in an HTML string as an **argument** and **returns** a DOM object created from it ([info](https://stackoverflow.com/questions/494143/creating-a-new-dom-element-from-an-html-string-using-built-in-dom-methods-or-pro)). It can also take in an object as a second, optional, **argument**. The object can have a **property** called `boundTo` that's assumed to be an object itself. The function assumes the `boundTo` object has a **property** of the same name as the `name` **attribute** of the element, and binds that **property** to the element. For example...
```javascript
newElement(`<input type="text" name="price" />`, { boundTo: product })
```
...binds the product's price **property** to the textbox.

_Note: It's safe to use the `name` attribute this way, because its traditional purpose is to be the "key" associated with the textbox's value._

### Benefits

* ✔️ Less code
This approach takes less total code, has little repeating code, and automatically scales with the number of products in the array.

* ✔️ Data binding
This approach updates the products as the user types.
_Note: for simplicity, I only demonstrated one-way binding. Two way binding can be added to `newElement` easily._

* ✔️ Reusable components
This approach turns the product list and product form into easily reusable components.

* ✔️ Less naming involved
This approach eliminates the need for classes entirely, and makes some of the temporary middleman variables less necessary.

* ✔️ Loose coupling
The HTML and JS in this approach are much less interdependent. The JS no longer depends on the HTML having tags with so many classes ("product", "name", "price", and "description"). This makes the JS more easily reusable, among other things.

## Conclusion

I faced the problems with the first approach countless times, and ended up patching them different ways every time. I realized this was taking so long that I would actually _save_ time by investing in this approach when I started the project. Now I do it any time I can't use a framework.

Note that the example is simplified for demonstration. You could improve on it with, for example, two-way binding in `newElement`, handling other input types in `newElement`, more "state" parameters and a nested `render` function inside your components, etc...