anti-destin
anti-destin

Reputation: 869

Vue.js custom directive: $refs is empty

Background

I created a custom directive (to detect clicks outside of an element) by following this guide: https://tahazsh.com/detect-outside-click-in-vue.

Element:

<button
 id='burger'
 ref='burger'
 class='burger button is-white'>
 <span class='burger-top' aria-hidden='true' style='pointer-events: none;'></span>

 <span class='burger-bottom' aria-hidden='true' style='pointer-events: none;'></span>
</button>

Directive:

import Vue from 'vue';

let handleOutsideClick;

Vue.directive('outside', {
  bind(el, binding, vnode) {
    setTimeout(() => {
      handleOutsideClick = (e) => {
        e.stopPropagation();

        const { handler, exclude } = binding.value;

        let clickedOnExcludedEl = false;

        exclude.forEach((refName) => {
          if (!clickedOnExcludedEl) {
            const excludedEl = vnode.context.$refs[refName]
            console.log(vnode.context.$refs);
            clickedOnExcludedEl = excludedEl.contains(e.target);
          }
        });

        if (!el.contains(e.target) && !clickedOnExcludedEl) {
          vnode.context[handler]();
        }
      };
      document.addEventListener('click', handleOutsideClick);
      document.addEventListener('touchstart', handleOutsideClick);
    }, 50);
  },
  unbind() {
    document.removeEventListener('click', handleOutsideClick);
    document.removeEventListener('touchstart', handleOutsideClick);
  },
});

Note that I have had to add setTimeout.

Using the directive in a sidebar component:

<div
  id='sidebar'
  class='sidebar-container'
  v-show='toggleSideBar'
  v-outside='{
    handler: "close",
    exclude: [
      "burger",
    ],
  }'>
  <div class='sidebar-content'>
    // other content
  </div>
</div>

Problem

$refs is empty, even though the element clearly has a ref.

This is clear also from console.log(vnode.context.$refs);.

Notably, getting the element by id works:

        exclude.forEach((id) => {
          if (!clickedOnExcludedEl) {
            const excludedEl = document.getElementById(id);
            clickedOnExcludedEl = excludedEl.contains(e.target);
          }
        });

Question

Why is $refs empty when the element clearly exists and can be accessed by id?

Upvotes: 7

Views: 2445

Answers (2)

Adam Orłowski
Adam Orłowski

Reputation: 4464

It is so, because your ref is not what refName is.

Your ref is "burger" but refName is a exclude property given to v-closable which you then restructure.

In the tutorial you linked to, refName is button. So I'm assuming it is the same in your code.

Make sure both ref="burger" and

v-closable="{ exclude: ['burger'], // this is your refName handler: 'onClose' }"

are the same.

Upvotes: 2

skirtle
skirtle

Reputation: 29092

The way this code is currently written the ref='burger' and v-outside must be in the same template.

The key line is:

const excludedEl = vnode.context.$refs[refName]

vnode.context will refer to the Vue instance for your sidebar component. The $refs must be defined in the template for that component.

$refs are local to a particular Vue instance. By 'Vue instance' I mean the component instance, if that terminology is any clearer. So the sidebar will have one set of $refs and the navbar will have its own set of $refs.

Based on the comment under the question it would seem that the button is currently defined in the template for the navbar component. So you'll end up with a burger ref in the navbar's $refs but the directive is looking for it inside the sidebar's $refs.

The way that directive is written needs a template a bit like this:

<div>
  <div
    v-outside='{
      handler: "close",
      exclude: ["burger"]
    }'
  >
    ...  
  </div>
  <button ref='burger'>...</button>
</div>

Here the v-outside and ref='burger' are both in the same template so everything will work fine. As soon as you move the button to a different template it won't be able to find it.

Using an id and document.getElementById is a very different scenario. Element ids are registered globally. It doesn't matter where the element is in the DOM, so long as it is present it will be found.

Upvotes: 4

Related Questions