Content Distribution in Vue JS

Vue implements a content distribution API inspired by the Web Components spec draft, using the <slot> element to serve as distribution outlets for content.

It promotes building composable and reusable components. Many folks have been writing about, or discussing, best practices, how to build components, how they communicate with each other, and how they should be structured in an app.

In this article, we'll dive head-first into Vue.js Slots, and Scoped Slots concepts, and go through code samples.

Slots

A Slot is a placeholder that allows content projection from a parent component to a child component.

The Vue engine, at runtime, processes the parent template, pulls the dynamic content, and injects it inside the Slot placeholder in the child component. In general, the child component hosts the <slot> element, and the parent component specifies the content to go inside it.

Figure 1 shows the interaction between a parent and child component.

| Parent and child component Figure 1: Parent and child component| | - |

Basically, Slots are used for building Layout components (also known as Master Pages), base components (Button, Hyperlink and the like) or any other type of composable components to provide flexibility for component consumers to inject dynamic content at runtime.

Let’s create a Button component with a <slot> element.

<template>
  <button>
    <slot>Click Me</slot>
  </button>
</template>

The Button component is basic and contains a single <button> element. The button wraps a <slot> component with a default fallback text. This means, if the parent component didn’t provide any content for the Slot, it will be replaced at runtime with the default fallback text Click Me.

Let's embed this Button component inside a parent component.

<template>
    <Button></Button>
</template>

The child component Button is embedded into the parent component like any other normal component in Vue.js. Notice that in the code snippet above, the parent component is not injecting any content. When you run the app, you will see an HTML <button> element with the default fallback text. Figure 2 shows the app running.

Slot with default content
Figure 2: Slot with default content

Let's inject some other text instead of the default fallback text, and run the app again. You have multiple choices when it comes to injecting content.

Without <template> element

<Button>
  Pretty Nice Button!
</Button>

With <template> element

<Button>
  <template v-slot:default>
    Pretty Nice Button!
  </template>
</Button>

Personally, I prefer the option using <template> element. The template content will be rendered in place of the <slot> element.

The <slot> element has a special attribute called name. This allows you to embed multiple Slots in your component, each designated with a different Slot name.

A <slot> element without a name attribute is considered the default Slot. To provide content for a <slot> element, you would use the v-slot directive, and then specify the name of the Slot.

In this code snippet, we could have removed the v-slot:default as this is the only <slot> element in the component. But, as a best practice, and in case you add more Slots in the future, I’d recommend you stick to this naming convention by using the v-slot:default.

You can play with this example on codesandbox.io.

Let's assume the Button component has another slot:

<template>
  <button>
    <slot>Click Me</slot>
    <br>
    <slot name="footer"/>
  </button>
</template>

Inside the parent component:

<template>
    <Button>
      <template v-slot:default>Pretty Nice Button!</template>
      <template v-slot:footer>Footer!</template>
    </Button>
</template>

There are two <template> elements, each for a specific slot.To add content to the default <slot> element, you add the v-slot:default directive by specifying the name of the slot. For the footer Slot, you provide the v-slot:footer directive to target the footer <slot> element.

Now, it makes more sense to use the v-slot directive on both Slots to differentiate them, and make your code readable.

You can play with this example on codesandbox.io.

Slots and Props

Thus far, we have seen how to use <slot> elements, and inject content to them. In some other cases, you might need to pass data to Slots.

Slots are generally compiled under the parent’s component context. This means whatever content you project inside a <slot> element is aware of the parent’s context only. For instance, if you have an interpolation or binding projected inside the <slot> element, these will be compiled with respect to the parent component, and not the child component. A code snippet is worth a thousand words.

<template>
    <Button>
      <template v-slot:default>
        {{ text }}
      </template>
    </Button>
</template>

<script>
import Button from "./components/Button";

export default {
  components: {
    Button
  },
  data() {
    return {
      text: 'Pretty Nice Button!',
    };
  },
};
</script>

The {{ text }} interpolation is referencing a local data variable defined on the component named text. The Vue.js engine compiles the projected content inside the parent component scope, and embeds the results into the child component.

You can play with this example on codesandbox.io.

A <slot> element can also render default fallback text passed from the parent component to the child component. How? Nothing special really. It's similar to how you pass data to Vue components via props.

Let's introduce a new text property on the Button component:

<template>
  <button>
    <slot>{{ text }}</slot>
  </button>
</template>

<script>
export default {
  props: {
    text: {
      type: String,
      default: "Click Me"
    }
  }
};
</script>

In the parent component:

<template>
  <div id="app">
    <h2>Vue Slots Demo</h2>
    <Button v-bind:text="text"></Button>
  </div>
</template>

<script>
import Button from "./components/Button";

export default {
  components: {
    Button
  },
  data() {
    return {
      text: "Pretty Nice Button!!!!"
    };
  }
};
</script>

You make use of Vue.js binding convention to bind the text prop on Button component to a local variable also named text. That’s all!

You can play with this example on codesandbox.io.

You can even go wild and accept a prop of type Function on the child component! The child component can share its context with the parent component. The parent component can then manipulate the child component context and return whatever content back to the child component.

The child component accepts a prop of type Function:

<template>
  <button>
    <slot>{{ textFnc('Hi from child!') }}</slot>
  </button>
</template>

<script>
export default {
  props: {
    textFnc: {
      type: Function
    }
  }
};
</script>

The default fallback content inside the child component calls the prop Function passing some random text.

The parent component defines the prop Function as follows:

<template>
  <div id="app">
    <h2>Vue Slots Demo</h2>
    <Button v-bind:textFnc="(prefix) => prefix + ' - ' + message"></Button>
  </div>
</template>

<script>
import Button from "./components/Button";

export default {
  components: {
    Button
  },
  data() {
    return {
      message: "Pretty Nice Button!!!!"
    };
  }
};
</script>

The function textFunc() executes under the parent component context. In this case, the function returns the text received from the child component, and appends it to a message that’s local to the parent component. The end result is shown in Figure 3.

Function Prop
Figure 3: Function prop

You can play with this example on codesandbox.io.

The takeaway, in using functions as props, is the ability to share the child component’s context with the parent component.

Scoped Slots

Passing functions as props to a child component is one way of sharing context between child and parent components. However, this method is limited to text only. The function defined on the parent component can only return text.

What if you want the parent component to pass HTML content to the child component’s slot(s) and also make use of the child component context? This is where Scoped Slots play their part.

Pretty complicated right? Well, not as complicated as you might think. Let’s check it out together!

Let's build a List component that owns an array of objects with a title and description properties.

<template>
  <ul>
    <li v-for="item in items" :key="item.id">
      <slot :item="item">
        <p>
          {{ item['title'] }}
          <br>
          {{ item['description'] }}
        </p>
      </slot>
    </li>
  </ul>
</template>

<script>
export default {
  props: {
    items: {
      type: Array,
      default: () => []
    }
  }
};
</script>

The component uses a <ul> element to loop over the items in the array. An <li> element is used to represent a single item. The code places a <slot> element inside each and every <li> element. The slot is bound to the single item object. In addition, the <slot> element defines a default fallback content.

Let’s switch to the parent component:

<template>
  <div id="app">
    <h2>Vue Slots Demo</h2>
    <List :items="items" />
  </div>
</template>

<script>
import List from "./components/List";

export default {
  components: {
    List
  },
  data() {
    return {
      items: [
        {
          id: 1,
          title: "Item #1",
          description: "Item #1 description"
        },
        {
          id: 2,
          title: "Item #2",
          description: "Item #2 description"
        },
        {
          id: 3,
          title: "Item #3",
          description: "Item #3 description"
        },
        {
          id: 4,
          title: "Item #4",
          description: "Item #4 description"
        },
        {
          id: 5,
          title: "Item #5",
          description: "Item #5 description"
        }
      ]
    };
  }
};
</script>

The component binds the items array property to the List component property items. It uses the default fallback content coming from the child component.

Figure 4 shows the app running:

List component with default fallback content
Figure 4: List component with default fallback content

Let’s see how we can make use of the Slot binding.

As a refresher, the child component defines the following:

<slot :item="item">
  <p>
    {{ item['title'] }}
    <br>
    {{ item['description'] }}
  </p>
</slot>

The <slot> element binds the item object on the Slot. It also provides a default fallback content. What if the parent component wants to replace this default content with a new content? That's easy!

Let's change the parent component as follows:

<template>
  <List :items="items">
    <template v-slot:default="prop">
      <strong>{{ prop.item.title }}</strong>
    </template>
  </List>
</template>

The code snippet provides a <template> with a v-slot:default directive. The value given to the v-slot:default directive is actually the directive value representing the object passed in by the <slot> element defined inside the child component.

As a side note, I am writing another article, to be published very soon, exploring custom directives in Vue.js. Stay tuned!

Going back to the code snippet, the parent component now has access to the child component represented by the v-slot:default directive value.

The <template> element replaces the default fallback content by rendering just the title field and making it bold.

Figure 5 shows the app running:

List component with custom content
Figure 5: List component with custom content

Scoped Slots is a powerful tool in Vue.js to allow you to build composable and reusable components.

Slots in Vue 3

The content on Slots and Scoped Slots in this article applies to both Vue 2 and Vue 3. I am not aware of any change up until this moment. Things might change with the final release of Vue 3, but we’ll have to wait and see together!

Conclusion

Vue Slots feature gives developers the flexibility needed to build reusable and composable components in their applications.

You might also like