🛠️ Tiny: Add event listeners

Hey ,

I'm thrilled to help you learn JavaScript. Unfortunately, you've landed on a page where you cannot access with your current purchase.

Please upgrade (use this link) access this content.

I'm super eager to help you learn more!

🛠️ Tiny: Add event listeners

After rendering the component, the next thing we need to do is add event listeners. After all, all interactions begin with event listeners.

Normally, we need to add event listeners after we get the element from the DOM.

const element = document.querySelector('some-element')
element.addEventListener('eventName', event => {
  // Do something
})

Unfortunately, Tiny users won’t be able to add event listeners this way. Why? Because they can’t grab the element from the DOM before the component gets rendered.

So we need to create a way for users to add event listeners directly onto elements inside Tiny.

Adding Event Listeners to HTML Elements

There’s a way to add event listeners directly on HTML elements. It uses the following syntax:

<button onevent="callback">Click me</button>
function callback (event) {
  // Do something
}
  • onevent is an on keyword followed by the name of the event you want to listen to. For example, you can use onclick to listen to clicks.
  • callback is the event handler. If you use a function name, this name has to be accessible in the global scope.

This method is rarely used since it requires variables in the global scope – which is a bad practice.

But we can use this syntax to create event listeners inside each Tiny component. In fact, most frameworks actually do something like this.

For example, you can use onClick in a React component.

<button onClick={handleClick}>Click me</button>

Vue does it slightly differently with a v-on: prefix.

<button v-on:click="handleClick">Click me</button>

Adding event listeners

We can do something similar to React and Vue. The problem with copying their style is we need to parse the HTML to get the event name. However, parsing HTML is a challenging task.

The simplest alternative I found is to use a custom data attribute. The downside to using custom data attributes is everything gets converted into strings. But we can live with this problem because we’re building a framework more for educational purposes and not for real-use. I don’t want you to get stuck with parsing HTML. I want you to understand the inner workings of what’s actually going on.

We’ll use a custom data-attribute called tiny-listener. In tiny-listener, we need to tell Tiny two things:

  1. The event name
  2. The event handler

We can do something like this:

<button tiny-listener="[eventName, callback]">Click me</button>

The actual component should look like this:

Tiny({
  // ...
  template () {
    return `
      <div class="component parent-component flow">
        <!-- ... -->
        <button tiny-listener="[click, increaseCount]">Increase count by 1</button>

        <div class="half">
          <div class="component child-component flow">
            <!-- ... -->
            <button tiny-listener="[click, increaseChildCount]">Increase count by 1</button>
            <button tiny-listener="[click, increaseCount]">Increase parent count by 1</button>
          </div>
		      <!-- ... -->
        </div>
      </div>`
  }
})

We want to allow users to create an event handler directly inside the Tiny call. Each event handler is a method. In this case, we have two methods: increaseCount and increaseChildCount

Tiny({
  // ...
  increaseCount (event) {
    console.log('Increasing count')
  },

  increaseChildCount (event) {
    console.log('Increasing child count')
  },
  // ...
})

Adding the event listener via Tiny

We want to add event listeners after the component gets rendered. Let’s start by creating a function called _addEventListeners and call it after _render.

export default function Tiny (options) {
  // ...
  function _addEventListeners () {
    // ...
  }

  _render()
  _addEventListeners()
}

To add event listeners, we need to find all elements that contain the tiny-listener attribute. Each element with this attribute has at least one event listener (but we’re only dealing with one at this point).

export default function Tiny (options) {
  // ...
  function _addEventListeners () {
    const listenerElements = document.querySelectorAll('[tiny-listener]')
    for (const listenerElement of listenerElements) {
      console.log(listenerElement)
    }
  }
  // ...
}

We can find the attribute from each element with getAttribute.

export default function Tiny (options) {
  // ...
  function _addEventListeners () {
    const listenerElements = document.querySelectorAll('[tiny-listener]')
    for (const listenerElement of listenerElements) {
      const attribute = listenerElement.getAttribute('tiny-listener')
      console.log(attribute)
    }
  }
  // ...
}

attribute is a string. We need to extract the event name (click) and the callback (increaseCount or increaseChildCount) into variables so we can use them.

One way we can do this is:

  1. Remove the [ and ] brackets with replace
  2. Split the string at ,
  3. Trim any whitespaces in each value.
export default function Tiny (options) {
  // ...
  function _addEventListeners () {
    const listenerElements = document.querySelectorAll('[tiny-listener]')
    for (const listenerElement of listenerElements) {
      const attribute = listenerElement.getAttribute('tiny-listener')

      const listenerInfo = attribute
        .replace('[', '')
        .replace(']', '')
        .split(',')
        .map(l => l.trim())

      console.log(listenerInfo)
    }
  }
  // ...
}

This gives us an array that contains the event name and the callback

We can then add the event listener.

export default function Tiny (options) {
  // ...
  function _addEventListeners () {
    const listenerElements = document.querySelectorAll('[tiny-listener]')
    for (const listenerElement of listenerElements) {
       // ...
      const [eventName, fn] = listenerInfo
      listenerElement.addEventListener(eventName, options[fn])
    }
  }
  // ...
}

This activates the event handlers we added in the component.