Advanced Vue: Controlling Parent Slots (Case Study)
Tags: vue, javascript, tutorial
Published: 2019-08-16 17:24:35

---
title: "Advanced Vue: Controlling Parent Slots (Case Study)"
canonical_url: https://michaelnthiessen.com/advanced-vue-controlling-parent-slots
description: "Is there a way to populate a parent's slot from a child component? It's a thorny Vue architecture problem, but also a very interesting one."
published: true
tags:
- vue
- javascript
- tutorial
---

Let me ask you about something you've _probably_ never thought about:

> Is there a way to populate a parent's slot from a child component?

Recently a coworker asked me this, and the short answer is:

Yes.

But the solution I arrived at is probably _very_ different from what you're thinking right now.

You see, my first approach turned out to be a terrible idea, and it took me a few attempts before I figured out what I think is the best approach to this problem.

It's a thorny Vue architecture problem, but also a very interesting one.

In this article we'll go through each of these solutions one by one, and see why I think they aren't that great. Ultimately we'll land on the best solution at the end.

But why did we have this problem in the first place?

## Why this obscure problem?

![](https://michaelnthiessen.com/assets/static/screenshot.2665e34.726cb87.png)

In our application we have a top bar that contains different buttons, a search bar, and some other controls.

It can be slightly different depending on which page you're on, so we need a way of configuring it on a per page basis.

![](https://michaelnthiessen.com/assets/static/screenshot2.2665e34.5c8f699.png)

To do this, we want each page to be able to configure the action bar.

Seems straightforward, but here's the catch:

This top bar (which we call an `ActionBar`) is actually part of our main layout scaffolding, which looks like this:
```html
<template>
  <div>
    <FullPageError />
    <ActionBar />
    <App />
  </div>
</template>
```

Where `App` is dynamically injected based on the page/route you're on.

There are some slots that `ActionBar` has that we can use to configure it. But how can we control those slots from the `App` component?

## Defining the Problem

First it's a good idea to be as clear as we can about what exactly we are trying to solve.

Let's take a component that has one child component and a slot:

```html
// Parent.vue
<template>
  <div>
    <Child />
    <slot />
  </div>
</template>
```

We can populate the slot of `Parent` like this:

```html
// App.vue
<template>
  <Parent>
    <p>This content goes into the slot</p>
  </Parent>
</template>
```

Nothing too fancy here...

Populating the slot of a child component is easy, that's how slots are _usually_ used.

But is there a way that we can control what goes into the `slot` of the `Parent` component from inside of our `Child` component?

Stated more generally:

> Can we get a child component to populate the slots of a parent component?

Let's take a look at the first solution I came up with.

## Props down, events up

My initial reaction to this problem was with a mantra that I keep coming back to:

> Props down, events up

The only way data flows down through your component tree is through using _props_. And the only way you communicate back up the tree is by emitting _events_.

This means that if we need to communicate from a child to a parent, we use events for that.

So we'll use events to pass content into the `ActionBar`s slots!

In each application component we'll need to do the following:
```js
import SlotContent from './SlotContent';

export default {
  name: 'Application',
  created() {
    // As soon as this component is created we'll emit our events
    this.$emit('slot-content', SlotContent);
  }
};
```

We package up whatever we want to put in the slot into a `SlotContent` component (the name is unimportant). As soon as the application component is created, we emit the `slot-content` event, passing along the component we want to use.

Our scaffold component would then look like this:
```html
<template>
  <div>
    <FullPageError />
    <ActionBar>
      <Component :is="slotContent" />
    </ActionBar>
    <App @slot-content="component => slotContent = component" />
  </div>
</template>
```

It will listen for that event, and set `slotContent` to whatever our `App` component sent us. Then, using the built-in `Component`, we can render that component dynamically.

Passing around components with events feels weird though, because it's not really something that "happens" in our app. It's just part of the way the app was designed.

Luckily there's a way we can avoid using events altogether.

## Looking for other $options

Since Vue components are just Javascript objects, we can add whatever properties we want to them.

Instead of passing the slot content using events, we can just add it as a field to our component:
```js
import SlotContent from './SlotContent';

export default {
  name: 'Application',
  slotContent: SlotContent,
  props: { /***/ },
  computed: { /***/ },
};
```

We'll have to slightly change how we access this component in our scaffolding:
```html
<template>
  <div>
    <FullPageError />
    <ActionBar>
      <Component :is="slotContent" />
    </ActionBar>
    <App />
  </div>
</template>
```
```js
import App from './App';
import FullPageError from './FullPageError';
import ActionBar from './ActionBar';

export default {
  name: 'Scaffold',
  components: {
    App,
    FullPageError,
    ActionBar,
  }
  data() {
    return {
      slotContent: App.slotContent,
    }
  },
};
```

This is more like static configuration, which is a lot nicer and cleaner 👌

But this still isn't right.

**Ideally, we wouldn't be mixing paradigms in our code, and _everything_ would be done declaratively.**

But here, instead of taking our components and composing them together, we're passing them around as Javascript objects.

It would be nice if we could just write what we wanted to appear in the slot in a normal Vue way.

## Thinking in portals

This is where portals come in.

And they work exactly like you would expect them to. You're able to teleport anything from one location to another. In our case, we're "teleporting" elements from one location in the DOM somewhere else.

We're able to control where a component is rendered in the DOM, regardless of what the component tree looks like.

For example, let's say we wanted to populate a modal. But our modal has to be rendered at the root of the page so we can have it overlay properly. First we would specify what we want in the modal:
```html
<template>
  <div>
    <!-- Other components -->
    <Portal to="modal">
      Rendered in the modal.
    </Portal>
  </div>
</template>
```

Then in our modal component we would have another portal that would render that content out:
```html
<template>
  <div>
    <h1>Modal</h1>
    <Portal from="modal" />
  </div>
</template>
```

This is certainly an improvement, because now we're actually writing HTML instead of just passing objects around. It's far more declarative and it's easier to see what's going on in the app.

Except that in some ways it _isn't_ easier to see what's going on.

Because portals are doing some magic under the hood to render elements in different places, it completely breaks the model of how DOM rendering works in Vue. It looks like you're rendering elements normally, but it's not working normally at all. This is likely to cause lots of confusion and frustration.

There's another huge issue with this, but we'll cover that later on.

At least with adding the component to the `$options` property, it's clear that you're doing something different.

I think there's a better way still.

## Lifting state

"Lifting state" is a term that's thrown around the front end development circles a bit.

All it means is that you move state from a child component to a parent, or grandparent component. You move it up the component tree.

This can have profound effects on the architecture of your application. And for our purposes, it actually opens up a completely different — and simpler — solution.

Our "state" here is the content that we are trying to pass into the slot of the `ActionBar` component.

But that state is contained within the `Page` component, and we can't really move page specific logic into the layout component. Our state has to stay within that `Page` component that we're dynamically rendering.

So we'll have to lift the whole `Page` component in order to lift the state.

Currently our `Page` component is a child of the `Layout` component:
```html
<template>
  <div>
    <FullPageError />
    <ActionBar />
    <Page />
  </div>
</template>
```

Lifting it would require us to flip that around, and make the `Layout` component a child of the `Page` component. Our `Page` component would look something like this:
```html
<template>
  <Layout>
    <!-- Page-specific content -->
  </Layout>
</template>
```

And our `Layout` component would now look something like this, where we can just use a slot to insert the page content:
```html
<template>
  <div>
    <FullPageError />
    <ActionBar />
    <slot />
  </div>
</template>
```

But this doesn't let us customize anything just yet. We'll have to add some named slots into our `Layout` component so we can pass in the content that should be placed into the `ActionBar`.

The most straightforward way to do this would be to have a slot that replaces the `ActionBar` component completely:
```html
<template>
  <div>
    <FullPageError />
    <slot name="actionbar">
      <ActionBar />
    </slot>
    <slot />
  </div>
</template>
```

This way, if you don't specify the "actionbar" slot, we get the default `ActionBar` component. But you can still override this slot with your own custom `ActionBar` configuration:
```html
<template>
  <Layout>
    <template #actionbar>
      <ActionBar>
        <!-- Custom content that goes into the action bar -->
      </ActionBar>
    </template>
    <!-- Page-specific content -->
  </Layout>
</template>
```

To me, this is the ideal way of doing things, but it does require you to refactor how you lay out your pages. That could be a huge undertaking depending on how your app is built.

If you can't do this method, my next preferred method would probably #2, using the `$options` property. It's the cleanest, and most likely to be understood by anyone reading the code.

## We can make this simpler

When we first defined the problem we stated it in it's more general form as this:

> Can we get a child component to populate the slots of a parent component?

But really, this problem has nothing to do with props specifically. More simply, it's about getting a child component to control what is rendered outside of it's own subtree.

In it's _most_ general form, we would state the problem as this:

> What is the best way for a component to control what is rendered outside of it's subtree?

Examining each of our proposed solutions through this lens gives us an interesting new perspective.

### Emitting events up to a parent

Because our component can't directly influence what happens outside of it's subtree, we instead find a component whose subtree contains the target element we are trying to control.

Then we ask it nicely to change it for us.

### Static configuration

Instead of actively asking another component to do something on our behalf, we simply make the necessary information available to other components.

### Portals

You may be noticing a pattern here among these first 3 methods.

So let me make this assertion:

**There is no way for a component to control something outside of it's subtree.**

(proving it is left as an exercise to the reader)

So each method here is a different way to get another component to do our bidding, and control the element that we are actually interested in.

The reason that portals are nicer in this regard is that they allow us to encapsulate all of this communication logic into separate components.

### Lifting State

This is where things really start to change, and why lifting state is a simpler and more powerful technique than the first 3 we looked at.

Our main limitation here is that what we want to control is outside of our subtree.

The simplest solution to that:

Move the target element into our subtree so we can control it!

Lifting state — along with the logic to manipulate that state — allows us to have a larger subtree and to have our target element contained within that subtree.

If you can do this, it's the simplest way to solve this specific problem, as well as a whole class of related problems.

Keep in mind, this doesn't necessarily mean lifting the _entire component_. You can also refactor your application to move a piece of logic into a component higher up in the tree.

## It's really just dependency injection

Some of you who are more familiar with software engineering design patterns may have noticed that what we're doing here is dependency injection — a technique we've been using for decades in software engineering.

One of it's uses is in making code that is easy to configure. In our case, we're configuring the `Layout` component differently in each `Page` that uses it.

When we flipped the `Page` and `Layout` components around, we were doing what is called an inversion of control.

In component-based frameworks the parent component controls what the child does (because it is within it's subtree), so instead of having the `Layout` component controlling the `Page`, we chose to have the `Page` control the `Layout` component.

In order to do this, we supply the `Layout` component what it needs to get the job done using slots.

As we've seen, using dependency injection has the effect of making our code a lot more modular and easier to configure.

## Conclusion

We went through 4 different ways of solving this problem, showing the pros and cons of each solution. Then we went a little further and transformed the problem into a more general one of controlling something outside of a component's subtree.

I hope that you'll see that lifting state and dependency injection are two very useful patterns to use. They are wonderful tools for you to have in your arsenal, as they can be applied to a myriad of software development problems.

But above all, I hope you take this away:

By using some common software patterns we were able to turn a problem that only had ugly solutions into a problem that had a very elegant one.

Many other problems can be attacked in this way — by taking an ugly, complicated problem and transforming it into a simpler, easier to solve problem.

If you want some more advanced content on slots, I [replicated the v-for directive](https://michaelnthiessen.com/nested-slots-in-vue), showing how to use nested slots and nested scoped slots recursively. It's one of my favourite articles, so do check it out!