It's OK to break out of the Vue paradigm (sometimes)

andi23rosca

Andi Rosca

Posted on September 26, 2020

It's OK to break out of the Vue paradigm (sometimes)

The Vue way™ to do things is great.

The pipeline Data->Template->DOM works remarkably well.
You specify the data and the template and let Vue do the grunt work of connecting them together into the real DOM.

There are times however, when abiding by this clean paradigm can make implementing a certain feature feel like an uphill battle, maybe even impossible.

It's important to know when it makes sense to break out of the Vue loop and sidestep the issues with some plain JavaScript.

"Global" DOM behavior

The number one biggest problem you will run into that can't usually be solved the Vue way is when you need to access some type of DOM functionality that doesn't fit into Vue's component based parent/child paradigm.

Imagine the following scenario: You're implementing a panel that can have it's width resized by dragging a handle.

The "naive" way to do it is to use the Vue v-on directive to listen for mouse/touch events.

<div class="relative inline-block">
  <div :style="{ width: `${width}px` }" ref="panel"></div>
  <button
    class="handle"
    @mousedown="startDrag"
    @mousemove="drag"
    @mouseup="stopDrag"
    @touchstart.prevent="startDrag"
    @touchmove.prevent="drag"
    @touchend.prevent="stopDrag"
  />
</div>
Enter fullscreen mode Exit fullscreen mode
export default {
  data() {
    return {
      width: 300,
      offset: 0,
      dragging: false,
    };
  },
  methods: {
    startDrag() {
      const { left, height } = this.$refs.panel.getBoundingClientRect();
      this.offset = left;
      this.height = height;
      this.dragging = true;
    },
    drag(e) {
      if (this.dragging) {
        if (e.touches) this.width = e.touches[0].clientX - this.offset;
        else this.width = e.clientX - this.offset;
      }
    },
    stopDrag() {
      this.dragging = false;
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

I say naive because with this approach, if you drag the mouse around too quickly the pointer will leave the handle and then the listeners attached to it will stop triggering.

Head onto the original post to try out the interactive component.

To combat this problem what you would need to do is to register the mousemove and mouseup events on the document itself, so that even when the pointer goes out of the handle, the listeners will still trigger. And since accessing the document object randomly from any component is not Vue's concern, you just have to do it in regular JS.

<div class="relative inline-block">
  <div :style="{ width: `${width}px` }" ref="panel"></div>
  <button
    class="handle"
    @mousedown="startDrag(true)"
    @touchstart.prevent="startDrag(false)"
    @touchmove.prevent="drag"
  />
</div>
Enter fullscreen mode Exit fullscreen mode
export default {
  data() {
    return {
      width: 300,
      offset: 0,
      dragging: false,
    };
  },
  methods: {
    startDrag(mouse) {
      const { left, height } = this.$refs.panel.getBoundingClientRect();
      this.offset = left;
      this.height = height;
      if (mouse) {
        document.addEventListener("mouseup", this.drag);
        document.addEventListener("mousemove", this.stopDrag);
      }
    },
    drag(e) {
      if (e.touches) this.width = e.touches[0].clientX - this.offset;
      else this.width = e.clientX - this.offset;
    },
    stopDrag() {
      document.removeEventListener("mouseup", this.drag);
      document.removeEventListener("mousemove", this.stopDrag);
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

The above example might seem trivial, we're just adding some event listeners ourselves. But the point is that even in situations that are not as obvious as this, you should ask yourself if you're fighting Vue needlessly when you could just call the DOM APIs yourself.

Not only event listeners

Though the example focused on event listeners, there's other(less common) types of global DOM functionality you might run into, that will require you to side-step Vue.

If you had to make a notifications component, you would probably need to append your HTML to the end of the body tag to ensure it will always stay on top of the other content. Same goes for modals (tough with Vue 3 this case is mitigated through the use of teleport).

Working with the Canvas or WebGL will also require you to make the connection between data and rendering yourself.

And finally, wrapping third party libraries also requires you to manually connect the reactive data to the relevant properties and function calls in the library, since many packages need to have control over the rendering.

💖 💪 🙅 🚩
andi23rosca
Andi Rosca

Posted on September 26, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related