Пользовательские директивы - мощная часть API Vue.js. Давайте посмотрим, как создать собственную директиву v-drag
и опубликовать ее в NPM вместе с набором тестов.
Мы создадим простую директиву, которая позволит любому элементу стать draggable
. Вот краткий предварительный просмотр:
С такой разметкой:
<template> <div v-drag> </div> </template>
Исходный код демонстрационного проекта находится здесь, а окончательный код директивы - здесь. Наконец, опубликованный пакет здесь.
Настраивать
Для начала мы создадим новый проект, используя vue-cli
. Если вы еще не установили, запустите npm install vue-cli -g
. Затем создайте новый проект, используя vue init webpack-simple dragger
.
После создания приложения внутри src/App.vue
удалите существующую разметку и создайте, добавьте следующее:
<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>
Наша директива будет жить в drag.js
. Создайте его на том же уровне, что и App.vue
, и добавьте следующее:
import Vue from 'vue' export default Vue.directive('drag', { inserted: function (el, binding, vnode) { console.log('Drag.js') } })
Запустите приложение, запустив npm run dev
, и посетите localhost:8080
. Вы должны увидеть «Drag.js» в консоли:
Обратите внимание на функцию inserted
выше. В директивах Vue есть несколько ловушек, доступных разработчикам. Мы будем использовать inserted
, который вызывается при монтировании элемента (например, компоненты хуковmounted
). Мы также будем использовать unbind
, который вызывается, когда директива не привязана, когда компонент удаляется из DOM.
Обнаружение щелчка по элементу
Следующим шагом будет обнаружение, когда пользователь нажимает на элемент с v-drag
. Когда пользователь щелкает элемент, мы устанавливаем логическое значение down
на true
и наоборот, когда пользователь отпускает. Мы также сохраним координаты x и y, чтобы вычислить положение элемента при его перемещении.
Добавьте в 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
получает три аргумента: сам элемент, binding
объект с набором свойств, которые мы не будем использовать, и vnode
, который используется компилятором Vue и виртуальным домом. Мы привяжем к элементу события mouseup
и mousedown
. Данные, которые будут использовать v-drag
, будут сохранены в data
. Затем мы передаем объект data
всем функциям, которым он нужен - это упростит тестирование функций.
Тесты с Jest
Прежде чем двигаться дальше, давайте напишем несколько тестов. Мы будем использовать замечательную библиотеку тестирования Jest. Идите и установите Jest и некоторые вспомогательные пакеты.
npm install jest babel-preset-env babel-jest --save-dev
Вам также необходимо обновить .babelrc
новым env
, о чем я узнал из документации Jest.
{ "presets": [ ["env", { "modules": false }], "stage-3" ], "env": { "test": { "presets": [["env"]] } } }
Создайте папку __tests__
внутри src
и добавьте к ней drag.test.js
. Теперь проект выглядит так:
Давайте начнем с теста на mouseup
и mousedown
. Добавьте следующее в drag.test.js
. Также не забудьте добавить export
к двум функциям в drag.js
, чтобы их можно было импортировать в тест.
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) }) }) })
Добавьте новую строку в package.json
:
"scripts": { "test": "jest" }
Теперь вы можете запустить набор тестов, используя npm run test
.
В настоящее время директива out по-прежнему ничего не «делает» при нажатии, но с настроенным набором тестов мы готовы двигаться вперед.
Расчет движения и смещения
Чтобы рассчитать положение элемента при его перетаскивании, мы должны сохранить исходное местоположение при щелчке по элементу. Давайте сделаем это в setInitialOffset
функции.
const _data = { /* other variables */ draggerOffsetLeft: 0, draggerOffsetTop: 0 } export function setDraggerOffset (el, _data) { _data.draggerOffsetLeft = el.offsetLeft _data.draggerOffsetTop = el.offsetTop }
и быстрый тест, чтобы убедиться, что он работает:
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) }) })
Большой. Давайте установим начальное смещение при монтировании элемента, вызвав setInitialOffset
в 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) } })
Если вы добавите console.log(_data)
в mousedown
, чтобы дважды проверить, все работает. Щелчок теперь приведет к регистрации объекта _data
.
Перемещение элемента
Давайте добавим mousemove
функцию. Если _data.down
истинно, мы вычислим новую позицию элемента и обновим значения style.top
и style.left
.
Начнем с ситуации, когда _data.down
false. Мы не должны ничего делать. Тест выглядит так:
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) }) })
Мы утверждаем, что стиль элемента не меняется. Теперь самое интересное - перемещение элемента. Формула выглядит так:
elementNewLeft = elementInitialLeft + (mouseNewX - mouseInitialX)
У нас есть все эти ценности!
- elementNewLeft:
el.style.left
- elementInitialLeft:
_data.draggerOffsetLeft
- mouseNewX:
e.clientX
. (Мы передадим клиентское событие) - mouseInitialX:
_data.initialX
Итак, в коде:
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' } }
И добавьте слушателя в 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) } })
Этого должно быть достаточно, чтобы коробка сдвинулась с места!
Добавим еще тест:
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') }) })
Есть несколько мелких ошибок. Во-первых, вы можете нарисовать элемент только один раз - мы должны сбросить начальное смещение элемента в mouseup
.
export function mouseup (e, el, _data) { _data.down = false setDraggerOffset(el, _data) }
Теперь тест не проходит. Обновите тест с помощью mock el
.
describe('mouseup', () => { it('sets down = false', () => { const el = {} const data = { down: true } mouseup(undefined, el, data) expect(data.down).toBe(false) }) })
Набор тестов снова зеленый. Есть и другие улучшения: если пользователь быстро перетаскивает мышь, и она покидает элемент, mouseup
не будет вызываться, и элемент будет вести себя некорректно. Для краткости перейдем к публикации модуля.
Публикация в NPM
Хотя для построения директивы мы использовали проект vue-cli
. При публикации мы хотим немного изменить настройку проекта. Мы хотим, чтобы экспорт по умолчанию был директивой и чтобы код компилировался в ES5, чтобы гарантировать правильное поведение в любом браузере. Создайте новую папку v-drag
и инициализируйте новый проект узла:
npm init -y
и установите несколько пакетов:
npm install babel-cli babel-preset-env rimraf --save-dev
Нам нужен babel для компиляции и rimraf для очистки файлов compiledlib
каждый раз, когда мы публикуем.
Обновить package.json
:
{ "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" }
Убедитесь, что вы обновили свойство name
. Я публикую пакет с ограниченной областью видимости, дополнительную информацию можно найти в документации npm. Также обратите внимание, что у нас есть сценарий build
, который компилирует любой код из папки src
в папку lib
.
Создайте папку src
и внутри нее добавьте файл index.js
с кодом директивы из drag.js
.
Мы также должны добавить __tests__
в папку src
и обновить их для импорта из index.js
, а не из drag.js
. Наконец, мы хотим установить Vue как peerDependency
, v-drag
предполагаемый Vue установлен в любом проекте, который его использует.
Для публикации запускаем npm run build
, который компилируется в ES5 в папке lib
.
Теперь просто запустите npm publish
. Если вы раньше не публиковали в npm, вы увидите сообщение об ошибке:
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`
Если у вас нет учетной записи, создайте в npm. Затем запустите npm adduser
и добавьте свои данные. Тогда npm publish
, и ваш пакет готов. Тебе следует увидеть
+ @branu-jp/[email protected]
Если все будет хорошо. Моя копия живая здесь. Теперь его можно установить, как и любой другой пакет. В моем случае npm install @branu-jp/v-drag
.
Следующие шаги?
Вперед и попробуйте опубликовать пакет! Вот некоторые вещи, которые я буду делать, чтобы улучшить свой пакет:
- Добавьте файл readme, объясняющий, как установить и использовать ваш пакет.
- Добавить демо
Демо-код находится здесь (ветка статьи), а опубликованный код модуля - здесь.