Custom Directives

Basics

In addition to the default set of directives shipped in core, Vue.js also allows you to register custom directives. Custom directives provide a mechanism for mapping data changes to arbitrary DOM behavior.

You can register a global custom directive with the Vue.directive(id, definition) method, passing in a directive id followed by a definition object. You can also register a local custom directive by including it in a component’s directives option.

Hook Functions

A definition object can provide several hook functions (all optional):

  • bind: called only once, when the directive is first bound to the element.

  • update: called for the first time immediately after bind with the initial value, then again whenever the binding value changes. The new value and the previous value are provided as the argument.

  • unbind: called only once, when the directive is unbound from the element.

Example

Vue.directive('my-directive', {
  bind: function () {
    // do preparation work
    // e.g. add event listeners or expensive stuff
    // that needs to be run only once
  },
  update: function (newValue, oldValue) {
    // do something based on the updated value
    // this will also be called for the initial value
  },
  unbind: function () {
    // do clean up work
    // e.g. remove event listeners added in bind()
  }
})

Once registered, you can use it in Vue.js templates like this (remember to add the v- prefix):

<div v-my-directive="someValue"></div>

When you only need the update function, you can pass in a single function instead of the definition object:

Vue.directive('my-directive', function (value) {
  // this function will be used as update()
})

Directive Instance Properties

All the hook functions will be copied into the actual directive object, which you can access inside these functions as their this context. The directive object exposes some useful properties:

  • el: the element the directive is bound to.
  • vm: the context ViewModel that owns this directive.
  • expression: the expression of the binding, excluding arguments and filters.
  • arg: the argument, if present.
  • name: the name of the directive, without the prefix.
  • modifiers: an object containing modifiers, if any.
  • descriptor: an object that contains the parsing result of the entire directive.
  • params: an object containing param attributes. Explained below.

You should treat all these properties as read-only and never modify them. You can attach custom properties to the directive object too, but be careful not to accidentally overwrite existing internal ones.

An example of a custom directive using some of these properties:

<div id="demo" v-demo:hello.a.b="msg"></div>
Vue.directive('demo', {
  bind: function () {
    console.log('demo bound!')
  },
  update: function (value) {
    this.el.innerHTML =
      'name - '       + this.name + '<br>' +
      'expression - ' + this.expression + '<br>' +
      'argument - '   + this.arg + '<br>' +
      'modifiers - '  + JSON.stringify(this.modifiers) + '<br>' +
      'value - '      + value
  }
})
var demo = new Vue({
  el: '#demo',
  data: {
    msg: 'hello!'
  }
})

Result

Object Literals

If your directive needs multiple values, you can also pass in a JavaScript object literal. Remember, directives can take any valid JavaScript expression:

<div v-demo="{ color: 'white', text: 'hello!' }"></div>
Vue.directive('demo', function (value) {
  console.log(value.color) // "white"
  console.log(value.text) // "hello!"
})

Literal Modifier

When a directive is used with the literal modifier, its attribute value will be interpreted as a plain string and passed directly into the update method. The update method will also be called only once, because a plain string cannot be reactive.

<div v-demo.literal="foo bar baz">
Vue.directive('demo', function (value) {
  console.log(value) // "foo bar baz"
})

Element Directives

In some cases, we may want our directive to be used in the form of a custom element rather than as an attribute. This is very similar to Angular’s notion of “E” mode directives. Element directives provide a lighter-weight alternative to full-blown components (which are explained later in the guide). You can register a custom element directive like so:

Vue.elementDirective('my-directive', {
  // same API as normal directives
  bind: function () {
    // manipulate this.el...
  }
})

Then, instead of:

<div v-my-directive></div>

We can write:

<my-directive></my-directive>

Element directives cannot accept arguments or expressions, but it can read the element’s attributes to determine its behavior.

A big difference from normal directives is that element directives are terminal, which means once Vue encounters an element directive, it will completely skip that element - only the element directive itself will be able to manipulate that element and its children.

Advanced Options

params

Custom directive can provide a params array, and the Vue compiler will automatically extract these attributes on the element that the directive is bound to. Example:

