Slots, Slots, Slots, Everybody!

Abbey Perini - Feb 23 - - Dev Community

Slots turn components into wrappers that enhance your dynamic content.

  1. What Are Slots?
  2. Slots in Web Components
  3. Slots in Vue
  4. Slots in Angular

What Are Slots?

In React, you can pass JSX to your component using the children prop. Manipulating, transforming, or getting information from the children prop in your component is not recommended.



// parent component
<blog-list>
  <p>My text here</p>
</blog-list>


Enter fullscreen mode Exit fullscreen mode


function BlogList({children}) {
  return(
    <div>
      {children}
    </div>
  )
}


Enter fullscreen mode Exit fullscreen mode

In HTML and other JavaScript frameworks, a <slot> element creates a placeholder that allows you to pass content to a component without using props at all.

You can easily make default content without any extra lines of code.



// FancyCard component
<div class=“fancy” />
  <slot name="text"><p>Default text</p></slot>
</div>

// parent component
<FancyCard />

// default result
<div class=“fancy” />
  <p>Default text</p>
</div>


Enter fullscreen mode Exit fullscreen mode

You can also pass entire elements.



// FancyCard component
<div class=“fancy” />
  <slot name="text"><p>Default text</p></slot>
</div>

// parent component
<FancyCard>
  <ul slot="text">
    <li><p>Here’s my text</p></li>
    <li><p>Here’s my text</p></li>
  </ul>
</FancyCard>

// result
<div class=“fancy” />
  <ul>
    <li><p>Here’s my text</p></li>
    <li><p>Here’s my text</p></li>
  </ul>
</div>


Enter fullscreen mode Exit fullscreen mode

Slots without a name are called default slots. If you don't add a slot attribute to the content you pass to your component, it will go in the default slot. Using both default and named slots, you can put some content in a specific place and dump the rest of it in another place.



// FancyCard component
<div class=“fancy” />
  <slot name="text"><p>Default text</p></slot>
  <slot></slot>
</div>

// parent component
<FancyCard>
  <p slot="text">This is other text</p>
  <p>This is more text</p>
  <button>Click me!</button>
</FancyCard>

// result
<div class=“fancy” />
  <p>This is other text</p>
  <p>This is more text</p>
  <button>Click me!</button>
</div>


Enter fullscreen mode Exit fullscreen mode

If the <slot> doesn't have any default content and you don't pass anything to it, nothing will mount.



// FancyCard component
<div class=“fancy” />
  <slot name="text"><p>Default text</p></slot>
  <slot></slot>
</div>

// parent component A
<FancyCard />

// result A
<div class=“fancy” />
  <p>Default text</p>
</div>


Enter fullscreen mode Exit fullscreen mode

Slots in Web Components

HTML has the Web Components spec that essentially allows users to create components with HTML.

Creating a custom element that uses <slots> requires three parts. The first is an HTML template.



<template id="preview">
  <style>
    @import "../styles.css";
  </style>
  <div class="blog-list-container">
    <h2><slot name="title">default title</slot></h2>
    <slot></slot>
  </div>
</template>


Enter fullscreen mode Exit fullscreen mode

The second part is a JavaScript class creating a custom element. Within the constructor of our custom element, we need to create a shadow DOM to hold our <slot>s. The shadow Dom API creates a scoped, encapsulated, separate DOM inside the DOM created by the rest of your HTML page. To create a shadow DOM and put our <slot>s in it, the constructor does three things:

  • Access the content of the HTML template from part one with getElementById and the .content property and assign it to a variable.
  • Instantiate a shadow root in open mode, so that the browser and rest of the page can access the content within.
  • Clone the content of the HTML template and attach it to the shadow DOM as a child.


class BlogPreview extends HTMLElement {
  constructor() {
    super();
    const templateContent = document.getElementById("preview").content;
    this.attachShadow({mode: "open"});
    this.shadowRoot.appendChild(templateContent.cloneNode(true));
  }
}

customElements.define('preview-component', Preview);


Enter fullscreen mode Exit fullscreen mode

The third part of using <slot>s is calling the custom element in an HTML file.



<preview-component><span slot="title">My Title</span><p>My Description</p></preview-component>


Enter fullscreen mode Exit fullscreen mode

The result displays my <span> with my title text in an <h2>.



<div class="blog-list-container">
  <h2><span>My Title</span></h2>
  <p>My Description</p>
</div>


Enter fullscreen mode Exit fullscreen mode

When this custom element is inspected, DevTools will show the shadow root with <slot>s and the content you passed to the component..



<preview-component>
  #shadow-root (open)
    <div class="blog-list-container">
      <h2><slot name="title">default</slot></h2>
      <slot></slot>
    </div>
  <span slot="title">My Title</span>
  <p>My Description</p>
</preview-component>


Enter fullscreen mode Exit fullscreen mode

Both Chrome DevTools and FireFox DevTools have built in tools that will show you where <slot> content is coming from with a click.

If you want to learn more about how slots came to be, check out Jan Miksovsky's A History of the Slot Element.

Slots in Vue

Vue slots have a lot more features, but you can see how they're inspired by the Web Components spec.

Default Slots

If you're putting content in an default <slot>, you can just wrap it in your component.



// blog-list component
<template>
  <div class="blog-list-container">
    <h2><slot>default</slot></h2>
  </div>
</template>

//parent component
<blog-list>
  <span>My Title</span>
</blog-list>

// result
<div class="blog-list-container">
  <h2><span>My Title</span></h2>
</div>


Enter fullscreen mode Exit fullscreen mode

Named Slots

If you're using a named slot, you'll need a <template> with the slot name as an attribute, starting with #.



// blog-list component
<template>
  <div class="blog-list-container">
    <h2><slot name="title">default</slot></h2>
  </div>
</template>

// parent component
<blog-list>
  <template #title>
    <span>My Title</span>
  </template>
</blog-list>

// result
<div class="blog-list-container">
  <h2><span>My Title</span></h2>
</div>


Enter fullscreen mode Exit fullscreen mode

You can also use this syntax with default slots.



// blog-list component
<template>
  <div class="blog-list-container">
    <h2><slot name="title">default</slot></h2>
    <slot></slot>
  </div>
</template>

// parent component
<blog-list>
  <template #title>
    <span>My Title</span>
  </template>
  <template #default>
    <p>Put me in, coach!</p>
  </template>
</blog-list>

// result
<div class="blog-list-container">
  <h2><span>My Title</span></h2>
  <p>Put me in, coach!</p>
</div>


Enter fullscreen mode Exit fullscreen mode

Scoped Slots

Here's where it gets fun. You can pass data from the child component to the slot. You'll be able to access the data when you use the child component in the parent component.



// counter component
<script setup>
  const count = ref(0);
  const counterMessage = count < 10 ? "Keep counting!" : "Great job!"
</script>
<template>
  <div class="counter-container">
    <slot :number="count" :message="counterMessage"></slot>
  </div>
</template>

// parent component
<counter v-slot="slotProps">
  <h2>My Counter</h2>
  <p>{ slotProps.number }</p>
  <p>{ slotProps.message }</p>
</counter>

// result
<div class="counter-container">
  <h2>My Counter</h2>
  <p>0</p>
  <p>Keep counting!</p>
</div>


Enter fullscreen mode Exit fullscreen mode

You can use destructuring, so you don't have to use slotProps.property, just property.



// parent component
<counter v-slot="{number, message}">
  <span>My Counter</span>
  <p>{ number }</p>
  <p>{ message }</p>
</counter>


Enter fullscreen mode Exit fullscreen mode

This feature uses a slightly different syntax with named slots. Also, if you're mixing default and named slots while passing slot props, you will have to use <template #default>.



// counter component
<template>
  <div class="counter-container">
    <h2><slot>default</slot></h2>
    <slot name="display" :number="count" :message="counterMessage"></slot>
  </div>
</template>

// parent component
<counter>
  <template #default>
    <span>My Counter</span>
  </template>
  <template #display="{number, message}">
    <p>{ number }</p>
    <p>{ message }</p>
  </template>
</counter>

// result
<div class="counter-container">
  <h2><span>My Counter</span></h2>
  <p>0</p>
  <p>Keep counting!</p>
</div>


Enter fullscreen mode Exit fullscreen mode

If you've ever used Array.prototype.map() to make a JSX list, you can see where Vue is going with this. You can create an extensible list component, and display it however you want.



// list component
<script setup>
  defineProps({
    listItems: Array as PropType<String[]>
  })
</script>

<template>
  <ul>
    <li v-for="listItem in listItems" :key="listItem.id">
      <slot name="item" :item="listItem"></slot>
    </li>
  </ul>
</template>

// parent component
<script setup>
  const products = [
    {
      name: "Socks",
      description: "Luxury for your feet",
      count: 12,
    },
    {
      name: "Hat",
      description: "Wear it on your head",
      count: 8
    },
];
</script>
<template>
  <list-component :listItems="products">
    <template #item="{ item }">
      <div class="product">
        <h3>{ item.name }</h3>
        <p>{ item.description }</p>
        <span class="count">{ item.count }</span>
      </div>
    </template>
  </list-component>
</template>

// result
<ul>
  <li>
    <div class="product">
      <h3>Socks</h3>
      <p>Luxury for your feet</p>
      <span class="count">12</span>
    </div>
  </li>
  <li>
    <div class="product">
      <h3>Hat</h3>
      <p>Wear it on your head</p>
      <span class="count">8</span>
    </div>
  </li>
</ul>


Enter fullscreen mode Exit fullscreen mode

It's possible to take this concept even further and create Higher-Order Components or smart components that don't render elements and only contain logic. Vue calls them Renderless Components.

Slots in Angular

Angular saw the Web Components spec and ran with it. You can play with the shadow DOM API within Angular as a View Encapsulation Mode.

There are also slots, but Angular calls the functionality content projection. Instead of the <slot> element, you use the <ng-content> element.

When creating your component, you can create your template one of two ways.

In an HTML file:



// blog-list.component.html
<h2>Title</h2>
<ng-content></ng-content>
<ng-content select="[blog-text]"></ng-content>


Enter fullscreen mode Exit fullscreen mode

Or in the @component decorator:



@Component({
  selector: 'blog-list'
  template: `
    <h2>Title</h2>
    <ng-content></ng-content>
    <ng-content select="[blog-text]"></ng-content>
  `
})


Enter fullscreen mode Exit fullscreen mode

Then, in your parent component, you use the <ng-content> selector you just defined on the element you want to pass to it.



// parent component
<blog-list>
  <p blog-text>My text here</p>
</blog-list>

// result
<h2>Title</h2>
<p>My text here</p>


Enter fullscreen mode Exit fullscreen mode

Just like in the previous examples, the unnamed slot will take all the content you pass without using a defined selector.

You can also use a CSS class as a selector.



// child component
<ng-content select=".blog-text"></ng-content>

// parent component
<blog-list>
  <p class="blog-text" >My text here</p>
</blog-list>


Enter fullscreen mode Exit fullscreen mode

You can also use a string as a selector and use the ngProjectAs attribute.



// child component
<ng-content select="blog-text"></ng-content>

// parent component
<blog-list>
  <p ngProjectAs="blog-text" >My text here</p>
</blog-list>


Enter fullscreen mode Exit fullscreen mode

You can even configure your ng-module to all for tags as selectors like <blog-text>.

Angular also provides conditional content projection using <ng-template>.

Conclusion

Having worked in React (and AngularJS 2013) in the past, it took me some time to start thinking with <slot>s, not props when writing Vue components. Creating some <slot>s manually with the Web Components spec and inspecting them with Chrome DevTools helped a lot!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .