Slots Vue 3

Posted By admin On 12/04/22

# Slots Unificationbreaking # Overview. This change unifies normal and scoped slots in 3.x. For more information, read on! When using the render function, i.e., h, 2.x used to define the slot data property on the content nodes. And when you need to reference scoped slots. We can populate the slot of Parent like this: // App.vue Parent This content goes into the slot Parent Nothing too fancy here. Populating the slot of a child component is easy, that's how slots are usually used. Migrating slots from Vue 2 to Vue 3. Ask Question Asked 4 months ago. Active 4 months ago. Viewed 684 times 1.

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?

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.

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

Slots

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:

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:

We can populate the slot of Parent like this:

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 ActionBars slots!

In each application component we'll need to do the following:

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:

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:

We'll have to slightly change how we access this component in our scaffolding:

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:

Then in our modal component we would have another portal that would render that content out:

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:

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:

And our Layout component would now look something like this, where we can just use a slot to insert the page content:

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:

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:

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.

Slots

If you want some more advanced content on slots, I replicated the v-for directive, showing how to use nested slots and nested scoped slots recursively. It's one of my favourite articles, so do check it out!

Slots Vue 3d

Slots are a powerful tool for creating reusable components in Vue.js, though they aren’t the simplest feature to understand. Let’s take a look at how to use slots and some examples of how they can be used in your Vue applications.

With the recent release of Vue 2.6, the syntax for using slots has been made more succinct. This change to slots has gotten me re-interested in discovering the potential power of slots to provide reusability, new features, and clearer readability to our Vue-based projects. What are slots truly capable of?

If you’re new to Vue or haven’t seen the changes from version 2.6, read on. Probably the best resource for learning about slots is Vue’s own documentation, but I’ll try to give a rundown here.

What Are Slots?

Slots are a mechanism for Vue components that allows you to compose your components in a way other than the strict parent-child relationship. Slots give you an outlet to place content in new places or make components more generic. The best way to understand them is to see them in action. Let’s start with a simple example:

This component has a wrapper div. Let’s pretend that div is there to create a stylistic frame around its content. This component is able to be used generically to wrap a frame around any content you want. Let’s see what it looks like to use it. The frame component here refers to the component we just made above.

The content that is between the opening and closing frame tags will get inserted into the frame component where the slot is, replacing the slot tags. This is the most basic way of doing it. You can also specify default content to go into a slot simply by filling it in:

So now if we use it like this instead:

The default text of “This is the default content if nothing gets specified to go here” will show up, but if we use it as we did before, the default text will be overridden by the img tag.

Multiple/Named Slots

You can add multiple slots to a component, but if you do, all but one of them is required to have a name. If there is one without a name, it is the default slot. Here’s how you create multiple slots:

We kept the same default slot, but this time we added a slot named header where you can enter a title. You use it like this:

Just like before, if we want to add content to the default slot, just put it directly inside the titled-frame component. To add content to a named slot, though, we needed to wrap the code in a template tag with a v-slot directive. You add a colon (:) after v-slot and then write the name of the slot you want the content to be passed to. Note that v-slot is new to Vue 2.6, so if you’re using an older version, you’ll need to read the docs about the deprecated slot syntax.

Scoped Slots

One more thing you’ll need to know is that slots can pass data/functions down to their children. To demonstrate this, we’ll need a completely different example component with slots, one that’s even more contrived than the previous one: let’s sorta copy the example from the docs by creating a component that supplies the data about the current user to its slots:

This component has a property called user with details about the user. By default, the component shows the user’s last name, but note that it is using v-bind to bind the user data to the slot. With that, we can use this component to provide the user data to its descendant:

Jsx

To get access to the data passed to the slot, we specify the name of the scope variable with the value of the v-slot directive.

There are a few notes to take here:

  • We specified the name of default, though we don’t need to for the default slot. Instead we could just use v-slot='slotProps'.
  • You don’t need to use slotProps as the name. You can call it whatever you want.
  • If you’re only using a default slot, you can skip that inner template tag and put the v-slot directive directly onto the current-user tag.
  • You can use object destructuring to create direct references to the scoped slot data rather than using a single variable name. In other words, you can use v-slot='{user}' instead of v-slot='slotProps' and then you can use user directly instead of slotProps.user.

Taking those notes into account, the above example can be rewritten like this:

A couple more things to keep in mind:

  • You can bind more than one value with v-bind directives. So in the example, I could have done more than just user.
  • You can pass functions to scoped slots too. Many libraries use this to provide reusable functional components as you’ll see later.
  • v-slot has an alias of #. So instead of writing v-slot:header='data', you can write #header='data'. You can also just specify #header instead of v-slot:header when you’re not using scoped slots. As for default slots, you’ll need to specify the name of default when you use the alias. In other words, you’ll need to write #default='data' instead of #='data'.

There are a few more minor points you can learn about from the docs, but that should be enough to help you understand what we’re talking about in the rest of this article.

What Can You Do With Slots?

Slots weren’t built for a single purpose, or at least if they were, they’ve evolved way beyond that original intention to be a powerhouse tool for doing many different things.

