Setup

To get started, we will scaffold a new project using the vue-cli . If you haven’t installed in, run npm install vue-cli -g . Then create a new project using vue init webpack-simple dragger .

After creating the app, inside of src/App.vue , delete the existing markup and create add the following:

<template>

<div id="outer">

<div id="dragger" v-drag></div>

</div>

</template> <script>

import drag from './drag' export default {

name: 'app' directives: {

drag

}

}

</script> <style>

body {

height: 100vh;

} #outer {

height: 100%;

} #dragger {

position: absolute;

border: 1px solid red;

width: 100px;

height: 100px;

}

</style>

Our directive will live in drag.js . Go ahead and create that on the same level as App.vue , and add the following:

import Vue from 'vue' export default Vue.directive('drag', {

inserted: function (el, binding, vnode) {

console.log('Drag.js')

}

})

Start the app by running npm run dev , and visit localhost:8080 . You should see “Drag.js” in the console:

Drag.js directive.

Note the inserted function above. Vue directives have several hooks that are exposed to developers. We will be using inserted , which is called when the element is mounted (like the mounted hook components expose). We also will use unbind , which is called when the directive is unbound, when the component is removed from the DOM.

Detect when the element is clicked

The next the step will be detecting when the user clicks on the element with v-drag . When the user clicks the element, we will set a down boolean to true , and the opposite when the user lets go. We will also save the x and y coordinates to calculate the position of the element as we move it.

Add the following to drag.js :

const data = {

down: false,

initialX: 0,

initialY: 0

} export function mousedown (e, el, _data) {

_data.down = true

_data.initialX = e.clientX

_data.initialY = e.clientY

} export function mouseup (e, el, _data) {

_data.down = false

} export default Vue.directive('drag', {

inserted: function (el, binding, vnode) {

el.addEventListener('mouseup', (e) => mouseup(e, el))

el.addEventListener('mousedown', (e) => mousedown(e, el))

}

})

inserted receives three arguments: the element itself, a binding object with a bunch of properties we won’t be using and vnode , which is used by Vue’s compiler and virtual dom. We will bind mouseup and mousedown events to the element. The data v-drag will use will be saved in data . We then pass the data object to any functions who need it — this will make it easier to test the functions.

Tests with Jest

Before going any further, let’s write some tests. We will use the awesome Jest testing library.. Go ahead and install Jest and some supporting packages.

npm install jest babel-preset-env babel-jest --save-dev

You also need to update .babelrc with a new env , which I learned from the Jest documentation.

{

"presets": [

["env", { "modules": false }],

"stage-3"

],

"env": {

"test": {

"presets": [["env"]]

}

}

}

Make a __tests__ folder inside src , and add a drag.test.js to it. The project now looks like this:

Project hierarchy

Let’s start of with a test for mouseup , and mousedown . Add the following to drag.test.js . Also make sure to add export to the two functions in drag.js , so they are able to be imported to to the test.

import { mouseup, mousedown } from '../drag.js' describe('drag', () => {

describe('mouseup', () => {

it('sets down = false', () => {

const data = {

down: true

} mouseup(undefined, undefined, data)

expect(data.down).toBe(false)

})

}) describe('mousedown', () => {

it('sets down = true and initial mouse position', () => {

const data = {

initialX: 0,

initialY: 0,

down: false

} const evt = {

clientX: 1,

clientY: 1

} mousedown(evt, undefined, data)

expect(data.down).toBe(true)

expect(data.initialX).toBe(1)

expect(data.initialY).toBe(1)

})

})

})

Add a new line to package.json :

"scripts": {

"test": "jest"

}

Now you can run the test suite using npm run test .

Currently out directive still doesn’t “do” anything when it is clicked, but with a test suite set up, we are ready to power forward.

Calculating movement and offset

To calculate the position of the element as it is dragged, we should save the initial location when the element is clicked. Let’s do it in a setInitialOffset function.

const _data = {

/* other variables */

draggerOffsetLeft: 0,

draggerOffsetTop: 0

} export function setDraggerOffset (el, _data) {

_data.draggerOffsetLeft = el.offsetLeft

_data.draggerOffsetTop = el.offsetTop

}

and a quick test to make sure it’s working:

describe('setInitialOffset', () => {

it('sets the initial offset of the element', () => {

const data = {

draggerOffsetLeft: 0,

draggerOffsetTop: 0

}

const el = {

offsetLeft: 1,

offsetTop: 1

} setDraggerOffset(el, data)

expect(data.draggerOffsetLeft).toBe(1)

expect(data.draggerOffsetTop).toBe(1)

})

})

Great. Let’s set the initial offset when the element is mounted by calling setInitialOffset in inserted .

export default Vue.directive('drag', {

inserted: function (el, binding, vnode) {

el.addEventListener('mouseup', (e) => mouseup(e, el, _data))

el.addEventListener('mousedown', (e) => mousedown(e, el, _data))

setDraggerOffset(el, _data)

}

})

If you add console.log(_data) into mousedown to double check everything is working. Clicking will now log the _data object.

Saving the initial offset of the element.

Moving the element

Let’s add a mousemove function. If _data.down is true, we will calculate the element’s new position and update the style.top and style.left values.

Let’s start with the the situation when _data.down is false. We should do nothing. A test looks like this:

describe('mousemove', () => {

it('does nothing is down === false', () => {

const data = {

down: false

}

const el = {

style: {

left: 0,

top: 0

}

} mousemove(undefined, el, data) expect(el.style.left).toBe(0)

expect(el.style.top).toBe(0)

})

})

We assert that the element’s style does not change. Now the exciting bit, moving the element. The formula looks like this:

elementNewLeft = elementInitialLeft + (mouseNewX - mouseInitialX)

We have all these values!

elementNewLeft: el.style.left

elementInitialLeft: _data.draggerOffsetLeft

mouseNewX: e.clientX . (We will pass the client event in)

. (We will pass the client event in) mouseInitialX: _data.initialX

So, in code:

export function mousemove (e, el, _data) {

if (_data.down) {

el.style.left = _data.draggerOffsetLeft + (e.clientX - _data.initialX) + 'px'

el.style.top = _data.draggerOffsetTop + (e.clientY - _data.initialY) + 'px'

}

}

And add the listener in inserted :

export default Vue.directive('drag', {

inserted: function (el, binding, vnode) {

el.addEventListener('mouseup', (e) => mouseup(e, el, _data))

el.addEventListener('mousedown', (e) => mousedown(e, el, _data))

el.addEventListener('mousemove', (e) => mousemove(e, el, _data))

setDraggerOffset(el, _data)

}

})

This should be enough to get the box moving!

It works!

Let’s add a test as well:

describe('mousemove', () => {

it('updates the element style if down === true', () => {

const data = {

down: true,

initialX: 10,

initialY: 10,

draggerOffsetLeft: 0,

draggerOffsetTop: 0

}

const e = {

clientX: 20, // clientX - initialX. 20 - 10 = 10

clientY: 20

}

const el = {

style: {

left: 0,

top: 0

}

} mousemove(e, el, data) expect(el.style.left).toBe('10px')

expect(el.style.top).toBe('10px')

})

})

There is a few small bugs. Firstly, you can only draw the element once — we should reset the initial offset of the element in mouseup .

export function mouseup (e, el, _data) {

_data.down = false

setDraggerOffset(el, _data)

}

Now the test is failing. Update the test with a mock el .

describe('mouseup', () => {

it('sets down = false', () => {

const el = {}

const data = {

down: true

}



mouseup(undefined, el, data)

expect(data.down).toBe(false)

})

})

Test suite is green again. There are more improvements — if the user drags the mouse quickly and it leaves the element, mouseup won’t be called, and the element will behave incorrectly. For brevity, let’s move on to publishing the module.

Publishing to NPM

Although we used a vue-cli project to build the directive. When publishing, we want a bit of a different project setup. We want the default export to be the directive, and to compile the code to ES5, to ensure correct behavior in any browser. Create a new folder, v-drag , and initialize a new node project:

npm init -y

and install some packages:

npm install babel-cli babel-preset-env rimraf --save-dev

We need babel to compile, and rimraf to clear the compiled lib files each time we publish.

Update package.json :



"name": "

"version": "0.0.1",

"description": "A Vue.js draggable directive",

"main": "index.js",

"dependencies": {},

"devDependencies": {

"babel-cli": "^6.26.0",

"babel-preset-env": "^1.6.1",

"rimraf": "^2.6.2"

},

"main": "lib/index.js",

"scripts": {

"start": "babel-node src",

"build": "rimraf lib && babel src -d lib --ignore src/__tests__"

},

"repository": {

"type": "git",

"url": "

},

"keywords": [],

"author": "BRANU",

"license": "MIT"

} "name": " @branu -jp/v-drag","version": "0.0.1","description": "A Vue.js draggable directive","main": "index.js","dependencies": {},"devDependencies": {"babel-cli": "^6.26.0","babel-preset-env": "^1.6.1","rimraf": "^2.6.2"},"main": "lib/index.js","scripts": {"start": "babel-node src","build": "rimraf lib && babel src -d lib --ignore src/__tests__"},"repository": {"type": "git","url": " https://github.com/branu-ws/v-drag },"keywords": [],"author": "BRANU","license": "MIT"

Make sure you update the name property. I’m publishing a scoped package, more info is found in the npm documentation. Also notice we have a build script, that will compile any code in a src folder to a lib folder.

Create a src folder, and inside it add a index.js file, with the directive code from drag.js .

We should also add the __tests__ to the src folder, and update them to import from index.js , not drag.js . Lastly, we want to install Vue as a peerDependency , v-drag assumed Vue is installed in any project that uses it.

To publish we run npm run build , which compiles to ES5 in the lib folder.

Now just run npm publish . If you haven’t published to npm before, you will see an error:

npm ERR! code ENEEDAUTH

npm ERR! need auth auth required for publishing

npm ERR! need auth You need to authorize this machine using `npm adduser`

If you haven’t got an account, create on on npm. Then go ahead and run npm adduser , and add your details. Then npm publish , and your package is live. You should see

If everything went well. My copy is live here. It can now be installed like any other package. In my case, npm install @branu-jp/v-drag .

Next steps?

Go ahead and try publishing a package! Some things that I’ll be doing to improve my package is:

Add a readme, explaining how to install and use your package

Add a demo

The demo code is here (article branch) and the published module code is here.