What are Components?
Components are one of the most powerful features of Vue.js. They help you extend basic HTML elements to encapsulate reusable code. At a high level, Components are custom elements that Vue.js’ compiler would attach specified behavior to. In some cases, they may also appear as a native HTML element extended with the special is
attribute.
Using Components
Registration
We’ve learned in the previous sections that we can create a component constructor using Vue.extend()
:
var MyComponent = Vue.extend({ // options... })
To use this constructor as a component, you need to register it with Vue.component(tag, constructor)
:
// Globally register the component with tag: my-component Vue.component('my-component', MyComponent)
Note that Vue.js does not enforce the W3C rules for custom tag-names (all-lowercase, must contain a hyphen) though following this convention is considered good practice.
Once registered, the component can now be used in a parent instance’s template as a custom element, <my-component>
. Make sure the component is registered before you instantiate your root Vue instance. Here’s the full example:
<div id="example"> <my-component></my-component> </div>
// define var MyComponent = Vue.extend({ template: '<div>A custom component!</div>' }) // register Vue.component('my-component', MyComponent) // create a root instance new Vue({ el: '#example' })
Which will render:
<div id="example"> <div>A custom component!</div> </div>
Note the component’s template replaces the custom element, which only serves as a mounting point. This behavior can be configured using the replace
instance option.
Local Registration
You don’t have to register every component globally. You can make a component available only in the scope of another component by registering it with the components
instance option:
var Child = Vue.extend({ /* ... */ }) var Parent = Vue.extend({ template: '...', components: { // <my-component> will only be available in Parent's template 'my-component': Child } })
The same encapsulation applies for other assets types such as directives, filters and transitions.
Registration Sugar
To make things easier, you can directly pass in the options object instead of an actual constructor to Vue.component()
and the component
option. Vue.js will automatically call Vue.extend()
for you under the hood:
// extend and register in one step Vue.component('my-component', { template: '<div>A custom component!</div>' }) // also works for local registration var Parent = Vue.extend({ components: { 'my-component': { template: '<div>A custom component!</div>' } } })
Component Option Caveats
Most of the options that can be passed into the Vue constructor can be used in Vue.extend()
, with two special cases: data
and el
. Imagine we simply pass an object as data
to Vue.extend()
:
var data = { a: 1 } var MyComponent = Vue.extend({ data: data })
The problem with this is that the same data
object will be shared across all instances of MyComponent
! This is most likely not what we want, so we should use a function that returns a fresh object as the data
option:
var MyComponent = Vue.extend({ data: function () { return { a: 1 } } })
The el
option also requires a function value when used in Vue.extend()
, for exactly the same reason.
Template Parsing
Vue.js template engine is DOM-based and uses native parser that comes with the browser instead of providing a custom one. There are benefits to this approach when compared to string-based template engines, but there are also caveats. Templates have to be individually valid pieces of HTML. Some HTML elements have restrictions on what elements can appear inside them. Most common of these restrictions are:
-
a
can not contain other interactive elements (e.g. buttons and other links) -
li
should be a direct child oful
orol
, and bothul
andol
can only containli
-
option
should be a direct child ofselect
, andselect
can only containoption
(andoptgroup
) -
table
can only containthead
,tbody
,tfoot
andtr
, and these elements should be direct children oftable
-
tr
can only containth
andtd
, and these elements should be direct children oftr
In practice these restriction can cause unexpected behavior. Although in simple cases it might appear to work, you can not rely on custom elements being expanded before browser validation. E.g. <my-select><option>...</option></my-select>
is not a valid template even if my-select
component eventually expands to <select>...</select>
.
Another consequence is that you can not use custom tags (including custom elements and special tags like <component>
, <template>
and <partial>
) inside of ul
, select
, table
and other elements with similar restrictions. Custom tags will be hoisted out and thus not render properly.
In case of a custom element you should use the is
special attribute:
<table> <tr is="my-component"></tr> </table>
In case of a <template>
inside of a <table>
you should use <tbody>
, as tables are allowed to have multiple tbody
:
<table> <tbody v-for="item in items"> <tr>Even row</tr> <tr>Odd row</tr> </tbody> </table>
Props
Passing Data with Props
Every component instance has its own isolated scope. This means you cannot (and should not) directly reference parent data in a child component’s template. Data can be passed down to child components using props.
A “prop” is a field on a component’s data that is expected to be passed down from its parent component. A child component needs to explicitly declare the props it expects to receive using the props
option:
Vue.component('child', { // declare the props props: ['msg'], // the prop can be used inside templates, and will also // be set as `this.msg` template: '<span>{{ msg }}</span>' })
Then, we can pass a plain string to it like so:
<child msg="hello!"></child>
Result:
camelCase vs. kebab-case
HTML attributes are case-insensitive. When using camelCased prop names as attributes, you need to use their kebab-case (hyphen-delimited) equivalents:
Vue.component('child', { // camelCase in JavaScript props: ['myMessage'], template: '<span>{{ myMessage }}</span>' })
<!-- kebab-case in HTML --> <child my-message="hello!"></child>
Dynamic Props
Similar to binding a normal attribute to an expression, we can also use v-bind
for dynamically binding props to data on the parent. Whenever the data is updated in the parent, it will also flow down to the child:
<div> <input v-model="parentMsg"> <br> <child v-bind:my-message="parentMsg"></child> </div>
It is often simpler to use the shorthand syntax for v-bind
:
<child :my-message="parentMsg"></child>
Result:
Literal vs. Dynamic
A common mistake beginners tend to make is attempting to pass down a number using the literal syntax:
<!-- this passes down a plain string "1" --> <comp some-prop="1"></comp>
However, since this is a literal prop, its value is passed down as a plain string "1"
, instead of an actual number. If we want to pass down an actual JavaScript number, we need to use the dynamic syntax to make its value be evaluated as a JavaScript expression:
<!-- this passes down an actual number --> <comp :some-prop="1"></comp>
Prop Binding Types
By default, all props form a one-way-down binding between the child property and the parent one: when the parent property updates, it will flow down to the child, but not the other way around. This default is meant to prevent child components from accidentally mutating the parent’s state, which can make your app’s data flow harder to reason about. However, it is also possible to explicitly enforce a two-way or a one-time binding with the .sync
and .once
binding type modifiers:
Compare the syntax:
<!-- default, one-way-down binding --> <child :msg="parentMsg"></child> <!-- explicit two-way binding --> <child :msg.sync="parentMsg"></child> <!-- explicit one-time binding --> <child :msg.once="parentMsg"></child>
The two-way binding will sync the change of child’s msg
property back to the parent’s parentMsg
property. The one-time binding, once set up, will not sync future changes between the parent and the child.
Note that if the prop being passed down is an Object or an Array, it is passed by reference. Mutating the Object or Array itself inside the child will affect parent state, regardless of the binding type you are using.
Prop Validation
It is possible for a component to specify the requirements for the props it is receiving. This is useful when you are authoring a component that is intended to be used by others, as these prop validation requirements essentially constitute your component’s API, and ensure your users are using your component correctly. Instead of defining the props as an array of strings, you can use the object hash format that contain validation requirements:
Vue.component('example', { props: { // basic type check (`null` means accept any type) propA: Number, // multiple possible types (1.0.21+) propM: [String, Number], // a required string propB: { type: String, required: true }, // a number with default value propC: { type: Number, default: 100 }, // object/array defaults should be returned from a // factory function propD: { type: Object, default: function () { return { msg: 'hello' } } }, // indicate this prop expects a two-way binding. will // raise a warning if binding type does not match. propE: { twoWay: true }, // custom validator function propF: { validator: function (value) { return value > 10 } }, // coerce function (new in 1.0.12) // cast the value before setting it on the component propG: { coerce: function (val) { return val + '' // cast the value to string } }, propH: { coerce: function (val) { return JSON.parse(val) // cast the value to Object } } } })
The type
can be one of the following native constructors:
- String
- Number
- Boolean
- Function
- Object
- Array
In addition, type
can also be a custom constructor function and the assertion will be made with an instanceof
check.
When a prop validation fails, Vue will refuse to set the value on the child component, and throw a warning if using the development build.
Parent-Child Communication
Parent Chain
A child component holds access to its parent component as this.$parent
. A root Vue instance will be available to all of its descendants as this.$root
. Each parent component has an array, this.$children
, which contains all its child components.
Although it’s possible to access any instance in the parent chain, you should avoid directly relying on parent data in a child component and prefer passing data down explicitly using props. In addition, it is a very bad idea to mutate parent state from a child component, because:
-
It makes the parent and child tightly coupled;
-
It makes the parent state much harder to reason about when looking at it alone, because its state may be modified by any child! Ideally, only a component itself should be allowed to modify its own state.
Custom Events
All Vue instances implement a custom event interface that facilitates communication within a component tree. This event system is independent from the native DOM events and works differently.
Each Vue instance is an event emitter that can:
-
Listen to events using
$on()
; -
Trigger events on self using
$emit()
; -
Dispatch an event that propagates upward along the parent chain using
$dispatch()
; -
Broadcast an event that propagates downward to all descendants using
$broadcast()
.
Unlike DOM events, Vue events will automatically stop propagation after triggering callbacks for the first time along a propagation path, unless the callback explicitly returns true
.
A simple example:
<!-- template for child --> <template id="child-template"> <input v-model="msg"> <button v-on:click="notify">Dispatch Event</button> </template> <!-- template for parent --> <div id="events-example"> <p>Messages: {{ messages | json }}</p> <child></child> </div>
// register child, which dispatches an event with // the current message Vue.component('child', { template: '#child-template', data: function () { return { msg: 'hello' } }, methods: { notify: function () { if (this.msg.trim()) { this.$dispatch('child-msg', this.msg) this.msg = '' } } } }) // bootstrap parent, which pushes message into an array // when receiving the event var parent = new Vue({ el: '#events-example', data: { messages: [] }, // the `events` option simply calls `$on` for you // when the instance is created events: { 'child-msg': function (msg) { // `this` in event callbacks are automatically bound // to the instance that registered it this.messages.push(msg) } } })
v-on for Custom Events
The example above is pretty nice, but when we are looking at the parent’s code, it’s not so obvious where the "child-msg"
event comes from. It would be better if we can declare the event handler in the template, right where the child component is used. To make this possible, v-on
can be used to listen for custom events when used on a child component:
<child v-on:child-msg="handleIt"></child>
This makes things very clear: when the child triggers the "child-msg"
event, the parent’s handleIt
method will be called. Any code that affects the parent’s state will be inside the handleIt
parent method; the child is only concerned with triggering the event.
Child Component Refs
Despite the existence of props and events, sometimes you might still need to directly access a child component in JavaScript. To achieve this you have to assign a reference ID to the child component using v-ref
. For example:
<div id="parent"> <user-profile v-ref:profile></user-profile> </div>
var parent = new Vue({ el: '#parent' }) // access child component instance var child = parent.$refs.profile
When v-ref
is used together with v-for
, the ref you get will be an Array or an Object containing the child components mirroring the data source.
Content Distribution with Slots
When using components, it is often desired to compose them like this:
<app> <app-header></app-header> <app-footer></app-footer> </app>
There are two things to note here:
-
The
<app>
component does not know what content may be present inside its mount target. It is decided by whatever parent component that is using<app>
. -
The
<app>
component very likely has its own template.
To make the composition work, we need a way to interweave the parent “content” and the component’s own template. This is a process called content distribution (or “transclusion” if you are familiar with Angular). Vue.js implements a content distribution API that is modeled after with the current Web Components spec draft, using the special <slot>
element to serve as distribution outlets for the original content.
Compilation Scope
Before we dig into the API, let’s first clarify which scope the contents are compiled in. Imagine a template like this:
<child-component> {{ msg }} </child-component>
Should the msg
be bound to the parent’s data or the child data? The answer is parent. A simple rule of thumb for component scope is:
Everything in the parent template is compiled in parent scope; everything in the child template is compiled in child scope.
A common mistake is trying to bind a directive to a child property/method in the parent template:
<!-- does NOT work --> <child-component v-show="someChildProperty"></child-component>
Assuming someChildProperty
is a property on the child component, the example above would not work as intended. The parent’s template should not be aware of the state of a child component.
If you need to bind child-scope directives on a component root node, you should do so in the child component’s own template:
Vue.component('child-component', { // this does work, because we are in the right scope template: '<div v-show="someChildProperty">Child</div>', data: function () { return { someChildProperty: true } } })
Similarly, distributed content will be compiled in the parent scope.
Single Slot
Parent content will be discarded unless the child component template contains at least one <slot>
outlet. When there is only one slot with no attributes, the entire content fragment will be inserted at its position in the DOM, replacing the slot itself.
Anything originally inside the <slot>
tags is considered fallback content. Fallback content is compiled in the child scope and will only be displayed if the hosting element is empty and has no content to be inserted.
Suppose we have a component with the following template:
<div> <h1>This is my component!</h1> <slot> This will only be displayed if there is no content to be distributed. </slot> </div>
Parent markup that uses the component:
<my-component> <p>This is some original content</p> <p>This is some more original content</p> </my-component>
The rendered result will be:
<div> <h1>This is my component!</h1> <p>This is some original content</p> <p>This is some more original content</p> </div>
Named Slots
<slot>
elements have a special attribute, name
, which can be used to further customize how content should be distributed. You can have multiple slots with different names. A named slot will match any element that has a corresponding slot
attribute in the content fragment.
There can still be one unnamed slot, which is the default slot that serves as a catch-all outlet for any unmatched content. If there is no default slot, unmatched content will be discarded.
For example, suppose we have a multi-insertion
component with the following template:
<div> <slot name="one"></slot> <slot></slot> <slot name="two"></slot> </div>
Parent markup:
<multi-insertion> <p slot="one">One</p> <p slot="two">Two</p> <p>Default A</p> </multi-insertion>
The rendered result will be:
<div> <p slot="one">One</p> <p>Default A</p> <p slot="two">Two</p> </div>
The content distribution API is a very useful mechanism when designing components that are meant to be composed together.
Dynamic Components
You can use the same mount point and dynamically switch between multiple components by using the reserved <component>
element and dynamically bind to its is
attribute:
new Vue({ el: 'body', data: { currentView: 'home' }, components: { home: { /* ... */ }, posts: { /* ... */ }, archive: { /* ... */ } } })
<component :is="currentView"> <!-- component changes when vm.currentview changes! --> </component>
keep-alive
If you want to keep the switched-out components alive so that you can preserve its state or avoid re-rendering, you can add a keep-alive
directive param:
<component :is="currentView" keep-alive> <!-- inactive components will be cached! --> </component>
activate
Hook
When switching components, the incoming component might need to perform some asynchronous operation before it should be swapped in. To control the timing of component swapping, implement the activate
hook on the incoming component:
Vue.component('activate-example', { activate: function (done) { var self = this loadDataAsync(function (data) { self.someData = data done() }) } })
Note the activate
hook is only respected during dynamic component swapping or the initial render for static components - it does not affect manual insertions with instance methods.
transition-mode
The transition-mode
param attribute allows you to specify how the transition between two dynamic components should be executed.
By default, the transitions for incoming and outgoing components happen simultaneously. This attribute allows you to configure two other modes:
-
in-out
: New component transitions in first, current component transitions out after incoming transition has finished. -
out-in
: Current component transitions out first, new component transitions in after outgoing transition has finished.
Example
<!-- fade out first, then fade in --> <component :is="view" transition="fade" transition-mode="out-in"> :is="view" transition="fade" transition-mode="out-in"> </component>
.fade-transition { transition: opacity .3s ease; } .fade-enter, .fade-leave { opacity: 0; }
Misc
Components and v-for
You can directly use v-for
on the custom component, like any normal element:
<my-component v-for="item in items"></my-component>
However, this won’t pass any data to the component, because components have isolated scopes of their own. In order to pass the iterated data into the component, we should also use props:
<my-component v-for="item in items" :item="item" :index="$index"> v-for="item in items" :item="item" :index="$index"> </my-component>
The reason for not automatically injecting item
into the component is because that makes the component tightly coupled to how v-for
works. Being explicit about where its data comes from makes the component reusable in other situations.
Authoring Reusable Components
When authoring components, it is good to keep in mind whether you intend to reuse this component somewhere else later. It is OK for one-off components to have some tight coupling with each other, but reusable components should define a clean public interface.
The API for a Vue.js component essentially comes in three parts - props, events and slots:
-
Props allow the external environment to feed data to the component;
-
Events allow the component to trigger actions in the external environment;
-
Slots allow the external environment to insert content into the component’s view structure.
With the dedicated shorthand syntax for v-bind
and v-on
, the intents can be clearly and succinctly conveyed in the template:
<my-component :foo="baz" :bar="qux" @event-a="doThis" @event-b="doThat"> :foo="baz" :bar="qux" @event-a="doThis" @event-b="doThat"> <!-- content --> <img slot="icon" src="..."> <p slot="main-text">Hello!</p> </my-component>
Async Components
In large applications, we may need to divide the app into smaller chunks, and only load a component from the server when it is actually needed. To make that easier, Vue.js allows you to define your component as a factory function that asynchronously resolves your component definition. Vue.js will only trigger the factory function when the component actually needs to be rendered, and will cache the result for future re-renders. For example:
Vue.component('async-example', function (resolve, reject) { setTimeout(function () { resolve({ template: '<div>I am async!</div>' }) }, 1000) })
The factory function receives a resolve
callback, which should be called when you have retrieved your component definition from the server. You can also call reject(reason)
to indicate the load has failed. The setTimeout
here is simply for demonstration; How to retrieve the component is entirely up to you. One recommended approach is to use async components together with Webpack’s code-splitting feature:
Vue.component('async-webpack-example', function (resolve) { // this special require syntax will instruct webpack to // automatically split your built code into bundles which // are automatically loaded over ajax requests. require(['./my-async-component'], resolve) })
Assets Naming Convention
Some assets, such as components and directives, appear in templates in the form of HTML attributes or HTML custom tags. Since HTML attribute names and tag names are case-insensitive, we often need to name our assets using kebab-case instead of camelCase, which can be a bit inconvenient.
Vue.js actually supports naming your assets using camelCase or PascalCase, and automatically resolves them as kebab-case in templates (similar to the name conversion for props):
// in a component definition components: { // register using camelCase myComponent: { /*... */ } }
<!-- use dash case in templates --> <my-component></my-component>
This works nicely with ES6 object literal shorthand:
// PascalCase import TextBox from './components/text-box'; import DropdownMenu from './components/dropdown-menu'; export default { components: { // use in templates as <text-box> and <dropdown-menu> TextBox, DropdownMenu } }
Recursive Component
Components can recursively invoke itself in its own template, however, it can only do so when it has the name
option:
var StackOverflow = Vue.extend({ name: 'stack-overflow', template: '<div>' + // recursively invoke self '<stack-overflow></stack-overflow>' + '</div>' })
A component like the above will result in a “max stack size exceeded” error, so make sure recursive invocation is conditional. When you register a component globally using Vue.component()
, the global ID is automatically set as the component’s name
option.
Fragment Instance
When you use the template
option, the content of the template will replace the element the Vue instance is mounted on. It is therefore recommended to always have a single root-level, plain element in templates.
Instead of templates like this:
<div>root node 1</div> <div>root node 2</div>
Prefer this:
<div> I have a single root node! <div>node 1</div> <div>node 2</div> </div>
There are multiple conditions that will turn a Vue instance into a fragment instance:
- Template contains multiple top-level elements.
- Template contains only plain text.
- Template contains only another component (which can potentially be a fragment instance itself).
- Template contains only an element directive, e.g.
<partial>
or vue-router’s<router-view>
. - Template root node has a flow-control directive, e.g.
v-if
orv-for
.
The reason is that all of the above cause the instance to have an unknown number of top-level elements, so it has to manage its DOM content as a fragment. A fragment instance will still render the content correctly. However, it will not have a root node, and its $el
will point to an “anchor node”, which is an empty Text node (or a Comment node in debug mode).
What’s more important though, is that non-flow-control directives, non-prop attributes and transitions on the component element will be ignored, because there is no root element to bind them to:
<!-- doesn't work due to no root element --> <example v-show="ok" transition="fade"></example> <!-- props work --> <example :prop="someData"></example> <!-- flow control works, but without transitions --> <example v-if="ok"></example>
There are, of course, valid use cases for fragment instances, but it is in general a good idea to give your component template a single, plain root element. It ensures directives and attributes on the component element to be properly transferred, and also results in slightly better performance.
Inline Template
When the inline-template
special attribute is present on a child component, the component will use its inner content as its template, rather than treating it as distributed content. This allows more flexible template-authoring.
<my-component inline-template> <p>These are compiled as the component's own template</p> <p>Not parent's transclusion content.</p> </my-component>
However, inline-template
makes the scope of your templates harder to reason about, and makes the component’s template compilation un-cachable. As a best practice, prefer defining templates inside the component using the template
option.
Please login to continue.