Samwise the coder. Defender of Frodo and defender of DRY!

Samwise the Coder: saving the Ring and preventing redundant code since the Third Age!”

Alright, in all seriousness I wanted to actually continue the Lord of the Rings theme throughout the blog post, but I figured it would diverge from the meat and “po-tay-toes” of the post. (last one I swear)

Essentially in Vue, you will learn about the concepts of reusable components. A simple Don’t Repeat Yourself philosophy for sure. They allow you to write code once, and then place that whole component within other areas in your app with a single custom formed tag. Usually this will contain three sections. template, script, and style.

What if we’re looking to just add in some additional logic that we can just reuse regardless of needing a template or not? Well in Vue 2 mixins were a thing sometimes used to do this, but in Vue 3 with composition API mixins have been more so phased out in favour for composables.

If you go to the Vue documentation website, you’ll see a very simple concept that leverages JavaScript to showcase how composables can be used, but of course since Vue promotes TypeScript, we’ll build our examples in TypeScript!

Here’s a very simple component App.vue file we’ll use for the example:

App.vue

<script setup lang="ts">
import ButtonOne from './ButtonOne.vue';
import ButtonTwo from './ButtonTwo.vue';
import { clickListener } from './clickCapture.ts';

const listClicks = clickListener();

</script>

<template>
  <div class="container" style="">
    <ButtonOne class="button" />
    <ButtonTwo class="button"/>
    <button id="ButtonThree" class="button">Button Three</button>
    <p>Click anywhere to begin!</p>
  </div>
  <div>
    <p v-if="listClicks.length > 0"></p>
  </div>
</template>

<style scoped>
  .container {
    width: 600px; 
    background-color: #ccc;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .button {
    margin: 10px;
  }
</style>

Our boring button component (you’ll have to copy and paste it to make the second button.)

<script setup></script>

<template>
  <div>
    <button id="buttonOne">Button One</button>
  </div>
</template>

And then our reusable composables clickCapture.ts

import { Ref, ref, onMounted, onUnmounted } from 'vue';

export function clickListener(): Ref<string[]> {
  const clickHistory = ref<string[]>([]);

  function clickCapture(event: MouseEvent): void {
    clickHistory.value.push(
      event.target instanceof Element
        ? event.target.id || event.target.tagName
        : ''
    );
  }

  onMounted(() => {
    window.addEventListener('click', clickCapture);
  });

  onUnmounted(() => {
    window.removeEventListener('click', clickCapture);
  });

  return clickHistory;
}

Once you get these files up and running you should see something like this:

To test our code we can begin by clicking on any parts of the example. You’ll see the element clicks get captured in a <p> tag below.

So in our clickCapture.ts file we have the following moving parts:

A Ref is a reactive generic type in Vue.js. Here, we use ref<string[]>([]) to create a reactive reference to an empty array that will store our click history.

The clickCapture function is called on every click event, identifying the clicked element and adding it to our click history.

we use onMounted and onUnmounted hooks. On component mount, we add an event listener to the window to capture clicks. On component unmount, we ensure the event listener is removed to avoid any potential chances of a crash due to memory leak.

I am sure you have a couple of questions. Like for instance. is there a reason why const listClicks = clickListener(); isn’t within the onBeforeMount?

const listClicks = clickListener(); is placed outside of the onBeforeMount hook to ensure that listClicks is created once when the component is initialized and is available throughout the component’s lifecycle. If we moved it inside the onBeforeMount hook, it would mean that listClicks is created every time before the component is mounted, and it will not persist between re-renders or updates.

Or perhaps you’re wondering is there a reason why const listClicks = clickListener(); doesn’t have to be within a ref()?

const listClicks = clickListener(); doesn’t need to be wrapped in a ref(). The clickListener function itself already returns a Ref<string[]>. In our clickListener function, we use ref() to create a reactive reference (clickHistory) and return it. Therefore, when we call clickListener(), we are directly getting a reference to the reactive data (clickHistory) without the need for an additional ref() wrapper.

I understand I’ve been doing a slew of explaining and certainly even more typing. So, what I have done is create a vue sandbox with the code involved above so you can test this code right away and see if piques your interest further!

Vue clickCapture Sandbox

As always, I am open to any feedback. If there is something I can do to better explain the code or other ways to refactor my logic to optimize further, by all means please reach out!

See you in the next one!