I got myself inspired by Max Stoiber in his A deep dive into children in React to create this post about Vue children. So, credits on the title to him. If you're into React, check out his post, it has lots of nice information on React children.
I have started working with Vue a few months ago and it has been really nice. After working with React for some years, it's nice to experience something new.
I'm writing this post because there are some patterns that I used in React and I miss in Vue. In this case, this post will cover manipulating children elements in Vue components.
This is really straightforward to do with React. So, I'll try to cover some parts of Vue that enables us to do something similar as done in React.
The fantastic part is that Vue is really well thought (congratulations to all maintainers for that) and really simple, so, we can manipulate Vue objects in an easy way to achieve what we need.
If you come from a non React background and want to know more about its children concept, please refer to their documentation on that:
First of all, to be on the same page, we'll call children
elements all
elements rendered inside any given Vue component. Here are some examples:
<template>
<Parent>
<div>This is a child</div>
<Child /> <!-- This is also a child -->
<span>Another child</div>
</Parent>
</template>
Every single element that is put inside another one can be considered a child element. It doesn't matter if it is a custom component, a tag or some text.
There's only one detail to the statement above:
Vue Slots
work in a
different manner based on whether a component is
functional
or not (we'll get to that later).
To create elements that have children support we can simply use the <slot />
tag in our components.
For example, imagine we're creating a dynamic <Button>
component that receives
its children. We can define the component template like this:
<template>
<button>
<slot />
</button>
</template>
And, use it like this:
<template>
<Button>
Content goes here
</Button>
</template>
This will render a button with Content goes here
content.
Please note that you can add anything "renderable" inside <slot>
. In the
example above we rendered a simple text, but you can render a <div>
, a
<CustomComponent>
and so on.
Before we continue, it's interesting to know a little bit how Vue render process works. Basically, whenever you write a template, Vue compiles it to a render function. As said in template compilation part:
You may be interested to know that Vue’s templates actually compile to render functions. This is an implementation detail you usually don’t need to know about, but if you’d like to see how specific template features are compiled, you may find it interesting.
This knowledge will be useful from now on, as we can use this to manipulate our children elements.
Sadly Vue doesn't provide a way to manipulate children using the <template>
tag. For example, using React one would do:
const Component = ({ children }) => (
<div>
{React.Children.map(children, (child, i) => {
// do whatever I want with `child`
return child
})}
</div>
)
In Vue, we can leverage the use of
createElement
and render functions. So, we will not write a <template>
for our
components, as now we'll render the template manually.
As a minimal boilerplate, if you want to manipulate children with Vue, you should do:
<script>
export default {
functional: true,
render(createElement, context) {
const $children = context.children
const children = $children.map((child, i) => {
// do whatever I want with `child`
return child
})
return createElement('div', children)
}
}
</script>
Please note that, as we're using functional
components,
in this case context.children
contains all given children, no matter if user
specified slots
. For example, imagine that we're rendering the following
children inside our component:
<template>
<Component>
<div>Child 1</div>
<div slot="slot1">Child 2</div>
</Component>
</template>
In this case, context.children
contains both Child 1
and Child 2
. If you
want to have all slots separated, you can use context.slots()
function. This
will return an object like:
{
default: Array[VNode],
slot1: Array[VNode]
}
I don't see why not use functional components in those cases, however, if you really need to, it becomes a little bit harder and unstable to access children components.
First of all, you'll rely on this
to access data. Second, you'll need to
access some private properties of the Vue instance, which is not really good
(they're not documented in the "public api" -
https://vuejs.org/v2/api/#Instance-Properties), as if Vue core team decides to
change them, your code will break. Here's an example:
export default {
render(createElement) {
const children = this.$options._renderChildren // a "private" property
return createElement('div', children)
}
}
In the same way, you can also access this.$slots
to get all slots.
I created a simple example to illustrate those situations. Please go to
https://codesandbox.io/s/vue-template-ny5hg?fontsize=14 and check it out. There,
I'll have two components: one using functional
components and other using
regular components.
Well, mostly of those manipulations will be done in a higher level abstraction.
For example, imagine you need to filter out all <Other>
component instances,
you can do something like that:
<script>
import Other from './Other.vue'
export default {
functional: true,
render(createElement, { children }) {
return createElement("div", children.filter(child => {
const { componentOptions = {} } = child
console.log(child, componentOptions.Ctor, Other._Ctor[0])
return componentOptions.Ctor !== Other._Ctor[0]
}));
}
};
</script>
(It's a little bit ugly, I know)
Another example is when you need to inject some properties. For example, take
the great Reach's Tabs component: https://ui.reach.tech/tabs/. It's a generic
Tab component that enables the user to place <TabList>
(the tab navigation)
and <TabPanels>
wherever user wants. But, to do that, we need to have a smart
wrapper component to do this for us.
For example, take this template:
<template>
<div>
<Tabs>
<TabList>
<Tab>Tab 1</Tab>
<Tab>Tab 2</Tab>
</TabList>
<TabPanels>
<TabPanel>Tab panel 1</TabPanel>
<TabPanel>Tab panel 2</TabPanel>
</TabPanels>
</Tabs>
</div>
</template>
In this case, <Tabs>
can be "smart" and inject properties/callbacks to
children components, allowing <TabList>
and <TabPanels>
to know which tab is
currently active, handle tab change and so on.
While browsing for help on that subject, I've found some relevant discussions that you may find interesting on further readings:
There's also an abstraction that allows you to move this logic to your
<template>
, however, I don't really know if I like that, but it might help
you: