diff --git a/src/.vuepress/config.js b/src/.vuepress/config.js index 1791f38fc2..e73be5c321 100644 --- a/src/.vuepress/config.js +++ b/src/.vuepress/config.js @@ -28,6 +28,11 @@ const sidebar = { '/guide/component-provide-inject' ] }, + { + title: 'Reusability & Composition', + collapsable: false, + children: ['/guide/mixins', '/guide/custom-directive'] + }, { title: 'Migration to Vue 3', collapsable: true, diff --git a/src/guide/custom-directive.md b/src/guide/custom-directive.md new file mode 100644 index 0000000000..563aead6c7 --- /dev/null +++ b/src/guide/custom-directive.md @@ -0,0 +1,248 @@ +# Custom Directives + +## Intro + +In addition to the default set of directives shipped in core (like `v-model` or `v-show`), Vue also allows you to register your own custom directives. Note that in Vue, the primary form of code reuse and abstraction is components - however, there may be cases where you need some low-level DOM access on plain elements, and this is where custom directives would still be useful. An example would be focusing on an input element, like this one: + +

+ See the Pen + Custom directives: basic example by Vue (@Vue) + on CodePen. +

+ + +When the page loads, that element gains focus (note: `autofocus` doesn't work on mobile Safari). In fact, if you haven't clicked on anything else since visiting this page, the input above should be focused now. Also, you can click on the `Rerun` button and input will be focused. + +Now let's build the directive that accomplishes this: + +```js +const app = Vue.createApp({}) +// Register a global custom directive called `v-focus` +app.directive('focus', { + // When the bound element is mounted into the DOM... + mounted(el) { + // Focus the element + el.focus() + } +}) +``` + +If you want to register a directive locally instead, components also accept a `directives` option: + +```js +directives: { + focus: { + // directive definition + mounted(el) { + el.focus() + } + } +} +``` + +Then in a template, you can use the new `v-focus` attribute on any element, like this: + +```html + +``` + +## Hook Functions + +A directive definition object can provide several hook functions (all optional): + +- `beforeMount`: called when the directive is first bound to the element and before parent component is mounted. This is where you can do one-time setup work. + +- `mounted`: called when the bound element's parent component is mounted. + +- `beforeUpdate`: called before the containing component's VNode is updated + +:::tip Note +We'll cover VNodes in more detail [later](TODO:/render-function.html#The-Virtual-DOM), when we discuss [render functions](TODO:./render-function.html). +::: + +- `updated`: called after the containing component's VNode **and the VNodes of its children** have updated. + +- `beforeUnmount`: called before the bound element's parent component is unmounted + +- `unmounted`: called only once, when the directive is unbound from the element and the parent component is unmounted. + +You can check the arguments passed into these hooks (i.e. `el`, `binding`, `vnode`, and `prevVnode`) in [Custom Directive API](TODO) + +### Dynamic Directive Arguments + +Directive arguments can be dynamic. For example, in `v-mydirective:[argument]="value"`, the `argument` can be updated based on data properties in our component instance! This makes our custom directives flexible for use throughout our application. + +Let's say you want to make a custom directive that allows you to pin elements to your page using fixed positioning. We could create a custom directive where the value updates the vertical positioning in pixels, like this: + +```vue-html +
+

Scroll down the page

+

Stick me 200px from the top of the page

+
+``` + +```js +const app = Vue.createApp({}) + +app.directive('pin', { + mounted(el, binding) { + el.style.position = 'fixed' + // binding.value is the value we pass to directive - in this case, it's 200 + el.style.top = binding.value + 'px' + } +}) + +app.mount('#dynamic-arguments-example') +``` + +This would pin the element 200px from the top of the page. But what happens if we run into a scenario when we need to pin the element from the left, instead of the top? Here's where a dynamic argument that can be updated per component instance comes in very handy: + +```vue-html +
+

Scroll down inside this section ↓

+

I am pinned onto the page at 200px to the left.

+
+``` + +```js +const app = Vue.createApp({ + data() { + return { + direction: 'right' + } + } +}) + +app.directive('pin', { + mounted(el, binding) { + el.style.position = 'fixed' + // binding.arg is an argument we pass to directive + const s = binding.arg || 'top' + el.style[s] = binding.value + 'px' + } +}) + +app.mount('#dynamic-arguments-example') +``` + +Result: + +

+ See the Pen + Custom directives: dynamic arguments by Vue (@Vue) + on CodePen. +

+ + +Our custom directive is now flexible enough to support a few different use cases. To make it even more dynamic, we can also allow to modify a bound value. Let's create an additional property `pinPadding` and bind it to the `` + +```vue-html{4} +
+

Scroll down the page

+ +

Stick me 200px from the {{ direction }} of the page

+
+``` + +```js{5} +const app = Vue.createApp({ + data() { + return { + direction: 'right', + pinPadding: 200 + } + } +}) +``` + +Now let's extend our directive logic to recalculate the distance to pin on component update: + +```js{7-10} +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' + } +}) +``` + +Result: + +

+ See the Pen + Custom directives: dynamic arguments + dynamic binding by Vue (@Vue) + on CodePen. +

+ + +## Function Shorthand + +In previous example, you may want the same behavior on `mounted` and `updated`, but don't care about the other hooks. You can do it by passing the callback to directive: + +```js +app.directive('pin', (el, binding) => { + el.style.position = 'fixed' + const s = binding.arg || 'top' + el.style[s] = binding.value + 'px' +}) +``` + +## 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. + +```vue-html +
+``` + +```js +app.directive('demo', (el, binding) => { + console.log(binding.value.color) // => "white" + console.log(binding.value.text) // => "hello!" +}) +``` + +## Usage on Components + +In 3.0, with fragments support, components can potentially have more than one root nodes. This creates an issue when a custom directive is used on a component with multiple root nodes. + +To explain the details of how custom directives will work on components in 3.0, we need to first understand how custom directives are compiled in 3.0. For a directive like this: + +```vue-html +
+``` + +Will roughly compile into this: + +```js +const vFoo = resolveDirective('demo') + +return withDirectives(h('div'), [[vDemo, test]]) +``` + +Where `vDemo` will be the directive object written by the user, which contains hooks like `mounted` and `updated`. + +`withDirectives` returns a cloned VNode with the user hooks wrapped and injected as VNode lifecycle hooks (see [Render Function](TODO:Render-functions) for more details): + +```js +{ + onVnodeMounted(vnode) { + // call vDemo.mounted(...) + } +} +``` + +**As a result, custom directives are fully included as part of a VNode's data. When a custom directive is used on a component, these `onVnodeXXX` hooks are passed down to the component as extraneous props and end up in `this.$attrs`.** + +This also means it's possible to directly hook into an element's lifecycle like this in the template, which can be handy when a custom directive is too involved: + +```vue-html +
+``` + +This is consistent with the [attribute fallthrough behavior](component-props.html#non-prop-attributes). So, the rule for custom directives on a component will be the same as other extraneous attributes: it is up to the child component to decide where and whether to apply it. When the child component uses `v-bind="$attrs"` on an inner element, it will apply any custom directives used on it as well. diff --git a/src/guide/mixins.md b/src/guide/mixins.md new file mode 100644 index 0000000000..48df8b3b68 --- /dev/null +++ b/src/guide/mixins.md @@ -0,0 +1,222 @@ +# Mixins + +## Basics + +Mixins are a flexible way to distribute reusable functionalities for Vue components. A mixin object can contain any component options. When a component uses a mixin, all options in the mixin will be "mixed" into the component's own options. + +Example: + +```js +// define a mixin object +const myMixin = { + created() { + this.hello() + }, + methods: { + hello() { + console.log('hello from mixin!') + } + } +} + +// define an app that uses this mixin +const app = Vue.createApp({ + mixins: [myMixin] +}) + +app.mount('#mixins-basic') // => "hello from mixin!" +``` + +## Option Merging + +When a mixin and the component itself contain overlapping options, they will be "merged" using appropriate strategies. + +For example, data objects undergo a recursive merge, with the component's data taking priority in cases of conflicts. + +```js +const myMixin = { + data() { + return { + message: 'hello', + foo: 'abc' + } + } +} + +const app = Vue.createApp({ + mixins: [myMixin], + data() { + return { + message: 'goodbye', + bar: 'def' + } + }, + created() { + console.log(this.$data) // => { message: "goodbye", foo: "abc", bar: "def" } + } +}) +``` + +Hook functions with the same name are merged into an array so that all of them will be called. Mixin hooks will be called **before** the component's own hooks. + +```js +const myMixin = { + created() { + console.log('mixin hook called') + } +} + +const app = Vue.createApp({ + mixins: [myMixin], + created() { + console.log('component hook called') + } +}) + +// => "mixin hook called" +// => "component hook called" +``` + +Options that expect object values, for example `methods`, `components` and `directives`, will be merged into the same object. The component's options will take priority when there are conflicting keys in these objects: + +```js +const myMixin = { + methods: { + foo() { + console.log('foo') + }, + conflicting() { + console.log('from mixin') + } + } +} + +const app = Vue.createApp({ + mixins: [myMixin], + methods: { + bar() { + console.log('bar') + }, + conflicting() { + console.log('from self') + } + } +}) + +const vm = app.mount('#mixins-basic') + +vm.foo() // => "foo" +vm.bar() // => "bar" +vm.conflicting() // => "from self" +``` + +## Global Mixin + +You can also apply a mixin globally for a Vue application: + +```js +const app = Vue.createApp({ + myOption: 'hello!' +}) + +// inject a handler for `myOption` custom option +app.mixin({ + created() { + const myOption = this.$options.myOption + if (myOption) { + console.log(myOption) + } + } +}) + +app.mount('#mixins-global') // => "hello!" +``` + +Use with caution! Once you apply a mixin globally, it will affect **every** Vue instance created afterwards in the given app (for example, child components): + +```js +const app = Vue.createApp({ + myOption: 'hello!' +}) + +// inject a handler for `myOption` custom option +app.mixin({ + created() { + const myOption = this.$options.myOption + if (myOption) { + console.log(myOption) + } + } +}) + +// add myOption also to child component +app.component('test-component', { + myOption: 'hello from component!' +}) + +app.mount('##mixins-global') + +// => "hello!" +// => "hello from component!" +``` + +In most cases, you should only use it for custom option handling like demonstrated in the example above. It's also a good idea to ship them as [Plugins](TODO) to avoid duplicate application. + +## Custom Option Merge Strategies + +When custom options are merged, they use the default strategy which overwrites the existing value. If you want a custom option to be merged using custom logic, you need to attach a function to `app.config.optionMergeStrategies`: + +```js +const app = Vue.createApp({}) + +app.config.optionMergeStrategies.customOption = (toVal, fromVal) { + // return mergedVal +} +``` + +The merge strategy receives the value of that option defined on the parent and child instances as the first and second arguments, respectively. Let's try to check what do we have in these parameters when we use a mixin: + +```js +const app = Vue.createApp({ + custom: 'hello!' +}) + +app.config.optionMergeStrategies.custom = (toVal, fromVal) => { + console.log(fromVal, toVal) + // => "goodbye!", undefined + // => "hello", "goodbye!" + return fromVal || toVal +} + +app.mixin({ + custom: 'goodbye!', + created() { + console.log(this.$options.custom) // => "hello!" + } +}) +``` + +As you can see, in the console we have `toVal` and `fromVal` printed first from the mixin and then from the `app`. We always return `fromVal` if it exists, that's why `this.$options.custom` is set to `hello!` in the end. Let's try to change a strategy to _always return a value from the child instance_: + +```js +const app = Vue.createApp({ + custom: 'hello!' +}) + +app.config.optionMergeStrategies.custom = (toVal, fromVal) => toVal || fromVal + +app.mixin({ + custom: 'goodbye!', + created() { + console.log(this.$options.custom) // => "goodbye!" + } +}) +``` + +In Vue 2, mixins were the primary tool to abstract parts of component logic into reusable chunks. However, they have a few issues: + +- Mixins are conflict-prone: Since properties from each feature are merged into the same component, you still have to know about every other feature to avoid property name conflicts and for debugging. + +- Reusability is limited: we cannot pass any parameters to the mixin to change its logic which reduces their flexibility in terms of abstracting logic + +To address these issues, we added a new way to organize code by logical concerns: the [Composition API](TODO).