JAVASCRIPT

Advanced Content Projection with Vue 3 Scoped Slots

Master Vue 3's scoped slots to allow parent components to customize the rendering of child components by passing data back to the slot content.

// App.vue
<template>
  <div>
    <h1>App Root</h1>
    <UserList :users="activeUsers">
      <!-- Default slot content -->
      <template #header>
        <h2>Currently Active Users</h2>
      </template>

      <!-- Scoped slot for customizing each user item -->
      <template #user-item="{ user, index, toggleStatus }">
        <li :class="{ 'inactive': !user.isActive }">
          {{ index + 1 }}. {{ user.name }} ({{ user.email }})
          <button @click="toggleStatus(user.id)">
            {{ user.isActive ? 'Deactivate' : 'Activate' }}
          </button>
        </li>
      </template>

      <!-- Default slot for footer -->
      <template #footer>
        <p>Total active users: {{ activeUsers.length }}</p>
      </template>
    </UserList>

    <hr>

    <h2>Another User List Example</h2>
    <UserList :users="inactiveUsers">
      <template #header>
        <h2>Inactive Members</h2>
      </template>
      <template #user-item="{ user }">
        <div class="card">
          <h3>{{ user.name }}</h3>
          <p>Status: {{ user.isActive ? 'Active' : 'Inactive' }}</p>
        </div>
      </template>
    </UserList>
  </div>
</template>

<script setup>
import { ref } from 'vue';
import UserList from './components/UserList.vue';

const activeUsers = ref([
  { id: 1, name: 'Alice', email: '[email protected]', isActive: true },
  { id: 2, name: 'Bob', email: '[email protected]', isActive: true },
  { id: 3, name: 'Charlie', email: '[email protected]', isActive: false }
]);

const inactiveUsers = ref([
  { id: 4, name: 'David', email: '[email protected]', isActive: false },
  { id: 5, name: 'Eve', email: '[email protected]', isActive: true } // Example for toggling
]);
</script>

<style>
.inactive {
  text-decoration: line-through;
  color: gray;
}
.card {
  border: 1px solid #ccc;
  padding: 10px;
  margin: 5px;
  border-radius: 5px;
}
</style>

// components/UserList.vue
<template>
  <div class="user-list-container">
    <slot name="header"></slot>
    <ul>
      <li v-if="users.length === 0">No users to display.</li>
      <slot name="user-item" v-for="user in users" :key="user.id" :user="user" :index="users.indexOf(user)" :toggleStatus="toggleUserStatus"></slot>
    </ul>
    <slot name="footer"></slot>
  </div>
</template>

<script setup>
import { defineProps, defineEmits } from 'vue';

const props = defineProps({
  users: {
    type: Array,
    required: true
  }
});

// We'll mutate the users array here for simplicity,
// in a real app you might emit an event to the parent
// to update the user status if `users` is a prop.
const toggleUserStatus = (userId) => {
  const user = props.users.find(u => u.id === userId);
  if (user) {
    user.isActive = !user.isActive;
    console.log(`User ${user.name} status toggled to ${user.isActive}`);
  }
};
</script>

<style scoped>
.user-list-container {
  border: 1px solid #eee;
  padding: 15px;
  margin: 20px 0;
  border-radius: 8px;
}
</style>
How it works: Vue 3's scoped slots allow a parent component to render content within a child component, while also providing data from the child component back to the parent for use in that content. In this example, `UserList` provides `user`, `index`, and a `toggleStatus` method to its `user-item` slot. The parent component then uses `template #user-item="{ user, index, toggleStatus }"` to destructure these props and define how each user item should be displayed and interacted with, creating highly customizable and reusable components like lists or grids.

Need help integrating this into your project?

Our team of expert developers can help you build your custom application from scratch.

Hire DigitalCodeLabs