-
Notifications
You must be signed in to change notification settings - Fork 4.7k
Expand the page on render functions, especially their use of slots #712
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
fa58948
902addd
f56db92
fc2f8ba
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -62,9 +62,7 @@ const app = Vue.createApp({}) | |
|
||
app.component('anchored-heading', { | ||
render() { | ||
const { h } = Vue | ||
|
||
return h( | ||
return Vue.h( | ||
'h' + this.level, // tag name | ||
{}, // props/attributes | ||
this.$slots.default() // array of children | ||
|
@@ -164,6 +162,8 @@ h( | |
) | ||
``` | ||
|
||
If there are no props then the children can usually be passed as the second argument. In cases where that would be ambiguous, `null` can be passed as the second argument to keep the children as the third argument. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wanted to make this explicit so I could use it in the subsequent examples. |
||
|
||
## Complete Example | ||
|
||
With this knowledge, we can now finish the component we started: | ||
|
@@ -240,6 +240,47 @@ render() { | |
} | ||
``` | ||
|
||
## Creating Component VNodes | ||
|
||
To create a VNode for a component, the first argument passed to `h` should be the component itself: | ||
|
||
```js | ||
render() { | ||
return Vue.h(ButtonCounter) | ||
} | ||
``` | ||
|
||
If we need to resolve a component by name then we can call `resolveComponent`: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When are some use cases for resolving a component by name? You mention later that it's often unnecessary, so as a reader, I'm curious to know a scenario where knowing this helper method would be important. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've updated the next few sentences to try to clarify. I struggled to find a wording that wasn't a clumsy mess. It still doesn't flow as cleanly as I'd like but hopefully it's clear enough. |
||
|
||
```js | ||
render() { | ||
const ButtonCounter = Vue.resolveComponent('ButtonCounter') | ||
return Vue.h(ButtonCounter) | ||
} | ||
``` | ||
|
||
`resolveComponent` is the same function that templates use internally to resolve components by name. | ||
|
||
A `render` function will normally only need to use `resolveComponent` for components that are [registered globally](/guide/component-registration.html#global-registration). [Local component registration](/guide/component-registration.html#local-registration) can usually be skipped altogether. Consider the following example: | ||
|
||
```js | ||
// We can simplify this | ||
components: { | ||
ButtonCounter | ||
}, | ||
render() { | ||
return Vue.h(Vue.resolveComponent('ButtonCounter')) | ||
} | ||
``` | ||
|
||
Rather than registering a component by name and then looking it up we can use it directly instead: | ||
|
||
```js | ||
render() { | ||
return Vue.h(ButtonCounter) | ||
} | ||
``` | ||
|
||
## Replacing Template Features with Plain JavaScript | ||
|
||
### `v-if` and `v-for` | ||
|
@@ -268,6 +309,8 @@ render() { | |
} | ||
``` | ||
|
||
In a template it can be useful to use a `<template>` tag to hold a `v-if` or `v-for` directive. When migrating to a `render` function, the `<template>` tag is no longer required and can be discarded. | ||
|
||
### `v-model` | ||
|
||
The `v-model` directive is expanded to `modelValue` and `onUpdate:modelValue` props during template compilation—we will have to provide these props ourselves: | ||
|
@@ -297,7 +340,7 @@ render() { | |
|
||
#### Event Modifiers | ||
|
||
For the `.passive`, `.capture`, and `.once` event modifiers, they can be concatenated after event name using camel case. | ||
For the `.passive`, `.capture`, and `.once` event modifiers, they can be concatenated after the event name using camel case. | ||
|
||
For example: | ||
|
||
|
@@ -306,7 +349,7 @@ render() { | |
return Vue.h('input', { | ||
onClickCapture: this.doThisInCapturingMode, | ||
onKeyupOnce: this.doThisOnce, | ||
onMouseoverOnceCapture: this.doThisOnceInCapturingMode, | ||
onMouseoverOnceCapture: this.doThisOnceInCapturingMode | ||
}) | ||
} | ||
``` | ||
|
@@ -346,34 +389,34 @@ render() { | |
|
||
### Slots | ||
|
||
You can access slot contents as Arrays of VNodes from [`this.$slots`](../api/instance-properties.html#slots): | ||
We can access slot contents as Arrays of VNodes from [`this.$slots`](../api/instance-properties.html#slots): | ||
|
||
```js | ||
render() { | ||
// `<div><slot></slot></div>` | ||
return Vue.h('div', {}, this.$slots.default()) | ||
return Vue.h('div', this.$slots.default()) | ||
} | ||
``` | ||
|
||
```js | ||
props: ['message'], | ||
render() { | ||
// `<div><slot :text="message"></slot></div>` | ||
return Vue.h('div', {}, this.$slots.default({ | ||
return Vue.h('div', this.$slots.default({ | ||
text: this.message | ||
})) | ||
} | ||
``` | ||
|
||
To pass slots to a child component using render functions: | ||
For component VNodes, we need to pass the children to `h` as an Object rather than an Array. Each property is used to populate the slot of the same name: | ||
|
||
```js | ||
render() { | ||
// `<div><child v-slot="props"><span>{{ props.text }}</span></child></div>` | ||
return Vue.h('div', [ | ||
Vue.h( | ||
Vue.resolveComponent('child'), | ||
{}, | ||
null, | ||
// pass `slots` as the children object | ||
// in the form of { name: props => VNode | Array<VNode> } | ||
{ | ||
|
@@ -384,6 +427,95 @@ render() { | |
} | ||
``` | ||
|
||
The slots are passed as functions, allowing the child component to control the creation of each slot's contents. Any reactive data should be accessed within the slot function to ensure that it's registered as a dependency of the child component and not the parent. Conversely, calls to `resolveComponent` should be made outside the slot function, otherwise they'll resolve relative to the wrong component: | ||
|
||
```js | ||
// `<MyButton><MyIcon :name="icon" />{{ text }}</MyButton>` | ||
render() { | ||
// Calls to resolveComponent should be outside the slot function | ||
const Button = Vue.resolveComponent('MyButton') | ||
const Icon = Vue.resolveComponent('MyIcon') | ||
|
||
return Vue.h( | ||
Button, | ||
null, | ||
{ | ||
// Use an arrow function to preserve the `this` value | ||
default: (props) => { | ||
// Reactive properties should be read inside the slot function | ||
// so that they become dependencies of the child's rendering | ||
return [ | ||
Vue.h(Icon, { name: this.icon }), | ||
this.text | ||
] | ||
} | ||
} | ||
) | ||
} | ||
``` | ||
|
||
If a component receives slots from its parent, they can be passed on directly to a child component: | ||
|
||
```js | ||
render() { | ||
return Vue.h(Panel, null, this.$slots) | ||
} | ||
``` | ||
|
||
They can also be passed individually or wrapped as appropriate: | ||
|
||
```js | ||
render() { | ||
return Vue.h( | ||
Panel, | ||
null, | ||
{ | ||
// If we want to pass on a slot function we can | ||
header: this.$slots.header, | ||
|
||
// If we need to manipulate the slot in some way | ||
// then we need to wrap it in a new function | ||
default: (props) => { | ||
const children = this.$slots.default ? this.$slots.default(props) : [] | ||
|
||
return children.concat(Vue.h('div', 'Extra child')) | ||
} | ||
} | ||
) | ||
} | ||
``` | ||
|
||
### `<component>` and `is` | ||
|
||
Behind the scenes, templates use `resolveDynamicComponent` to implement the `is` attribute. We can use the same function if we need all the flexibility provided by `is` in our `render` function: | ||
|
||
```js | ||
// `<component :is="name"></component>` | ||
render() { | ||
const Component = Vue.resolveDynamicComponent(this.name) | ||
return Vue.h(Component) | ||
} | ||
``` | ||
|
||
Just like `is`, `resolveDynamicComponent` supports passing a component name, an HTML element name, or a component options object. | ||
|
||
However, that level of flexibility is usually not required. It's often possible to replace `resolveDynamicComponent` with a more direct alternative. | ||
|
||
For example, if we only need to support component names then `resolveComponent` can be used instead. | ||
|
||
If the VNode is always an HTML element then we can pass its name directly to `h`: | ||
|
||
```js | ||
// `<component :is="bold ? 'strong' : 'em'"></component>` | ||
render() { | ||
return Vue.h(this.bold ? 'strong' : 'em') | ||
} | ||
``` | ||
|
||
Similarly, if the value passed to `is` is a component options object then there's no need to resolve anything, it can be passed directly as the first argument of `h`. | ||
|
||
Much like a `<template>` tag, a `<component>` tag is only required in templates as a syntactical placeholder and should be discarded when migrating to a `render` function. | ||
|
||
## JSX | ||
|
||
If we're writing a lot of `render` functions, it might feel painful to write something like this: | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the only example that was written this way, so I've changed it to match the others.