Reusable Patterns

Components were always designed to be able to be reused, but some patterns aren’t practical to enforce with a single “normal” component because the number of props you’ll need in order to customize it can be excessive or you’d need to pass large sections of content and potentially other components through the props. Slots can be used to encompass the “outside” part of the pattern and allow other HTML and/or components to placed inside of them to customize the “inside” part, allowing the component with slots to define the pattern and the components injected into the slots to be unique.

For our first example, let’s start with something simple: a button. Imagine you and your team are using Bootstrap*. With Bootstrap, your buttons are often strapped with the base `btn` class and a class specifying the color, such as `btn-primary`. You can also add a size class, such as `btn-lg`.

* I neither encourage nor discourage you from doing this, I just needed something for my example and it’s pretty well known.

Let’s now assume, for simplicity’s sake that your app/site always uses btn-primary and btn-lg. You don’t want to always have to write all three classes on your buttons, or maybe you don’t trust a rookie to remember to do all three. In that case, you can create a component that automatically has all three of those classes, but how do you allow customization of the content? A prop isn’t practical because a button tag is allowed to have all kinds of HTML in it, so we should use a slot.

Now we can use it everywhere with whatever content you want:

Of course, you can go with something much bigger than a button. Sticking with Bootstrap, let’s look at a modal, or least the HTML part; I won’t be going into functionality… yet.

Now, let’s use this:

The above type of use case for slots is obviously very useful, but it can do even more.

Reusing Functionality

Vue components aren’t all about the HTML and CSS. They’re built with JavaScript, so they’re also about functionality. Slots can be useful for creating functionality once and using it in multiple places. Let’s go back to our modal example and add a function that closes the modal:

Now when you use this component, you can add a button to the footer that can close the modal. Normally, in the case of a Bootstrap modal, you could just add data-dismiss='modal' to a button, but we want to hide Bootstrap specific things away from the components that will be slotting into this modal component. So we pass them a function they can call and they are none the wiser about Bootstrap’s involvement:

Renderless Components

And finally, you can take what you know about using slots to pass around reusable functionality and strip practically all of the HTML and just use the slots. That’s essentially what a renderless component is: a component that provides only functionality without any HTML.

Making components truly renderless can be a little tricky because you’ll need to write render functions rather than using a template in order to remove the need for a root element, but it may not always be necessary. Let’s take a look at a simple example that does let us use a template first, though:

This is an odd example of a renderless component because it doesn’t even have any JavaScript in it. That’s mostly because we’re just creating a pre-configured reusable version of a built-in renderless function: transition.

Yup, Vue has built-in renderless components. This particular example is taken from an article on reusable transitions by Cristi Jora and shows a simple way to create a renderless component that can standardize the transitions used throughout your application. Cristi’s article goes into a lot more depth and shows some more advanced variations of reusable transitions, so I recommend checking it out.

For our other example, we’ll create a component that handles switching what is shown during the different states of a Promise: pending, successfully resolved, and failed. It’s a common pattern and while it doesn’t require a lot of code, it can muddy up a lot of your components if the logic isn’t pulled out for reusability.

So what is going on here? First, note that we are receiving a prop called promise that is a Promise. In the watch section we watch for changes to the promise and when it changes (or immediately on component creation thanks to the immediate property) we clear the state, and call then and catch on the promise, updating the state when it either finishes successfully or fails.

Then, in the template, we show a different slot based on the state. Note that we failed to keep it truly renderless because we needed a root element in order to use a template. We’re passing data and error to the relevant slot scopes as well.

And here’s an example of it being used:

We pass in somePromise to the renderless component. While we’re waiting for it to finish, we’re displaying “Working on it…” thanks to the pending slot. If it succeeds we display “Resolved:” and the resolution value. If it fails we display “Rejected:” and the error that caused the rejection. Now we no longer need to track the state of the promise within this component because that part is pulled out into its own reusable component.

So, what can we do about that span wrapping around the slots in promised.vue? To remove it, we’ll need to remove the template portion and add a render function to our component:

Vue slot example

There isn’t anything too tricky going on here. We’re just using some if blocks to find the state and then returning the correct scoped slot (via this.$scopedSlots['SLOTNAME'](...)) and passing the relevant data to the slot scope. When you’re not using a template, you can skip using the .vue file extension by pulling the JavaScript out of the script tag and just plunking it into a .js file. This should give you a very slight performance bump when compiling those Vue files.

This example is a stripped-down and slightly tweaked version of vue-promised, which I would recommend over using the above example because they cover over some potential pitfalls. There are plenty of other great examples of renderless components out there too. Baleada is an entire library full of renderless components that provide useful functionality like this. There’s also vue-virtual-scroller for controlling the rendering of list item based on what is visible on the screen or PortalVue for “teleporting” content to completely different parts of the DOM.

Slots Vue 360

I’m Out

Vue’s slots take component-based development to a whole new level, and while I’ve demonstrated a lot of great ways slots can be used, there are countless more out there. What great idea can you think of? What ways do you think slots could get an upgrade? If you have any, make sure to bring your ideas to the Vue team. God bless and happy coding.

(dm, il)