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... -->
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
<div id="product-section"></div>
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). 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 })
...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 tonewElement
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)
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
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
I've explored this subject and I made this small library you might want to give it a look:
github.com/JeyDotC/JustJs
Thanks! I'll check it out
and... how about server side rendering using "raw" approach?
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