# Пользовательские директивы

# Введение

Кроме использования встроенных директив (таких как v-model, v-show), Vue позволяет создавать пользовательские. При этом важно понимать, что основным механизмом для создания повторно используемого кода во Vue всё-таки являются компоненты. Но для выполнения низкоуровневых операций с DOM директивы могут быть очень полезны. К примеру, для реализации фокуса на элементе input:

See the Pen Пользовательские директивы: простой пример by Vue (@Vue) on CodePen.

После загрузки страницы этот элемент получит фокус (примечание: autofocus не работает на мобильном Safari). Если после открытия этой страницы руководства ещё не кликнули никуда, то фокус и сейчас должен быть на этом элементе. Можно также перезапустить пример по кнопке Rerun и убедиться что фокус будет на поле ввода.

Давайте реализуем директиву для этой функциональности:

const app = Vue.createApp({})

// Регистрируем глобальную пользовательскую директиву `v-focus`
app.directive('focus', {
  // Когда привязанный элемент будет примонтирован в DOM...
  mounted(el) {
    // Переключаем фокус на элемент
    el.focus()
  }
})
1
2
3
4
5
6
7
8
9
10

Чтобы зарегистрировать директиву локально, нужно передать опцию directives при определении компонента:

directives: {
  focus: {
    // определение директивы
    mounted(el) {
      el.focus()
    }
  }
}
1
2
3
4
5
6
7
8

После регистрации директивы можно использовать в шаблоне новый атрибут v-focus:

<input v-focus />
1

# Хуки

Для жизненного цикла директивы можно указать следующие хуки (все они опциональны):

  • created: вызывается до привязки атрибутов или слушателей событий к элементу. Это полезно в тех случаях, когда директиве необходимо привязать слушатели событий, которые должны вызываться перед обычными слушателями событий v-on.

  • beforeMount: вызывается при первой привязке директивы к элементу и перед монтированием родительского компонента. В нём можно выполнять какую-то единоразовую инициализацию.

  • mounted: вызывается перед монтированием родительского компонента, к элементу которого привязана директива.

  • beforeUpdate: вызывается перед обновлением VNode содержащего компонента.

Примечание

Подробнее VNode рассмотрим позднее, когда будем обсуждать render-функции.

  • updated: вызывается после того как обновлены VNode содержащего компонента и все VNode его дочерних элементов.

  • beforeUnmount: вызывается перед размонтированием родительского компонента, к элементу которого привязана директива.

  • unmounted: вызывается только один раз, когда директива отвязывается от элемента и родительский компонент размонтирован.

Подробнее об аргументах, которые передаются в эти хуки (например, el, binding, vnode и prevVnode) можно узнать в API приложения.

# Динамические аргументы директивы

Аргументы директивы могут быть динамическими. Для v-mydirective:[argument]="value" например, argument может обновляться в зависимости от свойства данных экземпляра компонента! Это позволяет сделать пользовательские директивы более гибкими при использовании в приложении.

Допустим, необходимо создать собственную директиву, которая позволит закрепить элемент на странице с помощью фиксированного позиционирования. Можно создать пользовательскую директиву, где значение определяет вертикальный отступ в пикселях:

<div id="dynamic-arguments-example" class="demo">
  <p>Прокрутите страницу вниз</p>
  <p v-pin="200">Элемент зафиксирован в 200px от начала страницы</p>
</div>
1
2
3
4
const app = Vue.createApp({})

app.directive('pin', {
  mounted(el, binding) {
    el.style.position = 'fixed'
    // binding.value — передаваемое в директиву значение, в этом случае 200
    el.style.top = binding.value + 'px'
  }
})

app.mount('#dynamic-arguments-example')
1
2
3
4
5
6
7
8
9
10
11

Это закрепит элемент в 200px от начала страницы. Но что если возникнет случай, когда потребуется закрепить элемент не сверху, а слева? Для этого пригодится динамический аргумент директивы, который можно определить для каждого экземпляра компонента:



 


<div id="dynamicexample">
  <h3>Прокрутите страницу вниз</h3>
  <p v-pin:[direction]="200">Зафиксировать в 200px от {{ direction }} страницы</p>
</div>
1
2
3
4



 







 
 






const app = Vue.createApp({
  data() {
    return {
      direction: 'right'
    }
  }
})

app.directive('pin', {
  mounted(el, binding) {
    el.style.position = 'fixed'
    // binding.arg — передаваемый в директиву аргумент
    const s = binding.arg || 'top'
    el.style[s] = binding.value + 'px'
  }
})

app.mount('#dynamic-arguments-example')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

Результат:

See the Pen Пользовательские директивы: пример с динамическим аргументом by Vue (@Vue) on CodePen.

Теперь можно гибко использовать пользовательскую директиву в различных сценариях. Можно сделать её ещё более динамичной, позволив изменять значение отступа. Для этого создадим дополнительное свойство pinPadding и привяжем его к <input type="range">.




 


<div id="dynamicexample">
  <h2>Scroll down the page</h2>
  <input type="range" min="0" max="500" v-model="pinPadding">
  <p v-pin:[direction]="pinPadding">Зафиксировать в {{ pinPadding + 'px' }} от {{ direction || 'top' }} страницы</p>
</div>
1
2
3
4
5




 




const app = Vue.createApp({
  data() {
    return {
      direction: 'right',
      pinPadding: 200
    }
  }
})
1
2
3
4
5
6
7
8

И добавим логику перерасчёта расстояния для директивы при обновлениях компонента:







 
 
 
 


app.directive('pin', {
  mounted(el, binding) {
    el.style.position = 'fixed'
    const s = binding.arg || 'top'
    el.style[s] = binding.value + 'px'
  },
  updated(el, binding) {
    const s = binding.arg || 'top'
    el.style[s] = binding.value + 'px'
  }
})
1
2
3
4
5
6
7
8
9
10
11

Результат:

See the Pen Пользовательские директивы: динамический аргумент + динамическая привязка by Vue (@Vue) on CodePen.

# Сокращённая запись

Как в примере выше, можно получить одинаковую логику в mounted и updated и не использовать другие хуки. В таких случаях функцию можно сразу передать в директиву:

app.directive('pin', (el, binding) => {
  el.style.position = 'fixed'
  const s = binding.arg || 'top'
  el.style[s] = binding.value + 'px'
})
1
2
3
4
5

# Передача в директиву объекта с данными

Если для директивы требуется несколько параметров, то их можно передавать объектом JavaScript — они могут принимать любые допустимые JavaScript выражения.

<div v-demo="{ color: 'белый', text: 'привет!' }"></div>
1
app.directive('demo', (el, binding) => {
  console.log(binding.value.color) // => "белый"
  console.log(binding.value.text) // => "привет!"
})
1
2
3
4

# Использование на компонентах

Пользовательская директива при использовании на компоненте будет всегда применяться к корневому элементу компонента, как в случае передачи обычных атрибутов.

<my-component v-demo="test"></my-component>
1
app.component('my-component', {
  template: `
    <div> // директива v-demo будет добавлена на этот элемент
      <span>Содержимое компонента</span>
    </div>
  `
})
1
2
3
4
5
6
7

Но в отличие от обычных атрибутов, директивы нельзя передать на другой элемент через v-bind="$attrs". А с поддержкой фрагментов у компонентов может быть более одного корневого элемента. При указании директивы на компоненте с несколькими корневыми элементами, директива будет проигнорирована и выведено предупреждение в консоль.