组件:具有预定义选项的实例
组件是一种抽象,允许我们使用小型、独立和通常可复用的组件构建大型应用
通过 prop ,我们可以实现父子组件之间的解耦,有了属性,子组件中就可以使用更为复杂的模板和逻辑,比如如下
<!-- 复制到 html 中可以直接运行哦,看看是什么结果吧 -->
<div id="main">
<ol>
<!-- 子组件 -->
<todo-item v-for="todo in groceryList"
:key="todo.id"
:todo="todo">
</todo-item>
</ol>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
const TodoList = {
data() {
return {
groceryList: [
{ id: 0, text: 'Vegetables' },
{ id: 1, text: 'Cheese' },
{ id: 2, text: 'Meat' }
]
}
}
}
const app = Vue.createApp(TodoList)
// 父组件
// 试想,如果没有定义在父组件中定义 todo 这个属性,该怎样实现同样的功能呢? 不好实现呀
app.component('todo-item', {
props: ['todo'],
template: `<li>{{ todo.text }}</li>`
})
app.mount('#main')
</script>
通过 createApp 函数创建创建了应用实例(application instance),它表明 Vue 应用的开始
这个应用实例未来会通过 mount 函数
变成根组件实例,这就是应用实例最终的命运吗?有点科幻色彩~
const app = Vue.createApp(RootComponent)
这个应用实例用来注册一个全局信息,在整个 Vue 应用中的所有组件都可以使用这个全局信息
应用实例相当于一个进程,而组件就相当于一个线程,线程之间可以相互合作,并共享进程的信息
const RootComponentConfig = {
// data() 函数是用于配置根组件的其中之一的选项,此外还有 methods()、computed() 等
data() {
return {
a: 123
}
}
}
// RootComponentConfig 用于配置根组件实例,虽然根组件是调用 mount() 才返回的,但是就好像提前占一个坑,预定一样
const applicationInstance = Vue.createApp(RootComponentConfig)
const rootComponentInstance = applicationInstance.mount('#app')
当我们为我们的应用实例使用 mount 方法
(即挂载)时,表明这个应用实例(此时应该是根组件了)被用作渲染的起点
应用实例调用方法(除了mount 方法)后,还是会返回一个应用实例(比如 app 是一个应用实例,调用 app.component( /* xxx */ ) 之后,还是会返回一个应用实例)
但是,mount 方法不会返回应用实例,而是会返回根组件实例
应用实例最终需要挂载到一个 DOM 元素中() 如<div id="app"></div>
,于是我们给 mount 传递 #app
mount 内部是这样实现的
const res = document.querySelector('#app')
return res
每个组件都会有自己的组件实例,比如
// 会有一个组件实例
app.component('haha', {
template: `
<h1>
haha
</h1>`
})
在应用中的组件实例都会共享根组件实例
我们可以在组件实例中添加用户自定义的属性,如methods
, props
, computed
, inject
and setup
这些所有的自定义属性都可以在模板中使用,比如
<!-- 可复制直接运行 -->
<div id="main">
<!-- 在模板中使用了组件实例中的 data() 属性 -->
{{ a }}
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
const app = Vue.createApp({
data() {
return {
a: 123
}
},
})
app.mount('#main')
</script>
看英文版是真的过瘾,比中文版更好理解
component is an abstraction that allows us to build large-scale applications composed of small, self-contained, and often reusable components.
component :instance with pre-defined options
In Vue, a component
is essentially an ❤️instance with pre-defined options. Registering a component in Vue is straightforward
// Create Vue application
const app = Vue.createApp(...)
// Define a new component called todo-item
app.component('todo-item', {
template: `<li>This is a todo</li>`
})
// Mount Vue application
app.mount(...) // mount will returns the root component instance
But this would render the same text for every todo, which is not super interesting. We should be able to pass data from the parent scope into child components. Let’s modify the component definition to make it accept a prop
app.component('todo-item', {
props: ['todo'],
template: `<li>{{ todo.text }}</li>`
})
we have managed to separate our app into two smaller units, and the child is reasonably well-decoupled from the parent via the ⭐️props interface. We can now further improve our <todo-item>
component with more complex template and logic without affecting the parent app.
In a large application, it is necessary to divide the whole app into components to make development manageable. We will talk a lot more about components later in the guide, but here’s an (imaginary) example of what an app’s template might look like with components:
<div id="app">
<app-nav></app-nav>
<app-view>
<app-sidebar></app-sidebar>
<app-content></app-content>
</app-view>
</div>
Every Vue application starts by creating a new application instance
with the createApp
function:
// app is a new application instance
const app = Vue.createApp({
/* options */
})
The application instance is used to register ‘globals’ that can then be used by components within that application. We’ll discuss that in detail later in the guide but as a quick example:
const app = Vue.createApp({})
// application instance is used by components
app.component('SearchInput', SearchInputComponent)
app.directive('focus', FocusDirective)
app.use(LocalePlugin)
Most of the methods exposed by the application instance
return that same instance (chain)
The options passed to createApp
are used to configure the root component.
That component is used as the starting point for rendering when we mount the application.
An application needs to be mounted into a DOM element. For example, if we want to mount a Vue application into <div id="app"></div>
, we should pass #app
Unlike most of the application methods, mount
does not return the application. Instead it ⭐️returns the root component instance.
Although not strictly associated with the MVVM pattern (opens new window), Vue’s design was partly inspired by it.
As a convention, we often use the variable vm
(short for ViewModel) to refer to a ⭐️component instance.
While all the examples on this page only need a single component, most real applications are organized into a tree of nested, reusable components. For example, a Todo application’s component tree might look like this:
Root Component
└─ TodoList
├─ TodoItem
│ ├─ DeleteTodoButton
│ └─ EditTodoButton
└─ TodoListFooter
├─ ClearTodosButton
└─ TodoListStatistics
Each component will have its own component instance, vm
. For some components, such as TodoItem
, there will likely be multiple instances rendered at any one time. All of the component instances in this application will share the same application instance.
We’ll talk about the component system in detail later. For now, just be aware that the root component isn’t really any different from any other component. The configuration options are the same, as is the behavior of the corresponding component instance
difference between an instance and an obejct ?
Once you instantiate a class (using new), that instantiated thing becomes an object.
Instance
refers to the copy of the object
at a particular time whereas object refers to the memory address of the class
.
an object represents a set of instances while an instance is a certain, specific representation.
Earlier in the guide we met data
properties. Properties defined in data
are exposed via the component instance:
const app = Vue.createApp({
data() {
return { count: 4 }
}
})
const vm = app.mount('#app')
console.log(vm.count) // => 4
There are various other component options that add user-defined properties to the component instance
such as methods
, props
, computed
, inject
and setup
.
All of the properties of the component instance, no matter how they are defined, will be accessible in the component's template
.
Vue also exposes some built-in properties
via the component instance, such as $attrs
and $emit
.
These properties all have a $
prefix to avoid conflicting with user-defined property names
Each component instance goes through a series of initialization steps when it’s created
for example, it needs to set up data observation, compile the template, mount the instance to the DOM, and update the DOM when data changes.
Along the way, it also runs functions called lifecycle hooks
, giving users the opportunity to add their own code at specific stages.
For example, the created hook can be used to run code after an instance is created:
Vue.createApp({
data() {
return { count: 1 }
},
created() {
// `this` points to the vm instance
console.log('count is: ' + this.count) // => "count is: 1"
}
})
There are also other hooks which will be called at different stages of the instance’s lifecycle, such as mounted, updated, and unmounted. All lifecycle hooks are called with their this
context pointing to the current active instance invoking it.
TIP
Don’t use arrow functions (opens new window)on an options property or callback, such as created: () => console.log(this.a)
or vm.$watch('a', newValue => this.myMethod())
. Since an arrow function doesn’t have a this
, this
will be treated as any other variable and lexically looked up through parent scopes until found, often resulting in errors such as Uncaught TypeError: Cannot read property of undefined
or Uncaught TypeError: this.myMethod is not a function
.
Components are reusable instances with a name
We can use component as a custom element inside a root instance:
Since components are reusable instances, they accept the same options as a root instance, such as data
, computed
, watch
, methods
, and lifecycle hooks
Components can be reused as many times as you want
⭐️ each time you use a component, a new instance of it is created.(separate )
To use these components in templates, they must be registered so that Vue knows about them. There are two types of component registration: global and local
Globally registered components can be used in the template of any component within the app.
const app = Vue.createApp({})
app.component('my-component-name', {
// ... options ...
})
const app = Vue.createApp({
components: {
'component-a': ComponentA,
'component-b': ComponentB
}
})
locally registered components are not also available in subcomponents.
if you wanted ComponentA
to be available in ComponentB
, you’d have to use:
const ComponentA = {
/* ... */
}
const ComponentB = {
components: {
'component-a': ComponentA
}
// ...
}
If you’re still here, then it’s likely you’re using a module system, such as with Babel and Webpack. In these cases, we recommend creating a components
directory, with each component in its own file.
Then you’ll need to import each component you’d like to use, before you locally register it. For example, in a hypothetical ComponentB.js
or ComponentB.vue
file:
import ComponentA from './ComponentA'
import ComponentC from './ComponentC'
export default {
components: {
ComponentA,
ComponentC
}
// ...
}
Now both ComponentA
and ComponentC
can be used inside ComponentB
's template.
component won’t be useful unless you can pass data to it, such as the title and content of the specific post we want to display. That’s where props come in.
Props are custom attributes you can register on a component. To pass a title to our blog post component, we can include it in the list of props this component accepts, using the props
option:
const app = Vue.createApp({})
app.component('blog-post', {
props: ['title'],
template: `<h4>{{ title }}</h4>`
})
app.mount('#blog-post-demo')
When a value is passed to a prop attribute, it becomes a property on that component instance. The value of that property is accessible within the template, just like any other component property.
A component can have as many props as you like and, by default, any value can be passed to any prop.
Once a prop is registered, you can pass data to it as a custom attribute
, like this:
<div id="blog-post-demo" class="demo">
<blog-post title="My journey with Vue"></blog-post>
<blog-post title="Blogging with Vue"></blog-post>
<blog-post title="Why Vue is so fun"></blog-post>
</div>
we can use v-bind
to dynamically pass props.
This is especially useful when you don’t know the exact content you’re going to render ahead of time.
you’ll want every prop to be a specific type of value. In these cases, you can list props as an object,
where the properties’ names and values contain(分别包含) the prop names and types, respectively:
props: {
title: String,
likes: Number,
isPublished: Boolean,
commentIds: Array,
author: Object,
callback: Function,
contactsPromise: Promise // or any other constructor
}
So far, you’ve seen props passed a static value, like in:
<blog-post title="My journey with Vue"></blog-post>
You’ve also seen props assigned dynamically with v-bind
or its shortcut, the :
character, such as in:
<!-- Dynamically assign the value of a variable -->
<blog-post :title="post.title"></blog-post>
<!-- Dynamically assign the value of a complex expression -->
<blog-post :title="post.title + ' by ' + post.author.name"></blog-post>
In the two examples above, we happen to pass string values, but any type of value can actually be passed to a prop.
<!-- Even though `42` is static, we need v-bind to tell Vue that -->
<!-- this is a JavaScript expression rather than a string. -->
<blog-post :likes="42"></blog-post>
<!-- Dynamically assign to the value of a variable. -->
<blog-post :likes="post.likes"></blog-post>
<!-- Including the prop with no value will imply `true`. -->
<blog-post is-published></blog-post>
<!-- Even though `false` is static, we need v-bind to tell Vue that -->
<!-- this is a JavaScript expression rather than a string. -->
<blog-post :is-published="false"></blog-post>
<!-- Dynamically assign to the value of a variable. -->
<blog-post :is-published="post.isPublished"></blog-post>
<!-- Even though the array is static, we need v-bind to tell Vue that -->
<!-- this is a JavaScript expression rather than a string. -->
<blog-post :comment-ids="[234, 266, 273]"></blog-post>
<!-- Dynamically assign to the value of a variable. -->
<blog-post :comment-ids="post.commentIds"></blog-post>
<!-- Even though the object is static, we need v-bind to tell Vue that -->
<!-- this is a JavaScript expression rather than a string. -->
<blog-post
:author="{
name: 'Veronica',
company: 'Veridian Dynamics'
}"
></blog-post>
<!-- Dynamically assign to the value of a variable. -->
<blog-post :author="post.author"></blog-post>
If you want to pass all the properties of an object as props, you can use v-bind
without an argument (v-bind
instead of :prop-name
). For example, given a post
object:
post: {
id: 1,
title: 'My Journey with Vue'
}
The following template:
<blog-post v-bind="post"></blog-post>
Will be equivalent to:
<blog-post v-bind:id="post.id" v-bind:title="post.title"></blog-post>
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.
In addition, every time the parent component is updated, all props in the child component will be refreshed with the latest value.
This means you should not attempt to mutate a prop inside a child component. If you do, Vue will warn you in the console.
There are usually two cases where it’s tempting to mutate a prop:
props: ['initialCounter'],
data() {
return {
counter: this.initialCounter
}
}
<div id="main">
<my-component count='0'></my-component>
<my-component count='1'></my-component>
<my-component count='2'></my-component>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
const app = Vue.createApp({})
app.component('myComponent', {
props: ['count'],
data() {
return {
aCount: this.count
}
},
template: `
<button @click="aCount++">add</button>
{{aCount}} <br/><br/>
`
})
app.mount('#main')
</script>
props: ['size'],
computed: {
normalizedSize() {
return this.size.trim().toLowerCase()
}
}
<div id="main">
<my-component text='ASD'></my-component>
<my-component text='QWE'></my-component>
<my-component text='ZXC'></my-component>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
const app = Vue.createApp({})
app.component('myComponent', {
props: ['text'],
computed:{
changeCount(){
return this.text.trim().toLowerCase()
}
},
template: `
<div>{{changeCount}} </div>
<br/><br/>
`
})
app.mount('#main')
</script>
Note that objects and arrays in JavaScript are passed by reference, so if the prop is an array or object, mutating the object or array itself inside the child component will affect parent state.
<div id="main">
<my-component :haha="['dwa','sdaw','daw']"></my-component>
<my-component :haha="['dwa','sdaw','daw']"></my-component>
<my-component :haha="['dwa','sdaw','daw']"></my-component>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
const app = Vue.createApp({})
app.component('myComponent', {
props: ['haha'],
methods:{
pushHaha(){
console.log(this.haha);
this.haha.push("asd")
// this.haha will change
console.log(this.haha);
}
},
template: `
<button @click="pushHaha()">pushHaha</button>
this will not change: {{haha}}
<br/><br/>
`
})
app.mount('#main')
</script>
Components can specify requirements for their props, such as the types you’ve already seen. If a requirement isn’t met, Vue will warn you in the browser’s JavaScript console. This is especially useful when developing a component that’s intended to be used by others.
To specify prop validations, you can provide an object
with validation requirements to the value of props
, instead of an array of strings. For example:
app.component('my-component', {
props: {
// Basic type check (`null` and `undefined` values will pass any type validation)
propA: Number,
// Multiple possible types
propB: [String, Number],
// Required string
propC: {
type: String,
required: true
},
// Number with a default value
propD: {
type: Number,
default: 100
},
// Object with a default value
propE: {
type: Object,
// Object or array defaults must be returned from
// a factory function
default() {
return { message: 'hello' }
}
},
// Custom validator function
propF: {
validator(value) {
// The value must match one of these strings
return ['success', 'warning', 'danger'].includes(value)
}
},
// Function with a default value
propG: {
type: Function,
// Unlike object or array default, this is not a factory function - this is a function to serve as a default value
default() {
return 'Default function'
}
}
}
})
In JavaScript, any function can return an object.
But if without the new
keyword, it’s a factory function.
HTML attribute names are case-insensitive
, so browsers will interpret any uppercase characters as lowercase. That means when you’re using in-DOM templates, camelCased prop names
need to use their kebab-cased (hyphen-delimited)
equivalents:
const app = Vue.createApp({})
app.component('blog-post', {
// camelCase in JavaScript
props: ['postTitle'],
template: '<h3>{{ postTitle }}</h3>'
})
<!-- kebab-case in HTML -->
<blog-post post-title="hello!"></blog-post>
if you’re using string templates, this limitation does not apply.
字符串模板就是写在vue中的template中定义的模板,如.vue的单文件组件模板和定义组件时template属性值的模板。
字符串模板不会在页面初始化参与页面的渲染,会被vue进行解析编译之后再被浏览器渲染,所以不受限于html结构和标签的命名。
A component non-prop attribute is an attribute or event listener that is passed to a component, but does not have a corresponding property defined in props or emits. Common examples of this include class
, style
, and id
attributes. You can access those attributes via $attrs
property.
When a component returns a single root node, non-prop attributes
will automatically be added to the root node's attributes
.
For example, in the instance of a date-picker component:
app.component('date-picker', {
template: `
<div class="date-picker">
<input type="datetime-local" />
</div>
`
})
In the event we need to define the status of the date-picker component via a data-status
property, it will be applied to the root node (i.e., div.date-picker
).
<!-- Date-picker component with a non-prop attribute -->
<date-picker data-status="activated"></date-picker>
<!-- Rendered date-picker component -->
<div class="date-picker" data-status="activated">
<input type="datetime-local" />
</div>
Same rule applies to the event listeners:
<date-picker @change="submitChange"></date-picker>
app.component('date-picker', {
created() {
console.log(this.$attrs) // { onChange: () => {} }
}
})
@xxx => onXxx
This might be helpful when we have an HTML element with change
event as a root element of date-picker
.
app.component('date-picker', {
template: `
<select>
<option value="1">Yesterday</option>
<option value="2">Today</option>
<option value="3">Tomorrow</option>
</select>
`
})
In this case, change
event listener is passed from the parent component to the child and it will be triggered on native <select>
change
event. We won’t need to emit an event from the date-picker
explicitly:
<div id="date-picker" class="demo">
<date-picker @change="showChange"></date-picker>
</div>
const app = Vue.createApp({
methods: {
showChange(event) {
console.log(event.target.value) // will log a value of the selected option
}
}
})
If you do not want a component to automatically inherit attributes, you can set inheritAttrs: false
in the component’s options.
The common scenario for disabling an attribute inheritance is when attributes need to be applied to other elements besides the root node.
By setting the inheritAttrs
option to false
, you can control to apply to other elements attributes to use the component’s $attrs
property, which includes all attributes not included to component props
and emits
properties (e.g., class
, style
, v-on
listeners, etc.).
Using our date-picker component example from the previous section, in the event we need to apply all non-prop attributes to the input
element rather than the root div
element, this can be accomplished by using the v-bind
shortcut.
app.component('date-picker', {
inheritAttrs: false,
template: `
<div class="date-picker">
<input type="datetime-local" v-bind="$attrs" />
</div>
`
})
With this new configuration, our data-status
attribute will be applied to our input
element!
<!-- Date-picker component with a non-prop attribute -->
<date-picker data-status="activated"></date-picker>
<!-- Rendered date-picker component -->
<div class="date-picker">
<input type="datetime-local" data-status="activated" />
</div>
Unlike single root node components, components with multiple root nodes do not have an automatic attribute fallthrough behavior.
If $attrs
are not bound explicitly, a runtime warning will be issued.
<custom-layout id="custom-layout" @click="changeValue"></custom-layout>
// This will raise a warning
app.component('custom-layout', {
template: `
<header>...</header>
<main>...</main>
<footer>...</footer>
`
})
// No warnings, $attrs are passed to <main> element
app.component('custom-layout', {
template: `
<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>
`
})
As we develop our <blog-post>
component, some features may require communicating back up to the parent. For example, we may decide to include an accessibility feature to enlarge the text of blog posts, while leaving the rest of the page its default size.
In the parent, we can support this feature by adding a postFontSize
data property:
const App = {
data() {
return {
posts: [
/* ... */
],
postFontSize: 1
}
}
}
Which can be used in the template to control the font size of all blog posts:
<div id="blog-posts-events-demo">
<div :style="{ fontSize: postFontSize + 'em' }">
<blog-post
v-for="post in posts"
:key="post.id"
:title="post.title"
></blog-post>
</div>
</div>
Now let’s add a button to enlarge the text right before the content of every post:
app.component('blog-post', {
props: ['title'],
template: `
<div class="blog-post">
<h4>{{ title }}</h4>
<button>
Enlarge text
</button>
</div>
`
})
The problem is, this button doesn’t do anything:
<button>
Enlarge text
</button>
When we click on the button, we need to communicate to the parent that it should enlarge the text of all posts. To solve this problem, component instances provide a custom events system
. The parent can choose to listen to any event on the child component instance with v-on
or @
, just as we would with a native DOM event:
<blog-post ... @enlarge-text="postFontSize += 0.1"></blog-post>
Then the child component can emit an event on itself by calling the built-in $emit
method, passing the name of the event:
<button @click="$emit('enlargeText')">
Enlarge text
</button>
Thanks to the @enlarge-text="postFontSize += 0.1"
listener, the parent will receive the event and update the value of postFontSize
.
<div id="blog-posts-events-demo" class="demo">
<div :style="{ fontSize: postFontSize + 'em' }">
<blog-post
v-for="post in posts"
:key="post.id"
:title="post.title"
@enlarge-text="postFontSize += 0.1"
></blog-post>
</div>
</div>
const app = Vue.createApp({
data() {
return {
posts: [
{ id: 1, title: 'My journey with Vue'},
{ id: 2, title: 'Blogging with Vue'},
{ id: 3, title: 'Why Vue is so fun'}
],
postFontSize: 1
}
}
})
app.component('blog-post', {
props: ['title'],
template: `
<div class="blog-post">
<h4>{{ title }}</h4>
<button @click="$emit('enlargeText')">
Enlarge text
</button>
</div>
`
})
app.mount('#blog-posts-events-demo')
We can list emitted events in the component’s emits
option:
app.component('blog-post', {
props: ['title'],
emits: ['enlargeText']
})
This will allow you to check all the events that a component emits and optionally validate them.
When a native event (e.g., click
) is defined in the emits
option, the component event will be used instead of a native event listener.
Similar to prop type validation, an emitted event can be validated if it is defined with the Object syntax
instead of the array syntax.
To add validation, the event is assigned a function that receives the arguments passed to the $emit
call and returns a boolean to indicate whether the event is valid or not.
app.component('custom-form', {
emits: {
// No validation
click: null,
// Validate submit event
submit: ({ email, password }) => {
if (email && password) {
return true
} else {
console.warn('Invalid submit event payload!')
return false
}
}
},
methods: {
submitForm(email, password) {
this.$emit('submit', { email, password })
}
}
})
It’s sometimes useful to emit a specific value with an event. For example, we may want the <blog-post>
component to be in charge of how much to enlarge the text by. In those cases, we can pass a second parameter to $emit
to provide this value:
<button @click="$emit('enlargeText', 0.1)">
Enlarge text
</button>
Then when we listen to the event in the parent, we can access the emitted event’s value with $event
:
<blog-post ... @enlarge-text="postFontSize += $event"></blog-post>
Or, if the event handler is a method:
<blog-post ... @enlarge-text="onEnlargeText"></blog-post>
Then the value will be passed as the first parameter of that method:
methods: {
onEnlargeText(enlargeAmount) {
this.postFontSize += enlargeAmount
}
}
v-model
on ComponentsCustom events can also be used to create custom inputs that work with v-model
. Remember that:
<input v-model="searchText" />
does the same thing as:
<input :value="searchText" @input="searchText = $event.target.value" />
When used on a component, v-model
instead does this:
<custom-input
:model-value="searchText"
@update:model-value="searchText = $event"
></custom-input>
WARNING
Please note we used model-value
with kebab-case here because we are working with in-DOM templates. You can find a detailed explanation on kebab-cased vs camelCased attributes in the DOM Template Parsing Caveats section
For this to actually work though, the <input>
inside the component must:
value
attribute to the modelValue
propinput
, emit an update:modelValue
event with the new valueHere’s that in action:
app.component('custom-input', {
props: ['modelValue'],
emits: ['update:modelValue'],
template: `
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
>
`
})
We can modify these names passing an argument to v-model
:
<my-component v-model:title="bookTitle"></my-component>
In this case, child component will expect a title
prop and emits update:title
event to sync:
app.component('my-component', {
props: {
title: String
},
emits: ['update:title'],
template: `
<input
type="text"
:value="title"
@input="$emit('update:title', $event.target.value)">
`
})
Now v-model
should work perfectly with this component:
<custom-input v-model="searchText"></custom-input>
Another way of implementing v-model
within this component is to use the ability of computed
properties to define a getter and setter. The get
method should return the modelValue
property and the set
method should emit the corresponding event:
app.component('custom-input', {
props: ['modelValue'],
emits: ['update:modelValue'],
template: `
<input v-model="value">
`,
computed: {
value: {
get() {
return this.modelValue
},
set(value) {
this.$emit('update:modelValue', value)
}
}
}
})
That’s all you need to know about custom component events for now, but once you’ve finished reading this page and feel comfortable with its content, we recommend coming back later to read the full guide on Custom Events.
<!-- 复制可直接运行 -->
<div id="app">
<!-- For v-model bindings with arguments(no arguments:modelValue), the generated prop name will be arg + "Modifiers" -->
<my-component v-model:haha.capitalize="myText"></my-component>
{{myText}}
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
const app = Vue.createApp({
data() {
return {
myText: '',
}
}
})
app.component('my-component', {
props: {
// ① 定义 haha
haha: String,
hahaModifiers: {
default: () => ({})
},
},
// ④ haha
emits: ['update:haha'],
methods: {
emitValue(e) {
let value = e.target.value
if (this.hahaModifiers.capitalize) {
value = value.charAt(0).toUpperCase() + value.slice(1)
}
// ③ haha
this.$emit('update:haha', value)
}
},
created() {
// haha + Modifiers
console.log(this.hahaModifiers) // { capitalize: true }
},
// ② haha
template: `<input
type="text"
:value="haha"
@input="emitValue">`
})
app.mount('#app')
</script>
If you are writing your Vue templates directly in the DOM, Vue will have to retrieve the template string from the DOM
Some HTML elements, such as <ul>
, <ol>
, <table>
and <select>
have restrictions on what elements can appear inside them, and some elements such as <li>
, <tr>
, and <option>
can only appear inside certain other elements.
This will lead to issues when using components with elements that have such restrictions. For example:
<table>
<blog-post-row></blog-post-row>
</table>
The custom component <blog-post-row>
will be hoisted out as invalid content, causing errors in the eventual rendered output.
We can use the special is
attribute]as a workaround
<table>
<tr is="vue:blog-post-row"></tr>
</table>
When the is
attribute is used on a native HTML element, it will be interpreted as a Customized built-in element , which is a native web platform feature.
TIP
When used on native HTML elements, the value of is
must be prefixed with vue:
in order to be interpreted as a Vue component. This is required to avoid confusion with native customized built-in elements (opens new window).
HTML attribute names are case-insensitive, so browsers will interpret any uppercase characters as lowercase. That means when you’re using in-DOM templates, camelCased prop names and event handler parameters need to use their kebab-cased (hyphen-delimited) equivalents:
// camelCase in JavaScript
app.component('blog-post', {
props: ['postTitle'],
template: `
<h3>{{ postTitle }}</h3>
`
})
<!-- kebab-case in HTML -->
<blog-post post-title="hello!"></blog-post>
Just like with HTML elements, it’s often useful to be able to pass content to a component, like this:
<alert-box>
Something bad happened.
</alert-box>
This can be achieved using Vue’s custom <slot>
element:
app.component('alert-box', {
template: `
<div class="demo-alert-box">
<strong>Error!</strong>
<slot></slot>
</div>
`
})
As you’ll see above, we use the <slot>
as a placeholder where we want the content to go – and that’s it. We’re done!
If <todo-button>
's template did not contain a <slot>
element,
any content provided between its opening and closing tag would be discarded.
When you want to use data inside a slot, such as in:
<todo-button>
Delete a {{ item.name }}
</todo-button>
That slot has access to the same instance properties (i.e. the same “scope”) as the rest of the template.
But trying to access action
would not work:
<todo-button action="delete">
Clicking here will {{ action }} an item
<!--
The `action` will be undefined, because this content is passed
_to_ <todo-button>, rather than defined _inside_ the
<todo-button> component.
-->
</todo-button>
Everything in the parent template
is compiled in parent scope
; everything in the child template
is compiled in the child scope
We might want the text “Submit” to be rendered inside the <button>
most of the time. To make “Submit” the fallback content, we can place it in between the <slot>
tags:
<button type="submit">
<slot>Submit</slot>
</button>
But if we provide content:
<submit-button>
Save
</submit-button>
Then the provided content will be rendered instead:
<button type="submit">
Save
</button>
the <slot>
element has a special attribute, name
, which can be used to assign a unique ID
to different slots so you can determine where content should be rendered: in a <base-layout>
component
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
A <slot>
outlet without name
implicitly has the name “default”.
To provide content to named slots, we need to use the v-slot
directive on a <template>
element, providing the name of the slot as v-slot
's argument:order is not importance when use the v-slot on a template element
<base-layout>
<template v-slot:header>
<h1>Here might be a page title</h1>
</template>
<template v-slot:default>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
</template>
<template v-slot:footer>
<p>Here's some contact info</p>
</template>
</base-layout>
The rendered HTML will be:
<div class="container">
<header>
<h1>Here might be a page title</h1>
</header>
<main>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
</main>
<footer>
<p>Here's some contact info</p>
</footer>
</div>
Note that v-slot
can only be added to a <template>
(with one exception)
Sometimes, it’s useful for slot content to have access to data only available in the child component
. It’s a common case when a component is used to render an array of items
, and we want to be able to customize the way each item is rendered.
For example, we have a component, containing a list of todo-items.
app.component('todo-list', {
data() {
return {
items: ['Feed a cat', 'Buy milk']
}
},
template: `
<ul>
<li v-for="(item, index) in items">
{{ item }}
</li>
</ul>
`
})
We might want to replace the {{ item }}
with a <slot>
to customize it on parent component:
<todo-list>
<i class="fas fa-check"></i>
<span class="green">{{ item }}</span>
</todo-list>
That won’t work, because we are providing the slot content from its parent.
To make item
available to the slot content provided by the parent, we can add a <slot>
element and bind it as an attribute:
<ul>
<li v-for="( item, index ) in items">
<slot :item="item"></slot>
</li>
</ul>
You can bind as many attributes to the slot
as you need:
<ul>
<li v-for="( item, index ) in items">
<slot :item="item" :index="index" :another-attribute="anotherAttribute"></slot>
</li>
</ul>
Attributes bound to a <slot>
element are called slot props
. Now, in the parent scope, we can use v-slot
with a value to define a name for the slot props we’ve been provided:
<todo-list>
<template v-slot:default="slotProps">
<i class="fas fa-check"></i>
<span class="green">{{ slotProps.item }}</span>
</template>
</todo-list>
In this example, we’ve chosen to name the object containing all our slot props slotProps
, but you can use any name you like
<!-- 复制可直接运行 -->
<div id="app">
<todo>
<!-- { "aSlotProp": { "name": "OK" } } -->
<template v-slot:default="allSlotProps">
{{allSlotProps.aSlotProp.name}}
</template>
</todo>
<!-- Abbreviated default -->
<todo v-slot="allSlotProps">
{{allSlotProps.aSlotProp.name}}
</todo>
<!-- Named Slots Shorthand -->
<todo #default="allSlotProps">
{{allSlotProps.aSlotProp.name}}
</todo>
<!-- Named Slots Other -->
<todo #other="allSlotProps"></todo>
<!-- Destructuring Slot Props -->
<todo #default="{ aSlotProp }">
{{aSlotProp.name}}
</todo>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
const app = Vue.createApp({
})
app.component('todo', {
data() {
return {
item: {
name: 'OK'
}
}
},
template: `
<button class="btn-primary">
<slot :aSlotProp="item">haha</slot>
</button>
`
})
app.mount('#app')
</script>
In cases like above, when only the default slot is provided content, the component’s tags can be used as the slot’s template. This allows us to use v-slot
directly on the component:
<todo-list v-slot:default="slotProps">
<i class="fas fa-check"></i>
<span class="green">{{ slotProps.item }}</span>
</todo-list>
This can be shortened even further. Just as non-specified content is assumed to be for the default slot, v-slot
without an argument is assumed to refer to the default slot:
<todo-list v-slot="slotProps">
<i class="fas fa-check"></i>
<span class="green">{{ slotProps.item }}</span>
</todo-list>
Note that the abbreviated syntax for default slot cannot be mixed with named slots, as it would lead to scope ambiguity:
<!-- INVALID, will result in warning -->
<todo-list v-slot="slotProps">
<i class="fas fa-check"></i>
<span class="green">{{ slotProps.item }}</span>
<template v-slot:other="otherSlotProps">
slotProps is NOT available here
</template>
</todo-list>
Whenever there are multiple slots, use the full <template>
based syntax for all slots:
<todo-list>
<template v-slot:default="slotProps">
<i class="fas fa-check"></i>
<span class="green">{{ slotProps.item }}</span>
</template>
<template v-slot:other="otherSlotProps">
...
</template>
</todo-list>
Internally, scoped slots work by wrapping your slot content in a function passed a single argument
:
function (slotProps) {
// ... slot content ...
}
That means the value of v-slot
can actually accept any valid JavaScript expression that can appear in the argument position of a function definition. So you can also use ES2015 destructuring (opens new window)to pull out specific slot props, like so:
<todo-list v-slot="{ item }">
<i class="fas fa-check"></i>
<span class="green">{{ item }}</span>
</todo-list>
This can make the template much cleaner, especially when the slot provides many props. It also opens other possibilities, such as renaming props, e.g. item
to todo
:
<todo-list v-slot="{ item: todo }">
<i class="fas fa-check"></i>
<span class="green">{{ todo }}</span>
</todo-list>
You can even define fallbacks, to be used in case a slot prop is undefined:
<todo-list v-slot="{ item = 'Placeholder' }">
<i class="fas fa-check"></i>
<span class="green">{{ item }}</span>
</todo-list>
Dynamic directive arguments also work on v-slot
, allowing the definition of dynamic slot names:
<base-layout>
<template v-slot:[dynamicSlotName]>
...
</template>
</base-layout>
Similar to v-on
and v-bind
, v-slot
also has a shorthand, replacing everything before the argument (v-slot:
) with the special symbol #
. For example, v-slot:header
can be rewritten as #header
:
<base-layout>
<template #header>
<h1>Here might be a page title</h1>
</template>
<template #default>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
</template>
<template #footer>
<p>Here's some contact info</p>
</template>
</base-layout>
However, just as with other directives, the shorthand is only available when an argument is provided. That means the following syntax is invalid:
<!-- This will trigger a warning -->
<todo-list #="{ item }">
<i class="fas fa-check"></i>
<span class="green">{{ item }}</span>
</todo-list>
Instead, you must always specify the name of the slot if you wish to use the shorthand:
<todo-list #default="{ item }">
<i class="fas fa-check"></i>
<span class="green">{{ item }}</span>
</todo-list>
Sometimes, it’s useful to dynamically switch between components, like in a tabbed interface:
The above is made possible by Vue’s <component>
element with the special is
attribute:
<!-- 复制可直接运行 -->
<div id="dynamic-component-demo" class="demo">
<button v-for="tab in tabs" :key="tab" @click="currentTab = tab">
{{ tab }}
</button>
<component :is="currentTab" class="tab"></component>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
const app = Vue.createApp({
data() {
return {
currentTab: 'Home',
tabs: ['Home', 'Posts', 'Archive']
}
},
})
app.component('Home', {
template: `<div>Home component</div>`
})
app.component('Posts', {
template: `<div>Posts component</div>`
})
app.component('Archive', {
template: `<div>Archive component</div>`
})
app.mount('#dynamic-component-demo')
</script>
<!-- Component changes when currentTab changes -->
<component :is="currentTab"></component>
In the example above, currentTabComponent
can contain either:
You can also use the is
attribute to create regular HTML elements.
each time you switch to a new tab, Vue creates a new instance
Recreating dynamic components is normally useful behavior, but in this case, we’d really like those tab component instances to be cached
once they’re created for the first time.
To solve this problem, we can wrap our dynamic component with a <keep-alive>
element
<!-- Inactive components will be cached! -->
<keep-alive>
<component :is="currentTabComponent"></component>
</keep-alive>