<div v-example a="hi"></div>
Vue.directive('example', {
  params: ['a'],
  bind: function () {
    console.log(this.params.a) // -> "hi"
  }
})

This API also supports dynamic attributes. The this.params[key] value is automatically kept up-to-date. In addition, you can specify a callback when the value has changed:

<div v-example v-bind:a="someValue"></div>
Vue.directive('example', {
  params: ['a'],
  paramWatchers: {
    a: function (val, oldVal) {
      console.log('a changed!')
    }
  }
})

Note that similar to props, directive params follow the same camelCase <=> kebab case mapping between JavaScript and HTML. For example, for a param used as disable-effect in the template, you need to access it as disableEffect in JavaScript.

deep

If your custom directive is expected to be used on an Object, and it needs to trigger update when a nested property inside the object changes, you need to pass in deep: true in your directive definition.

<div v-my-directive="obj"></div>
Vue.directive('my-directive', {
  deep: true,
  update: function (obj) {
    // will be called when nested properties in `obj`
    // changes.
  }
})

twoWay

If your directive expects to write data back to the Vue instance, you need to pass in twoWay: true. This option allows the use of this.set(value) inside the directive:

Vue.directive('example', {
  twoWay: true,
  bind: function () {
    this.handler = function () {
      // set data back to the vm.
      // If the directive is bound as v-example="a.b.c",
      // this will attempt to set `vm.a.b.c` with the
      // given value.
      this.set(this.el.value)
    }.bind(this)
    this.el.addEventListener('input', this.handler)
  },
  unbind: function () {
    this.el.removeEventListener('input', this.handler)
  }
})

acceptStatement

Passing in acceptStatement:true enables your custom directive to accept inline statements like v-on does:

<div v-my-directive="a++"></div>
Vue.directive('my-directive', {
  acceptStatement: true,
  update: function (fn) {
    // the passed in value is a function which when called,
    // will execute the "a++" statement in the owner vm's
    // scope.
  }
})

Use this wisely though, because in general you want to avoid side-effects in your templates.

terminal

1.0.19+

Vue compiles templates by recursively walking the DOM tree. However when it encounters a terminal directive, it will stop walking that element’s children. The terminal directive takes over the job of compiling the element and its children. For example, v-if and v-for are both terminal directives.

Writing a custom terminal directive is an advanced topic and requires decent knowledge of Vue’s compilation pipeline, but it’s possible. You can specify a custom terminal directive by specifying terminal: true. You will also likely need to use Vue.FragmentFactory for partial compilation. Here’s an example of a custom terminal directive that compiles and “injects” its content template to another location on the page:

var FragmentFactory = Vue.FragmentFactory
var remove = Vue.util.remove
var createAnchor = Vue.util.createAnchor

Vue.directive('inject', {
  terminal: true,
  bind: function () {
    var container = document.getElementById(this.arg)
    this.anchor = createAnchor('v-inject')
    container.appendChild(this.anchor)
    remove(this.el)
    var factory = new FragmentFactory(this.vm, this.el)
    this.frag = factory.create(this._host, this._scope, this._frag)
    this.frag.before(this.anchor)
  },
  unbind: function () {
    this.frag.remove()
    remove(this.anchor)
  }
})
<div id="modal"></div>
...
<div v-inject:modal>
  <h1>header</h1>
  <p>body</p>
  <p>footer</p>
</div>

If you want to write a custom terminal directive, it is recommend that you read through the source code of built-in terminal directives like v-if and v-for to get a better understanding of Vue internals.

priority

You can optionally provide a priority number for your directive. If no priority is specified, a default priority will be used - 1000 for normal directives and 2000 for terminal directives. A directive with a higher priority will be processed earlier than other directives on the same element. Directives with the same priority will be processed in the order they appear in the element’s attribute list, although that order is not guaranteed to be consistent in different browsers.

You can checkout the priorities for some built-in directives in the API reference. Additionally, flow control directives v-if and v-for always have the highest priority in the compilation process.

doc_VueJS
2016-09-25 05:48:05
Comments
Leave a Comment

Please login to continue.