Clicking outside the box - Making your Vue app aware of events outside its world
Tags: vue, javascript
Published: 2020-03-19 16:49:23

Often I have created Vue apps that are not an SPA but part of an existing page. The page loads up, the app is injected and all is well. But recently I encountered an issue where the app needed to know when it wasn't the centre of attention anymore. More specifically, when the user was interacting with the page outside the app it needed to change its state.

This app was a search component which contained fields which expanded to display complex selection options. However, these obscured other parts of the form and indeed the page while in open. This is fine while being interacted with but expected behaviour when another part of the page is clicked or tapped - would see the expanded selection options hidden again.

_The app is outlined red here. Clicks outside go unnoticed by the app_
![Diagram of app and page structure](http://jamus.co.uk/assets/blog/outside-the-box-01-19032020.jpg)

So how do we do this? I could not use the _focus_ of the input as this was lost while making a selection. I needed to detect an event outside the app. There are a few packages developed by the Vue Community ([vue-clickaway](https://github.com/simplesmiler/vue-clickaway),
[v-click-outside](https://github.com/ndelvalle/v-click-outside)) but this seemed like something that could be solved without adding another dependancy. In this instance we also planned to rebuild the page entirely in Vue later so wanted something light touch that could be easily removed later.

Essentially, we want to add a listener to the document that the Vue app can listen for. To do this we will use a [Vue Custom Directive](https://vuejs.org/v2/guide/custom-directive.html)

There are a couple of ways to create a Custom Directive in Vue.
Here, we will register it locally on a component - which we will do on our `app.vue`. This way we can place it alongside a method that we want to call, emitting an event whenever the directive detects a click. We can then listen to this event in any of the components that need to close themselves.

```javascript
  name: 'App',
  directives: {
    'click-outside-app': {
      // Directive content that will call the defocusApp method below
    },
  },
  methods: {
    defocusApp() {
      this.$root.$emit('defocusApp'); // emitted event
    },
  },
```

So inside our custom directive we use the `bind` method to add an event listener to the page that detects clicks that are not on (or a child of) the component that is using the directive (`app.vue`).

```javascript
directives: {
    "click-outside-app": {
      bind: function(el, binding) {
        // Define ourClickEventHandler
        const ourClickEventHandler = event => {
          if (!el.contains(event.target) && el !== event.target) {
            // as we are attaching an click event listern to the document (below)
            // ensure the events target is outside the element or a child of it
            binding.value(event); // before binding it
          }
        };
        // attached the handler to the element so we can remove it later easily
        el.__vueClickEventHandler__ = ourClickEventHandler;

        // attaching ourClickEventHandler to a listener on the document here
        document.addEventListener("click", ourClickEventHandler);
      },
      unbind: function(el) {
        // Remove Event Listener
        document.removeEventListener("click", el.__vueClickEventHandler__);
      }
    }
  },
```

For completeness, we also use the `unbind` event to remove the event listener - should the component be removed.

Now the directive is created we can use it on the app element like so.

```html
<div id="app" v-click-outside-app="defocusApp">
    <someChildComponent />
  </div>
```

If you used your Vue developer extension you would see our `defocusApp` event firing on when you click anywhere on the page - outside the app! Now we need to do something from within our components on hearing that event.

Because we want all of our components to do the same thing when the user clicks outside the app (close their input dialog), it made sense to use a mixin that can be included in each of the components. This will, on the `created` lifecycle of those components, bind an event that calls a method on each components using it. In our case a `closeDialogues()` method that sets a commonly named data property to _false_.

_appFocusHandlerMixin.js_
```javascript
export default {
  created() {
    this.$root.$on("defocusApp", this.closeDialogues);
  },
  methods: {
    closeDialogues() {
      this.isDialbogueOpen = false;
    }
  }
};
```

Use the mixin with in any component that need to listen for a click outside the app, adding the the common data element that will be set false.

```javascript
mixins: [appFocusHandler],
  data() {
    return {
      isDialbogueOpen: true
    };
  }
```

Im pretty sure this could be cleaned up or extended to mulitple methods but this seems to fit the bill for my use case. Please use the comments below to offer suggestions. I've created a Codesandbox with a working example.

{% codesandbox clicking-outside-the-box-ufyti %}