Vue3 Composition API - Creating a dragable element
Tags: vue, vue3, compositionapi
Published: 2019-10-13 05:20:06

---
title: Vue3 Composition API - Creating a dragable element
published: true
description: This article discusses using Composition API functionality (reactive, computed, watch) and event listeners to add drag functionality to an element.
tags: Vue, Vue3, Composition-API
---

# Vue3 Composition API - Take 2

My initial [fumbling around](https://dev.to/cactuslibratus/composition-api-in-vue3-2ob6) with the newly released Vue3 (pre-release version) had not gone well. In short, I've made some silly mistakes and hadn't done nearly enough of reading before starting. Now, after couple more days, I wanted to give an update on my progress in a form of a how-to.

## The Goal:

### Break down dragable component using the Composition API in Vue3

I've chosen to do this, because dragging a component requires the following:

- reactive values
- computed values
- watch
- event listeners

Previously I've done similar things with Higher Order Components or Mixins. Either way, I got it to work, and there is nothing in this release that will unlock functionality that was not available before, but it allows us to do things with better ease and code maintainability.

## The Plan

The idea in this experiment is to separate the dragging functionality out of the component, so that we can call a function and pass the returned values to the template. The component code should look something like this: 

```js
// reusable function
const makeDragable = element => {
  // create reactive object
  const position = reactive({x: 0, y: 0, /*etc...*/ });

  // compute style
  const style = computed(() => {
    // To Be Implemented (TBI)
    return {};
  });

  // create  mouse interaction functions
  const onMouseDown = e => {/* TBI */};
  const onMouseMove = e => {/* TBI */};
  const onMouseUp = e => {/* TBI */};

  // assign mousedown listener
  element.addEventListener("mousedown", onMouseDown);

  // return objects
  return { position, style };
}

// my component
const MyComponent = Vue.createComponent({
  setup() {
    const { position, style } = makeDragable(el);
    return { position, style };
  },
  template: document.getElementById("myComponent").innerHTML
});
```

This shows the beginnings of what this code for the reusable function and the component may look like. The problem is that `el` is not defined, and if we were to define it, it would be null, since the component doesn't get mounted until after setup executes.

The way to deal with this, is to create a reference (`ref`) to a reactive value that the template will render.

```js
const MyComponent = Vue.createComponent({
  setup() {
    // create reactive reference variable el
    const el = ref(null);
    // send el to function to assign mouse listeners
    const { position, style } = makeDragable(el);
    // pass el to template
    return { el, position, style };
  },
  template: document.getElementById("myComponent").innerHTML
});
```

Then we can pass it to the template using (`ref="el"`)

```html
<template id="myComponent">
  <div ref="el" :style="style">
    <h3>DRAG ME</h3>
    <pre>{{ position }}</pre>
  </div>
</template>
```

This will create a reactive reference for variable `el` and initialize it as null and send (return) it for use in the template. The template assigns the reference to the div in the template.
At this point the `el` in the `makeDragable` function changes from `null` to an `HTMLElement`. If we were to assign listeners on first run, it would fail because the element is not mounted and the `el` variable is null. In order to assign the listeners to the element, I used a `watch` that will assign the functionality once the value changes


## The Code

The code uses the vue3 pre-release code current at the time of writing. The steps to generate can be found on the vue3 page on my previous post.
```js
// reusable function
const makeDragable = element => {
  const position = reactive({x: 0, y: 0, /*etc...*/ });

  // compute style
  const style = computed(() => {
    // To Be Implemented (TBI)
    return {};
  });

  const onMouseDown = e => {/* TBI */};
  const onMouseMove = e => {/* TBI */};
  const onMouseUp = e => {/* TBI */};

  // Add a watch to assign the function when it changes, and is an instance of HTMLElement
  watch(element, element => {
    if (!element instanceof HTMLElement) return;
    element.addEventListener("mousedown", onMouseDown);
  }

  // return objects
  return { position, style };
}
```

## Fill in the owl

As far as the Composition API implementation goes, this pretty much finishes it off. The rest is just implementing the mouse interaction which I'm including in the full code at the end. It can also be seen in [this jsFiddle](https://jsfiddle.net/dapo/3czn6txe/)

In this case, I'm using a single component, so the benefit may not be clear. The idea is that I could easily create other components that use this functionality. In [this jsFiddle](https://jsfiddle.net/dapo/8q1n6vj0/) I've split the position and style into separate functions, so that I can create a different style for the svg elements. With minor modifications, I can have a dragable `HTMLElement` or `SVGGraphicsElement`.

## Notes

Here is a list of things I've come across while working on this

- template `ref` and JavaScript `ref` are not the same.
  - the template `ref` allows referencing DOM elements. In Vue2 this would be a string that can be then referenced using `vm.$refs`. The `composition-api` plugin for Vue2 cannot handle it the same way as Vue3 and requires a render function or `jsx`. In Vue3, the concept has been _unified_, so even though the function of the two differs they work together and the ref expects a defined object instead of a string.
- `ref` is like `reactive` but not the same
  - `ref` is a useful for a single property. In this case we're interested in creating a single element for assignment and watching for changes.
  - `reactive` is useful when you have multiple properties, like the position parameters, which are tied together
- `watch` is a lifecycle hook for component fragments
  - use `watch` to handle the equivalent of `updated` and `beforeUnmount`
  - `watch` accepts an `onCleanup` parameter that fires between `beforeUnmount` and `unmounted` of the component
- lifecycle methods seemed to have changed
  - Vue3 currently supports 
    - `beforeMount`
    - `mounted`
    - `beforeUpdate`
    - `updated`
    - `beforeUnmount`
    - `unmounted`
  - The following lifecycle hooks from Vue2 are _currently_ (at the time of writing) not available.
    - `beforeCreate`
    - `created`
    - `activated`
    - `deactivated`
    - `beforeDestroy`
    - `destroyed`
    - `errorCaptured`
- Vue dev tools don't work with Vue3 yet

## Code

It uses a compiled IIFE Vue dependency, that [this article](https://dev.to/cactuslibratus/composition-api-in-vue3-2ob6) shows how I generated


### Template

```html
<div id="app"></div>

<!-- APP Template -->
<template id="appTemplate">
  <!-- one component -->
  <my-component>
    <!-- nested child component -->
    <my-component></my-component>
  </my-component>
</template>

<!-- myComponent Template -->
<template id="myComponent">
  <div ref="el" class="dragable" :style="style">
    <h3>DRAG ME</h3>
    <pre>{{ position }}</pre>
    <pre>{{ style }}</pre>
    <slot></slot>
  </div>
</template>

<style>
.dragable {font-family: "Lucida Sans", Geneva, Verdana, sans-serif;width: 40%;max-width: 90%;min-width: 320px;min-height: 6.5em;margin: 0;color: rgb(6, 19, 29);background-color: rgb(187, 195, 209);border-radius: 16px;padding: 16px;touch-action: none;user-select: none;-webkit-transform: translate(0px, 0px);transform: translate(0px, 0px);transition: transform 0.1s ease-in, box-shadow 0.1s ease-out;border: 1px solid rgb(6, 19, 29);} pre { width: 48%; display: inline-block; overflow: hidden; font-size: 10px; }
</style>
```

### JS

```js
const { reactive, computed, ref, onMounted, watch } = Vue;

const makeDragable = element => {
  const position = reactive({
    init: false,
    x: 0,
    y: 0,
    width: 0,
    height: 0,
    isDragging: false,
    dragStartX: null,
    dragStartY: null
  });

  const style = computed(() => {
    if (position.init) {
      return {
        position: "absolute",
        left: position.x + "px",
        top: position.y + "px",
        width: position.width + "px",
        height: position.height + "px",
        "box-shadow": position.isDragging
          ? "3px 6px 16px rgba(0, 0, 0, 0.15)"
          : "",
        transform: position.isDragging ? "translate(-3px, -6px)" : "",
        cursor: position.isDragging ? "grab" : "pointer"
      };
    }
    return {};
  });

  const onMouseDown = e => {
    let { clientX, clientY } = e;
    position.dragStartX = clientX - position.x;
    position.dragStartY = clientY - position.y;

    position.isDragging = true;

    document.addEventListener("mouseup", onMouseUp);
    document.addEventListener("mousemove", onMouseMove);
  };

  const onMouseMove = e => {
    let { clientX, clientY } = e;
    position.x = clientX - position.dragStartX;
    position.y = clientY - position.dragStartY;
  };

  const onMouseUp = e => {
    let { clientX, clientY } = e;
    position.isDragging = false;
    position.dragStartX = null;
    position.dragStartY = null;
    document.removeEventListener("mouseup", onMouseUp);
    document.removeEventListener("mousemove", onMouseMove);
  };

  watch(element, (element, prevElement, onCleanup) => {
    if (!element instanceof HTMLElement) return;
    let rect = element.getBoundingClientRect(element);

    position.init = true;
    position.x = Math.round(rect.x);
    position.y = Math.round(rect.y);
    position.width = Math.round(rect.width);
    position.height = Math.round(rect.height);

    element.addEventListener("mousedown", onMouseDown);

    onCleanup(() => {
      // do cleanup
    })
  });

  return {
    position,
    style
  };
};

const MyComponent = Vue.createComponent({
  setup(props) {
    const el = ref(null);
    const { position, style } = makeDragable(el);

    return {
      el,
      position,
      style
    };
  },
  template: document.getElementById("myComponent").innerHTML
});

const App = {
  template: document.getElementById("appTemplate").innerHTML
};

const app = Vue.createApp({});
app.component("my-component", MyComponent);
app.mount(App, "#app");
```