Vue3 Composition API - Take 2
My initial fumbling around 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:
// 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.
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"
)
<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.
// 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
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 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 JavaScriptref
are not the same.- the template
ref
allows referencing DOM elements. In Vue2 this would be a string that can be then referenced usingvm.$refs
. Thecomposition-api
plugin for Vue2 cannot handle it the same way as Vue3 and requires a render function orjsx
. 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.
- the template
-
ref
is likereactive
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 ofupdated
andbeforeUnmount
-
watch
accepts anonCleanup
parameter that fires betweenbeforeUnmount
andunmounted
of the component
- use
- 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 shows how I generated
Template
<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
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");
Top comments (3)
thanks man, will be trying to make it responsiveish
Does this work?
it does for me, currently trying to make it responsive... found out that setting the position.x on the watch function allows me to set an offset-like initial poisition