Learn JavaScript by building a UI framework: Part 3 - Rendering & testing DOM elements
Tags: dom, testing, html, javascript
Published: 2019-07-18 11:26:28

---
title: Learn JavaScript by building a UI framework: Part 3 - Rendering & testing DOM elements
published: true
description: 
tags: DOM, testing, html, javascript
---

*This article is the third in a series of deep dives into JavaScript. You can view previous articles [here](https://dev.to/carlmungazi/learning-javascript-by-building-a-ui-framework-from-scratch-1767) and [here](https://dev.to/carlmungazi/learn-js-by-building-a-ui-framework-part-2-testing-3pff).* 

*This series does not comprehensively cover every JavaScript feature. Instead, features are covered as they crop up in solutions to various problems. Also, every post is based on tutorials and open source libraries produced by other developers, so like you, I too am also learning new things with each article.*

___ 


Last time out we wrote a testing library for our framework. In today's post, we will add more functionality to the framework which will turn this:

```js
const aprender = require('../src/aprender');

const button = aprender.createElement('button', { children: ['Click Me!'] });
const component = aprender.createElement(
  'div', 
  {
    attrs: { id: 'app'},
    children: [
      'Hello, world!',
      button
    ]
  },
);

const app = aprender.render(component);

aprender.mount(app, document.getElementById('app'));
```

into this:

![](https://thepracticaldev.s3.amazonaws.com/i/6bny85qhusdrptfhqne4.png)

We will also new tests:

```js
let element;
let $root;
let app;

beforeAll(() => {
  element = createElement('div', {
    children: [
      createElement('h1', { children: ['Hello, World'] }),
      createElement('button', { children: ['Click Me!'] }),
    ]
  });

  createMockDom();

  $root = document.createElement("div");
  $root.setAttribute('id', 'root');
  document.body.appendChild($root);

  app = render(element);
});


check('it creates DOM elements', () => {
  assert.isDomElement( app );
});

check('it mounts DOM elements', () => {
  mount(app, document.getElementById('root'));

  assert.isMounted(app, $root);
});
```

### From JavaScript object to DOM element

Let us begin by reminding ourselves of the current file structure: 

```
- aprender
  - src
    - createElement.js
  - tests
    - index.js
- examinar
  - node_modules
    - colors
  - package.json
  - package-lock.json
  - src
    - assertions
      - deep-equal.js
      - index.js
    - index.js
```

Aprender is the name of our framework and it houses the `createElement` function and its associated test. Examinar is our testing framework and it has a node modules folder containing the `colors` package. In the assertions folder, the object equality utility function `deep-equal` sits in its own file. The index file contains the assertions `isDeeplyEqual` and `throws`. The first thing we will do is create a folder called `demo` in `aprender` so we can build the example application that prints `Hello, World` to the browser. In the `demo` folder we create an `index.html` and `index.js` file: 

```html
<html>
  <head>
    <title>Hello, World</title>
  </head>
  <body>
    <div id="app"></div>
    <script src="./index.js"></script>
  </body>
</html>
```

```js
const aprender = require('../src/aprender');

const button = aprender.createElement('button', { children: ['Click Me!'] });
const component = aprender.createElement(
  'div', 
  {
    attrs: { id: 'app'},
    children: [
      'Hello, world!',
      button
    ]
  },
);

const app = aprender.render(component);

aprender.mount(app, document.getElementById('app'));
```

The `index.js` file contains two new methods: `render` and `mount`. `render` turns the JavaScript object assigned to `component` into a DOM element whilst `mount` appends the newly created element to `<div id="app"></div>`. The code for `render` is:

```js
function renderElement({ type, attrs, children }) {
  const $el = document.createElement(type);

  for (const [attribute, value] of Object.entries(attrs)) {
    $el.setAttribute(attribute, value);
  }
  for (const child of children) {
    $el.appendChild(render(child));
  }

  return $el;
};

function render(vNode) {
  if (typeof vNode === 'string') {
    return document.createTextNode(vNode);
  }

  return renderElement(vNode);
};
```

Since `render` can be called recursively via `renderElement`, it needs to first check if it is dealing with a string. If it is, we create a text node and return it. Otherwise, we return the result of calling `renderElement` with our virtual dom object. `renderElement` then loops through the `attrs` object, sets any attributes it finds and then works on any children present in the `children` array. The `mount` function is more straightforward:

```js
function mount($app, $root) {
  return $root.appendChild($app);
}
```

If we were building React, for example, `mount`'s equivalent would be [`ReactDOM.render`](https://reactjs.org/docs/react-dom.html#render). Like `ReactDOM.render`, `mount` takes the element we want to add to the DOM as the first argument and then appends it to the element specified as the second argument. Of course, React does [a lot more](https://dev.to/carlmungazi/a-journey-through-reactdom-render-302c) than that but between `createElement`, `render` and `mount`, you have the basic workflow used by most UI frameworks to create DOM elements. The differences lie in all the things they do in between to manage events, keep track of state and perform updates.  

Aprender is not a framework destined for production but if it were, we would need to ensure that it correctly creates and appends DOM elements. This testing will happen in node, which has no DOM, so what can we do? We could:
- Use [`jsdom`](https://github.com/jsdom/jsdom). Jest and Enzyme, two popular testing tools, use it
- Stub DOM objects as shown [here](https://codeutopia.net/blog/2016/05/23/sinon-js-quick-tip-how-to-stubmock-complex-objects-such-as-dom-objects/)
- Use a [headless browser](https://github.com/dhamaniasad/HeadlessBrowsers)

All of these are good options but for us, using jsdom or a headless browser is overkill. Stubbing DOM objects does not fully meet our requirements because we want to simulate the process of creating a virtual dom object, turning it into a DOM element and then adding it to the DOM tree. Fortunately for us, the framework Mithril.js has already [tackled](https://github.com/MithrilJS/mithril.js/blob/next/test-utils/domMock.js) this problem. The maintainers have mocked the DOM in node as part of their testing suite. So we will model our own DOM on their implementation but adapt it to suit our needs. Those needs are expressed in the following tests:

```js
group('aprender', () => {
  let element;
  let $root;
  let app;

  beforeAll(() => {
    element = createElement('div', {
      children: [
        createElement('h1', { children: ['Hello, World'] }),
        createElement('button', { children: ['Click Me!'] }),
      ]
    });

    createMockDom();

    $root = document.createElement("div");
    $root.setAttribute('id', 'root');
    document.body.appendChild($root);

    app = render(element);
  });

  check('it creates DOM elements', () => {
    assert.isDomElement(app);
  });

  check('it mounts DOM elements', () => {
    mount(app, document.getElementById('root'));

    assert.isMounted(app, $root);
  });
});
```

`beforeAll` is a hook which takes a function as its argument and then calls it. The function we provided creates our virtual dom object and then initialises our mock DOM via `createMockDom()`. 

### What is the DOM?

According to MDN Web Docs, *"The Document Object Model (DOM) is the data representation of the objects that comprise the structure and content of a document on the web*". Our new tests dictate that we need the `createElement`, `appendChild`, `setAttribute`, `createTextNode` and `getElementById` methods, so the skeleton of our mock DOM object looks like this: 

```js
const document = {
  createElement() {
    appendChild,
    setAttribute
  },
  createTextNode() {},
  getElementById() {}
}

function appendChild() {}
function setAttribute() {}
```

We will begin by fleshing out the `createElement` method:

```js
createElement(tag) {
  return {
    nodeType: 1,
    nodeName: tag.toUpperCase(),
    parentNode: null,
    childNodes: [],
    appendChild,
    setAttribute,
    attributes: {},
    $$dom: true
  }
}
```

It returns an object which represents a DOM element. Real DOM elements contain more properties than listed above but we are only implementing the ones we need. The `$$dom` property is our own creation and we will look at why we need it when we cover the new test assertions `isMounted` and `isDomElement`. The next thing we will do is add the functionality for setting attributes:

```js
function setAttribute(name, value) {
  this.attributes[name] = value;
}
```

And appending children:

```js
function appendChild(child) {
  let ancestor = this;

  if (ancestor === child) throw new Error("Child element cannot be equal to parent element");
  if (child.nodeType == null) throw new Error("The child is not a DOM element");

  const index = this.childNodes.indexOf(child);
  if (index > -1 ) this.childNodes.splice(index, 1);

  this.childNodes.push(child);
}
```

The most interesting part of this method is that if the child element already exists in the `childNodes` array, we remove it and re-insert it at the end of the array. This behaviour mimics what happens in the browser if you append a child which already exists on the target element. 

Next we move on to the `createTextNode` method:

```js
createTextNode(text) {
  return {
    nodeType: 3,
    nodeName: "#text",
    parentNode: null,
    data: text
  }
}
```

There is an interesting [Stack Overflow](https://stackoverflow.com/questions/12286975/when-working-with-text-nodes-should-i-use-the-data-nodevalue-textcontent) answer explaining the differences between the `data` property we are using and the other properties which exist on text nodes and can also hold the text value.

After `createTextNode`, we come to the `getElementById` function. There is [no](https://stackoverflow.com/questions/2711303/javascript-getelementbyid-lookups-hash-map-or-recursive-tree-traversal) definitive answer on how different browsers have implemented this particular method but from reading the [HTML specification](https://dom.spec.whatwg.org/#dom-nonelementparentnode-getelementbyid), we can see that traversing the DOM tree is a viable option. Our DOM tree will not be large enough to need traversing so we will opt for the simpler option of creating a new property called `_elementIds` on our `document` object and assigning it an empty object. We will populate this object in the `setAttribute` method every time an id is set: 

```js
function setAttribute(name, value) {
  this.attributes[name] = value;
  
  if (name === 'id') {
    if (document._elementIds[value]) {
      throw new Error(`${value} is already the id of an existing element`);
    }
    document._elementIds[value] = this;
  }
}
```

When called, `getElementById` will return the element if its id exits in `_elementIds`. 

```js
getElementById(id) {
  if (document._elementIds[id]) {
    return document._elementIds[id]
  }

  return null;
}
```

### Testing the DOM

Our first new assertion `isDomElement` tries to answer the following question - **how can you tell if something is a DOM element?** The answer is not [straightforward](https://stackoverflow.com/questions/384286/javascript-isdom-how-do-you-check-if-a-javascript-object-is-a-dom-object). We can attempt an answer by taking inspiration from how React [adds](https://overreacted.io/why-do-react-elements-have-typeof-property/) a `$$typeof` property to React elements to help distinguish them from anything else not created by React. We will appropriate this by creating a property called `$$dom` on our `document` object and assigning it the value `true`. Then we write `isDomElement`:

```js
isDomElement(element) {
  if (element.hasOwnProperty("$$dom") && element.$$dom) return true;

  throw new Error('The supplied element is not a DOM element')
}
```

Our implementations of `$$dom` and `_elementIds` are not the best ways of adding what is effectively meta data to our `document` object. For one, we could have used [Symbols](https://www.keithcirkel.co.uk/metaprogramming-in-es6-symbols/) to ensure those properties do not show up via iteration methods such as `Object.keys` or `for..of`. But even then, symbols can still be found through the [`Reflect.ownKeys`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/ownKeys) or [`Object.getOwnPropertySymbols`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertySymbols) methods so that solution is not perfect. For `getElementById` we could traverse the DOM tree and find the element that way. Thankfully, our needs at this stage are relatively simple. 

The second assertion, `isMounted`, tests the results of calling the `mount` method. As mentioned earlier, `mount` plays a similar role to `ReactDOM.render`, so all we need to do is check that our specified DOM element is the only child of the `<div id="app"></div>` element we created in the `index.html` file. 

```js
isMounted(element, parentElement) {
  if (parentElement.childNodes.length > 1) throw new Error('The root element has more than one child');

  if (parentElement.childNodes[0] === element) return true;

  throw new Error('The supplied element has not been mounted');
}
```

All the new tests are predicated on the existence of a DOM like structure in the testing environment. This is handled by the `createMockDom` method, which first creates the [root element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/html) that is supposed to exist on every non-empty HTML document. It then adds a `head` and `body` element as children. Strictly speaking, the `html` element should also exist in a `childNodes` array on the `document` object but we can skip this in our implementation. We then finish by adding our `document` object to the node `global` variable.

```js
function createMockDom() {
  document.documentElement = document.createElement("html");
  document.documentElement.appendChild(document.createElement("head"));
  document.body = document.createElement("body");
  document.documentElement.appendChild(document.body);

  global.document = document;
}
```

### Summary

We now have a framework which creates and renders DOM elements. Or do we? Our tests pass but how can we view our application in the browser? We will cover that in the next article by building a module bundler and setting up our development environment. In the meantime, you can explore the code we have written so far [here](https://github.com/CarlMungazi/aprender-js). 

#### Resources
- [Creating a virtual DOM tutorial](https://dev.to/ycmjason/building-a-simple-virtual-dom-from-scratch-3d05)
- [Mithril's DOM implementation](https://github.com/MithrilJS/mithril.js/blob/next/test-utils/domMock.js)