Learn JavaScript by building a UI framework: Part 5 - Adding Events To Dom Elements
Tags: dom, events, javascript, ui
Published: 2019-10-11 16:59:41

---
title: Learn JavaScript by building a UI framework: Part 5 - Adding Events To Dom Elements
published: true
description: An example of how frontend frameworks add events to DOM elements
tags: dom, events, javascript, ui
cover_image: https://thepracticaldev.s3.amazonaws.com/i/qh7bs6unloolebo4v2f2.png
---



*This article is the fifth in a series of deep dives into JavaScript. You can view previous articles by visiting the [Github repository](https://github.com/CarlMungazi/aprender-js#what-is-this) associated with this project.*

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

___ 

At this stage in our project we have built a basic UI framework (Aprender), test library (Examinar) and module bundler (Maleta). We have not touched our framework for a while so in this post we will return to it. The most exciting thing Aprender can do is create and render DOM elements, so what more can we make it do? 

Every development tool is built to solve a particular problem and our framework is no different. Its primary purpose is to be an educational tool but for that education to be effective, it needs to happen in the context of *something*. That *something* will be a search application that allows users to choose from a selection of [these](https://github.com/public-apis/public-apis) free public APIs, search for something and then display the results. We will incrementally build functionality which handles this specific use case instead of worrying about our framework meeting the large number of requirements for a production level tool. For example, production standard UI libraries have to handle all the various quirks and requirements of every DOM element. Aprender will only handle the elements needed to create the application. 

The first order of business is to list the user stories for our search app:
- As a user, I can view the search app
- As a user, I can select an API
- As a user, after selecting an API, I can view information explaining the API and what search parameters I can use
- As a user, I can type in the search field and click the search button
- As a user, after clicking the search button I can view the search results
- As a user, I can clear the search results

We will also refactor our demo app to reflect the new goal:

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

const Button = aprender.createElement('button', { 
    attrs: {
      type: 'submit'
    },
    children: ['Search'] 
  }
);
const Search = aprender.createElement('input', { attrs: { type: 'search' }});

const Form = aprender.createElement('form', {
    attrs: { 
      id: 'form',
      onsubmit: (e) => { 
        e.preventDefault(); 
        console.log('I am being submitted..') 
      }
    },
    children: [
      Search,
      Button
    ]
  },
);

const App = aprender.render(Form);

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

The only new addition in the code above is the function assigned to the `onsubmit` property of the form's `attrs` object and it is this functionality we will explore next.

### Events and DOM elements

Adding event handling functionality to DOM elements is straightforward. You grab a reference to an element using a method such as `getElementById()` and then use the `addEventListener` method to set up the function that is called whenever an event is triggered. 

For Aprender's event handling functionality, we will take inspiration from [Mithril](https://mithril.js.org/). In our framework, the `renderElement` function is responsible for attaching attributes to DOM elements, so we will put the event code there:

```js
const EventDictionary = {
  handleEvent (evt) {
    const eventHandler = this[`on${evt.type}`];
    const result = eventHandler.call(evt.currentTarget, evt);

    if (result === false) {
      evt.preventDefault();
      evt.stopPropagation();
    } 
  }
}

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

  for (const [attribute, value] of Object.entries(attrs)) {
    if (attribute[0] === 'o' && attribute[1] === 'n') {
      const events = Object.create(EventDictionary);
      $el.addEventListener(attribute.slice(2), events)
      events[attribute] = value;
    }

    $el.setAttribute(attribute, value);
  }
  for (const child of children) {
    $el.appendChild(render(child));
  }

  return $el;
};
```

We are only interested in registering [`on-event`](https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Event_handlers) handlers. [Mithril](https://github.com/MithrilJS/mithril.js/blob/v2.0.4/render/render.js#L735) and [Preact](https://github.com/preactjs/preact/blob/8.5.2/src/dom/index.js#L96) both screen for these event types by checking if the first two letters of the attribute name begin with 'o' and 'n' respectively. We will do the same. `addEventListener` takes the event name as its first argument and either a function or object as the second argument. Typically, it is written like this:

```js
aDomElement.addEventListener('click,' () => console.log('do something'));
``` 

Like Mithril, we will use an object but its creation will be different. Mithril's source has some [comments](https://github.com/MithrilJS/mithril.js/blob/v2.0.4/render/render.js#L869) which explain their approach and offer great insight into the considerations framework authors make when building their tools. 

First, the event object is created using the `new EventDict()` constructor pattern as opposed to our `Object.create(EventDictionary)` approach. In Mithril, the object created whenever `new EventDict()` is called is prevented from inheriting from `Object.prototype` by this line:

```js
EventDict.prototype = Object.create(null);
```

Mithril maintainer [Isiah Meadows](https://github.com/isiahmeadows) said one of the reasons this was done was to guard against third parties adding properties such as `onsubmit` or `onclick` to `Object.prototype`.  

We are not worried about this so we create an object called `EventDictionary` which implements the [`EventListener`](https://developer.mozilla.org/en-US/docs/Web/API/EventListener) interface. We then use `Object.create` to specify `EventDictionary` as the prototype and create an object which will hold a list of `on-event` handlers for the DOM element in question. Finally, the newly created object is assigned the attribute value. 

After this, whenever an event is triggered on the DOM element in question, the `handleEvent` function on `EventDictionary` will be called and given the [event](https://developer.mozilla.org/en-US/docs/Web/API/Event) object. If the event exists on the event object, it is invoked using `call` and we specify the DOM element as the `this` context and pass the event object as the only argument. If our handler's return value is `false`, the `result === false` clause will stop the browser's default behaviour and also prevent the event from [propagating](https://www.sitepoint.com/event-bubbling-javascript/).

There is an excellent in-depth [post](https://github.com/getify/You-Dont-Know-JS/blob/1st-ed/this%20%26%20object%20prototypes/ch6.md) which explains the differences of the `Object.create` approach over `new Func()` when creating objects. This [Stack Overflow](https://stackoverflow.com/questions/29788181/kyle-simpsons-oloo-pattern-vs-prototype-design-pattern) question also has some interesting thoughts on the two patterns.

### A little bit about events

If we run our application, we should see an input field with a button next to it. Typing some text and clicking the button should log `I am being submitted..` in our console. But if we remember, the first line in our form's `onsubmit` function is:

```js
const Form = aprender.createElement('form', {
    // ...
      onsubmit: (e) => { 
        e.preventDefault(); 
        console.log('I am being submitted..') 
      }
    // ...
  },
);
```

What is `e.preventDefault()` and what does it do? The default behaviour when a form's `onsubmit` handler is called is for its data to be sent to the server and the page to be refreshed. Obviously, this is not always ideal. For one, you might want to validate the data before its sent or you might want to send the data via another method. The [`preventDefault`](https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault) function is a method on the Event object and it tells the browser to prevent the default action. However, if you were to programmatically create a form like this:

```js
const form = document.createElement('form');
form.action = 'https://google.com/search';
form.method = 'GET';

form.innerHTML = '<input name="q" value="JavaScript">';

document.body.append(form);
```

Submitting the form by calling `form.submit()` would not generate the `submit` event and the data would be sent.

The next event we will look at is on our input field. We need to capture the input value so we can use it to make a request to the selected API. We have a few events we can choose from for this: [`oninput`](https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/oninput), [`onblur`]() and [`onchange`](https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/onchange).

The `onblur` event fires when a focused element loses focus. In our case, it would fire only when the user focused away from the input field. The `onchange` event fires when the user changes the value of a form control, like our input field, and then focuses away from it. Finally, `oninput` is fired each time the value changes. This means every keystroke would fire the event. We will use the `oninput` event because it best suits our purposes. `onchange` and likewise `onblur` would be useful if we wanted to validate the input each time the search element lost focus. *Note: if you were like me and did not know much about events when you first started using React, you would have been surprised to know that React's `onchange` event behaves exactly like `oninput`. There is even an [issue](https://github.com/facebook/react/issues/3964) about it*. 

Our final act will be to create a [`select`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select) element for our list of API options and attach an `onchange` event handler to it. And with that, our application code should look like this:

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

const Button = aprender.createElement('button', { 
    attrs: {
      type: 'submit'
    },
    children: ['Search'] 
  }
);

const Search = aprender.createElement('input', { 
  attrs: { 
    type: 'search',
    oninput: (e) => console.log(e.target.value)
  }
});

const Form = aprender.createElement('form', {
    attrs: { 
      id: 'form',
      onsubmit: (e) => { 
        e.preventDefault(); 
        console.log('I am being submitted..')  
      }
    },
    children: [
      Search,
      Button
    ]
  },
);

const Dropdown = aprender.createElement('select', {
  attrs: {
    onchange: (e) => console.log(e.target.value)
  },
  children: [
    aprender.createElement('option', {
      children: ['--Please select an API--']
    }),
    aprender.createElement('option', {
      children: ['API 1']
    }),
    aprender.createElement('option', {
      children: ['API 2']
    })
  ]
});

const SelectAPI = aprender.createElement('div', {
  children: [
    aprender.createElement('h2', { children: ['Select API: ']}),
    Dropdown
  ]
})

const Container = aprender.createElement('div', {
  children: [
    SelectAPI,
    Form
  ]
})

const App = aprender.render(Container);

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

### Summary

We have completed our first user story: 

- **As a user, I can view the search app**

In the next post we will tackle:
 - **As a user, I can select an API**. 
 
This feature will expose us to the [core reason](https://medium.com/dailyjs/the-deepest-reason-why-modern-javascript-frameworks-exist-933b86ebc445) why UI frameworks exist - keeping the user interface in sync with the application state.