tutorial // Mar 03, 2023

How to Dynamically Create and Inject DOM Nodes with JavaScript

How to dynamically create and inject DOM nodes into a page with JavaScript.

How to Dynamically Create and Inject DOM Nodes with JavaScript

For this tutorial, we're going to use CheatCode's full-stack JavaScript framework, Joystick. Joystick brings together a front-end UI framework with a Node.js back-end for building apps.

To begin, we'll want to install Joystick via NPM. Make sure you're using Node.js 16+ before installing to ensure compatibility (give this tutorial a read first if you need to learn how to install Node.js or run multiple versions on your computer):

Terminal

npm i -g @joystick.js/cli

This will install Joystick globally on your computer. Once installed, next, let's create a fresh project:

Terminal

joystick create app

After a few seconds, you will see a message logged out to cd into your new project and run joystick start:

Terminal

cd app && joystick start

After this, your app should be running and we're ready to get started.

Wiring up a simple test component

To demonstrate this idea, we're going to wire up a component using the Joystick framework that we used to create our app above. While the actual code we'll be writing to dynamically create elements can run in any browser, this will help give us a solid test bed to work from. To start, in the project we created above, we're going to replace the file at /ui/pages/index/index.js with the following:

/ui/pages/index/index.js

import ui from '@joystick.js/ui';

const Index = ui.component({
  render: () => {
    return `
      <div>
      </div>
    `;
  },
});

export default Index;

This is a base component in Joystick. Though we won't see anything on screen, this will give us a good foundation to work from for writing our dynamic element creation.

Again: this isn't strictly required for what we'll look at next, we're using Joystick purely for convenience here to make our work easier.

With this, we're ready to dig into the dynamic DOM node creation/injection.

Dynamically creating and injecting DOM nodes

To get this all working, on the component we just created, we want to add an additional option above the render function: an object called lifecycle and on that object, a function called onMount:

/ui/pages/index/index.js

import ui from '@joystick.js/ui';

const Index = ui.component({
  lifecycle: {
    onMount: (component = {}) => {
      // We'll write our dynamic DOM node code here...
    },
  },
  render: () => {
    return `
      <div></div>
    `;
  },
});

export default Index;

A quick what/why: immediately after Joystick renders our component to the screen, it will call the lifecycle.onMount() function we see above. The idea being that, at the point when this is called, we can trust that Joystick has mounted this component in the browser and the DOM is ready.

/ui/pages/index/index.js

import ui from '@joystick.js/ui';

const Index = ui.component({
  lifecycle: {
    onMount: (component = {}) => {
      const div = document.createElement('div');
      const form = document.createElement('form');
      const input = document.createElement('input');
      const button = document.createElement('button');
    },
  },
  render: () => {
    return `
      <div></div>
    `;
  },
});

export default Index;

Creating elements dynamically is fairly straightforward. Above, to show this off we're going to create a fake email newsletter signup form, 100% dynamically.

To start, we create each of the elements we want to add to the screen using the document.createElement() function (this is built-in to JavaScript and is globally available in the browser—no imports are required). Here, notice that all we're doing is passing the name of some HTML element to the .createElement() function as a string. Any valid HTML element will work.

What this achieves is creating the element in memory, meaning, we haven't actually rendered it into the DOM yet. Before we do that, we want to make some modifications to our elements.

/ui/pages/index/index.js

import ui from '@joystick.js/ui';

const Index = ui.component({
  lifecycle: {
    onMount: (component = {}) => {
      const div = document.createElement('div');
      const form = document.createElement('form');
      const input = document.createElement('input');
      const button = document.createElement('button');

      div.id = 'form-wrapper';
      div.style.padding = '40px';
      div.style.background = '#fafafa';

      form.classList.add('email-newsletter');
      
      input.placeholder = 'Type your email address here...';
      input.style.padding = '20px';
      input.style.borderRadius = '3px';
      input.style.border = '1px solid #eee';
      input.style.marginRight = '20px';
      input.style.fontSize = '16px';
      
      button.innerText = 'Join the Newsletter';
      button.style.padding = '20px';
      button.style.border = 'none';
      button.style.fontSize = '16px';
    },
  },
  render: () => {
    return `
      <div></div>
    `;
  },
});

export default Index;

This may seem like a lot but let's consider what's happening. On each of our four elements, in memory, we're modifying the attributes of each element. To do it, when we're working with a DOM node in memory, we use dot notation to modify the DOM node. Why? Well, technically speaking, at this stage our DOM node is nothing more than a special type of JavaScript object. In the same way that we can access and modify properties on a regular JavaScript object, we can do the same with DOM nodes.

