DEV Community

Frederik Dietz
Frederik Dietz

Posted on

Vue.js Component Composition with Scoped Slots

In the previous post we looked into slots and named slots to compose our components and content in a very flexible way.
There's one catch though we have not discussed. The content we pass to our slot is in the context of the parent component and not the child component. This sounds quite abstract, let's build an example component and investigate the problem further!

List of Items Example

Probably the most canonical example for this kind of scenario is a todo list which renders for each todo a checkbox with name.

Example 1
Example 1
<div id="demo">
  <div class="list">
    <div v-for="item in listItems" key="item.id" class="list-item">
      <input type="checkbox" v-model="item.completed" class="list-item__checkbox" />
      {{item.name}}
    </div>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode
new Vue({ 
  el: '#demo',
  data: {
    listItems: [
      {id: 1, name: "item 1", completed: false},
      {id: 2, name: "item 2", completed: false},
      {id: 3, name: "item 3", completed: false}
    ]
  }
});
Enter fullscreen mode Exit fullscreen mode

In the next step we refactor this code into a reusable list component and our goal is to leave it up to the client of the component to decide what and how to render the list item.

Refactor to Reusable List component

Let's start with the implementation of the List component:

Vue.component("List", {
  template: "#template-list",
  props: {
    items: {
      type: Array, 
      default: []
    }
  }
});
Enter fullscreen mode Exit fullscreen mode
<template id="template-list">  
  <div class="list">
    <div v-for="item in items" class="list-item">
      <slot></slot>
    </div>
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

Following our previous examples we use the default slot to render a list item.

And now make use of our new component:

<div id="demo">
  <List :items="listItems">
    <div class="list-item">
      <input type="checkbox" v-model="item.completed" class="list-item__checkbox" />
      <div class="list-item__title">{{item.name}}</div>
    </div>
  </List>
</div>
Enter fullscreen mode Exit fullscreen mode

But, when trying this example we run into a Javascript error message:

ReferenceError: item is not defined
Enter fullscreen mode Exit fullscreen mode

It seems we cannot access item from our slot content. In fact the content we passed runs in the context of the parent and not the child component List.

Let's verify this by printing the total number of items in our List component using the listItems data defined in our Vue instance.

<div id="demo">
  <List :items="listItems">
    <div class="list-item">
      {{listItems}}
    </div>
  </List>
</div>
Enter fullscreen mode Exit fullscreen mode

That works because we run in the context of the parent component which is in this example the Vue instance. But, how can we pass the item data from our child <List> to our slot? This is where "scoped slots" come to the rescue!

Our component must pass along item as a prop to the slot itself:

<template id="template-list">  
  <div class="list">
    <div v-for="item in items" class="list-item">
      <slot :item="item"></slot>
    </div>
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

Note, that it is important to pass this with a binding :item instead of only item!

Okay let's try this again:

<div id="demo">
  <List :items="listItems">
    <div slot-scope="slotProps" class="list-item">
      <input type="checkbox" v-model="slotProps.item.completed" class="list-item__checkbox" />
      <div class="list-item__title">{{slotProps.item.name}}</div>
    </div>
  </List>
</div>
Enter fullscreen mode Exit fullscreen mode

This time we use the slot-scope attribute and assign the name slotProps to it. Inside this scoped slot we can access all props passed along via this slotProps variable.

In Vue.js 2.5.0+, scope is no longer limited to the <template> element, but can instead be used on any element or component in the slot.

Extending the rendering of the list item

Now that we know how to pass data along we are free to extend the list item with some new functionality without changing the List component. It would be awesome if we could remove a todo item!

First of all we define the Vue app with a method to remove a todo item:

new Vue({ 
  el: '#demo',
  data: {
    listItems: [
      {id: 1, name: "item 1", completed: false},
      {id: 2, name: "item 2", completed: false},
      {id: 3, name: "item 3", completed: false}
    ]
  },
  methods: {
    remove(item) {
      this.listItems.splice(this.listItems.indexOf(item), 1);
    }
  }
});
Enter fullscreen mode Exit fullscreen mode

We use the Javascript splice function to remove the item using it's index from listItems.

Next, we use this method when rendering the list item:

<template slot-scope="slotProps" class="list-item">
  <input type="checkbox" v-model="slotProps.item.completed" class="list-item__checkbox" />
  <div class="list-item__title">{{slotProps.item.name}}</div>
  <button @click="remove(slotProps.item)" class="list-item__remove">×</button>
</template>
Enter fullscreen mode Exit fullscreen mode

We add a button with a click event which calls our previously defined remove function. That's it!

Using Destructuring for the slot-scope

We can further simplify this template by using a modern Javascript trick on the slot-scope attribute.

Here's an example of using Javascript "destructuring" to access an attribute of an object:

const item = slotProps.item;
// same as 
const { item } = slotProps;
Enter fullscreen mode Exit fullscreen mode

Instead of using the value slotProps we can now access the item directly.

Let's use this in our template:

<template slot-scope="{item}" class="list-item">
  <input type="checkbox" v-model="item.completed" class="list-item__checkbox" />
  <div class="list-item__title">{{item.name}}</div>
  <button @click="remove(item)" class="list-item__remove">×</button>
</template>
Enter fullscreen mode Exit fullscreen mode

This is easier to read because we can directly use the item variable instead of always going via slotProps.item.

Summary

In this chapter we used scoped slots to allow the parent to access data from the child. This gives us lots of new possibilities which weren't possible before. This feature is especially useful in scenarios where you want to leave the rendering of the slot content to the user of the component. In our case the list component is very reusable by decoupling the rendering of the list items.

You can find the complete examples on Github.

If you like this post also check out my new course Vue.js Component Patterns Course.

Stay tuned for my upcoming post about headless components!

Top comments (0)