DEV Community

Joe Boris
Joe Boris

Posted on • Updated on

The "Best" Raw JS Approach to the DOM

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.

<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... -->
Enter fullscreen mode Exit fullscreen mode
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;
  };
}
Enter fullscreen mode Exit fullscreen mode

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

<div id="product-section"></div>
Enter fullscreen mode Exit fullscreen mode
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));
Enter fullscreen mode Exit fullscreen mode

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). 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...

newElement(`<input type="text" name="price" />`, { boundTo: product })
Enter fullscreen mode Exit fullscreen mode

...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...

Top comments (6)

Collapse
 
jai_type profile image
Jai Sandhu

This is very clever! I usually find if I've ended up in this situation there's something wrong with my approach, I would rethink and adopt a library for sure, just so a future person seeing the code wouldn't have a tough time understanding. I love that you figured out the second approach, makes the first one feel so hacky

Collapse
 
jdboris profile image
Joe Boris • Edited

Thanks! Yeah I think libraries/frameworks are usually the best approach too. I love React but it's difficult to use it on my phone, which I like to do on the train. Do you have any popular DOM libraries that don't require a bundler to recommend?

EDIT: Like jQuery for example, but I stopped using it since it's going out of style

Collapse
 
jeydotc profile image
Jeysson Guevara

I've explored this subject and I made this small library you might want to give it a look:

github.com/JeyDotC/JustJs

Collapse
 
jdboris profile image
Joe Boris

Thanks! I'll check it out

Collapse
 
blackjyn profile image
ZVHR El Ekhsaan

and... how about server side rendering using "raw" approach?

Collapse
 
jdboris profile image
Joe Boris • Edited

I guess you could some of this approach in server side rendering. You can make reusable components on the back-end. You would need some front-end js for the data binding though. I haven't thought about how to do that with vanilla js, but I think most server side rendering libraries facilitate that, luckily