https://cheatcode-post-assets.s3.us-east-2.amazonaws.com/RjCYv6THcatJ3UXw/Screen%20Shot%202023-03-03%20at%209.54.29%20AM.png
How a DOM node looks in the browser, in memory.

Here, we're modifying a few different things. The bulk of our code is focused on applying inline styles to our elements (as opposed to authoring separate CSS for each element). To do it, we modify the nested style object on each of our elements (this is universal for DOM nodes). Notice that instead of using the dashed naming convention you might be used to in CSS (e.g., font-size), we use camel case like fontSize. Alternatively, if you do prefer the dashed notation, you can use JavaScript bracket notation to access properties like this input.style['font-size'].

Aside from styles, on each element we're setting some additional, arbitrary properties:

  1. On the div element, we set the id attribute to form-wrapper.
  2. On the form, we add a class to the element's classList via the classList.add() method.
  3. On the input, we add a placeholder to show some "helper" text in the input.
  4. On the button, we set the innerText property to "Join the Newsletter," changing the label of our button.

Again, even though we've made these changes, these elements only exist in memory—they have not been rendered or "drawn" on screen yet.

Let's add that in now:

/ui/pages/index/index.js

import ui from '@joystick.js/ui';

const Index = ui.component({
  lifecycle: {
    onMount: (component = {}) => {
      const div = document.createElement('div');
      const form = document.createElement('form');
      const input = document.createElement('input');
      const button = document.createElement('button');

      div.id = 'form-wrapper';
      div.style.padding = '40px';
      div.style.background = '#fafafa';

      form.classList.add('email-newsletter');
      
      input.placeholder = 'Type your email address here...';
      input.style.padding = '20px';
      input.style.borderRadius = '3px';
      input.style.border = '1px solid #eee';
      input.style.marginRight = '20px';
      input.style.fontSize = '16px';
      
      button.innerText = 'Join the Newsletter';
      button.style.padding = '20px';
      button.style.border = 'none';
      button.style.fontSize = '16px';
      
      div.appendChild(form);
      
      form.appendChild(input);
      form.appendChild(button);

      component.DOMNode.appendChild(div);
    },
  },
  render: () => {
    return `
      <div></div>
    `;
  },
});

export default Index;

Now for the fun part. To actually put our UI on screen, next, we need to "append" or add our DOM nodes to the existing, rendered DOM. To do it, first, we need to construct the DOM that we're going to actually render. By construct, we mean to "compose together" all of our individual elements. Though our intent is to render a form, at this point, none of our elements are connected.

To connect them, first, we start with our "root" element or the element we ultimately want to render on screen, the div. Beneath all of our attribute assignments, we call to div.appendChild(form) to—again, in memory—embed our form inside of our div. Next, because our input and button are children of our form, we call form.appendChild(), appending both of those elements.

Finally, to "draw our DOM on screen," we call to component.DOMNode.appendChild(div) (where div is the element containing all of our other elements).

Here, component.DOMNode is a Joystick convention—this gives us access to our component's DOM node rendered in the browser. This component.DOMNode could be replaced with any DOM node, for example, document.body. We're only using component.DOMNode here for convenience.

/ui/pages/index/index.js

import ui from '@joystick.js/ui';

const Index = ui.component({
  lifecycle: {
    onMount: (component = {}) => {
      const div = document.createElement('div');
      const form = document.createElement('form');
      const input = document.createElement('input');
      const button = document.createElement('button');

      ...

      form.addEventListener('submit', () => {
        window.alert(`Signing up as ${input?.value}!`);
      });
    },
  },
  render: () => {
    return `
      <div></div>
    `;
  },
});

export default Index;

To tie a ribbon around all of this, with our DOM node drawn to the screen, now we want to add an event listener to run some code whenever our form element has its submit event triggered (this automatically happens when any button inside of a <form></form> with no explicit type is clicked). To do it, on our form element we call the .addEventListener() method, passing the name of the event we want to listen for as a string for the first argument, and a function we want to call when that event is triggered as the second argument.

Now, if we open our app in the browser, we should see our form drawn and if we type in an email address and click the button, we should see our alert!

Wrapping up

In this tutorial, we learned how to dynamically create DOM nodes using JavaScript. We learned how to create elements in memory, modifying their properties and attributes first and then learning how to append them to the DOM. Finally, we learned how to attach an event listener to a dynamically created element to make our dynamic DOM interactive.

Written By
Ryan Glover

Ryan Glover

CEO/CTO @ CheatCode