Vue.js custom directive: $refs is empty
Asked Answered
I

2

7

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?

Isley answered 6/12, 2019 at 11:27 Comment(3)
What error do you have?Bloat
Could you show us the complete tag markup that is taking this directive?Cartouche
Updated my post. The burger is in the navbar. Clicking on it will open/close the sidebar. The point of the directive here is to allow the sidebar to be closed by clicking outside of it.Isley
B
4

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.

Bibby answered 6/12, 2019 at 20:33 Comment(1)
Thanks for the explanation. That makes sense. I guess that I will just continue to use ids in this case.Isley
A
2

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.

Armistead answered 6/12, 2019 at 12:13 Comment(2)
Thanks for the response. Everything is correct, and I have updated my original post to show this. The issue is that $refs is empty (even though it should not be), so .contains fails.Isley
Try switching outside single quoates ' to double ", and double inside to single.Bloat

© 2022 - 2024 — McMap. All rights reserved.