🎯

Vue3のスロット機能を完全理解!基本から応用まで徹底解説

に公開

Vue3のスロット機能を完全理解!基本から応用まで徹底解説

Vue3のスロット(Slot)機能は、コンポーネントの再利用性と柔軟性を大幅に向上させる重要な機能です。この記事では、スロットの基本概念から実践的な応用パターンまで、段階的に詳しく解説していきます。

スロットとは

スロットは、親コンポーネントから子コンポーネントにコンテンツを渡すための仕組みです。HTMLの要素のように、コンポーネントの開始タグと終了タグの間にコンテンツを記述し、子コンポーネント内で<slot>要素を使って表示します。

基本的なスロットの例

<!-- ChildComponent.vue -->
<template>
  <div class="card">
    <h3>カードタイトル</h3>
    <slot></slot>
  </div>
</template>
<!-- ParentComponent.vue -->
<template>
  <ChildComponent>
    <p>これはスロットに渡されるコンテンツです</p>
  </ChildComponent>
</template>

結果:

<div class="card">
  <h3>カードタイトル</h3>
  <p>これはスロットに渡されるコンテンツです</p>
</div>

デフォルトスロット

最も基本的なスロットがデフォルトスロットです。<slot>要素に名前を指定しない場合、デフォルトスロットとして扱われます。

実践例:モーダルコンポーネント

<!-- Modal.vue -->
<template>
  <div v-if="isOpen" class="modal-overlay" @click="close">
    <div class="modal-content" @click.stop>
      <div class="modal-header">
        <h2>{{ title }}</h2>
        <button @click="close" class="close-btn">&times;</button>
      </div>
      <div class="modal-body">
        <slot></slot>
      </div>
      <div class="modal-footer">
        <slot name="footer">
          <button @click="close">閉じる</button>
        </slot>
      </div>
    </div>
  </div>
</template>

<script setup>
defineProps({
  isOpen: Boolean,
  title: String
})

const emit = defineEmits(['close'])

const close = () => {
  emit('close')
}
</script>

使用例:

<template>
  <Modal :is-open="showModal" title="確認" @close="showModal = false">
    <p>本当に削除しますか?</p>
    <template #footer>
      <button @click="deleteItem">削除</button>
      <button @click="showModal = false">キャンセル</button>
    </template>
  </Modal>
</template>

名前付きスロット

複数のスロットを使用する場合は、名前付きスロットを使用します。<slot name="slotName">で定義し、親コンポーネントでは<template #slotName>で指定します。

実践例:レイアウトコンポーネント

<!-- Layout.vue -->
<template>
  <div class="layout">
    <header class="header">
      <slot name="header"></slot>
    </header>
    <main class="main">
      <slot></slot>
    </main>
    <aside class="sidebar">
      <slot name="sidebar"></slot>
    </aside>
    <footer class="footer">
      <slot name="footer"></slot>
    </footer>
  </div>
</template>

使用例:

<template>
  <Layout>
    <template #header>
      <h1>サイトタイトル</h1>
      <nav>ナビゲーション</nav>
    </template>
    
    <p>メインコンテンツ</p>
    
    <template #sidebar>
      <div>サイドバーコンテンツ</div>
    </template>
    
    <template #footer>
      <p>&copy; 2024 My Site</p>
    </template>
  </Layout>
</template>

スコープ付きスロット

スコープ付きスロットは、子コンポーネントから親コンポーネントにデータを渡すことができる高度な機能です。

基本的なスコープ付きスロット

<!-- UserList.vue -->
<template>
  <div class="user-list">
    <div v-for="user in users" :key="user.id" class="user-item">
      <slot :user="user" :index="index"></slot>
    </div>
  </div>
</template>

<script setup>
defineProps({
  users: Array
})
</script>

使用例:

<template>
  <UserList :users="users">
    <template #default="{ user, index }">
      <div class="user-card">
        <img :src="user.avatar" :alt="user.name">
        <h3>{{ user.name }}</h3>
        <p>{{ user.email }}</p>
        <span class="user-number">#{{ index + 1 }}</span>
      </div>
    </template>
  </UserList>
</template>

実践例:データテーブルコンポーネント

<!-- DataTable.vue -->
<template>
  <div class="data-table">
    <table>
      <thead>
        <tr>
          <th v-for="column in columns" :key="column.key">
            {{ column.label }}
          </th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="(item, index) in data" :key="item.id">
          <td v-for="column in columns" :key="column.key">
            <slot 
              :name="column.key" 
              :item="item" 
              :index="index"
              :value="item[column.key]"
            >
              {{ item[column.key] }}
            </slot>
          </td>
        </tr>
      </tbody>
    </table>
  </div>
</template>

<script setup>
defineProps({
  data: Array,
  columns: Array
})
</script>

使用例:

<template>
  <DataTable :data="users" :columns="columns">
    <template #avatar="{ item }">
      <img :src="item.avatar" :alt="item.name" class="avatar">
    </template>
    
    <template #status="{ value }">
      <span :class="['status', `status--${value}`]">
        {{ value }}
      </span>
    </template>
    
    <template #actions="{ item }">
      <button @click="editUser(item)">編集</button>
      <button @click="deleteUser(item)">削除</button>
    </template>
  </DataTable>
</template>

<script setup>
const users = ref([
  { id: 1, name: '田中太郎', email: 'tanaka@example.com', avatar: '/avatar1.jpg', status: 'active' },
  { id: 2, name: '佐藤花子', email: 'sato@example.com', avatar: '/avatar2.jpg', status: 'inactive' }
])

const columns = [
  { key: 'avatar', label: 'アバター' },
  { key: 'name', label: '名前' },
  { key: 'email', label: 'メール' },
  { key: 'status', label: 'ステータス' },
  { key: 'actions', label: '操作' }
]
</script>

動的スロット

動的スロットを使用すると、実行時にスロット名を決定できます。

<!-- DynamicSlot.vue -->
<template>
  <div class="dynamic-slot">
    <slot :name="slotName" :data="data"></slot>
  </div>
</template>

<script setup>
defineProps({
  slotName: String,
  data: Object
})
</script>

使用例:

<template>
  <DynamicSlot :slot-name="currentView" :data="userData">
    <template #profile="{ data }">
      <div>プロフィール: {{ data.name }}</div>
    </template>
    
    <template #settings="{ data }">
      <div>設定: {{ data.email }}</div>
    </template>
  </DynamicSlot>
</template>

<script setup>
const currentView = ref('profile')
const userData = ref({ name: '田中太郎', email: 'tanaka@example.com' })
</script>

条件付きスロット

条件に応じてスロットの内容を表示する方法です。

<!-- ConditionalSlot.vue -->
<template>
  <div class="conditional-slot">
    <slot v-if="showContent" name="content"></slot>
    <slot v-else name="loading"></slot>
  </div>
</template>

<script setup>
defineProps({
  showContent: Boolean
})
</script>

使用例:

<template>
  <ConditionalSlot :show-content="!loading">
    <template #content>
      <div>データが読み込まれました</div>
    </template>
    
    <template #loading>
      <div class="spinner">読み込み中...</div>
    </template>
  </ConditionalSlot>
</template>

スロットのフォールバック

スロットにフォールバックコンテンツを設定できます。親コンポーネントからコンテンツが渡されない場合に表示されます。

<!-- Button.vue -->
<template>
  <button class="btn" :class="variant">
    <slot>
      <span>デフォルトボタン</span>
    </slot>
  </button>
</template>

<script setup>
defineProps({
  variant: {
    type: String,
    default: 'primary'
  }
})
</script>

使用例:

<!-- フォールバックコンテンツが表示される -->
<Button variant="secondary" />

<!-- カスタムコンテンツが表示される -->
<Button variant="danger">
  <i class="icon-delete"></i>
  削除
</Button>

実践的な応用パターン

1. カードコンポーネント

<!-- Card.vue -->
<template>
  <div class="card" :class="cardClass">
    <div v-if="$slots.header" class="card-header">
      <slot name="header"></slot>
    </div>
    
    <div class="card-body">
      <slot></slot>
    </div>
    
    <div v-if="$slots.footer" class="card-footer">
      <slot name="footer"></slot>
    </div>
  </div>
</template>

<script setup>
const props = defineProps({
  variant: {
    type: String,
    default: 'default',
    validator: (value) => ['default', 'primary', 'success', 'warning', 'danger'].includes(value)
  },
  shadow: {
    type: Boolean,
    default: false
  }
})

const cardClass = computed(() => [
  `card--${props.variant}`,
  { 'card--shadow': props.shadow }
])
</script>

2. フォームフィールドコンポーネント

<!-- FormField.vue -->
<template>
  <div class="form-field">
    <label v-if="label" class="form-field__label">
      {{ label }}
      <span v-if="required" class="required">*</span>
    </label>
    
    <div class="form-field__input">
      <slot :field="fieldProps" :error="error"></slot>
    </div>
    
    <div v-if="error" class="form-field__error">
      {{ error }}
    </div>
    
    <div v-if="$slots.help" class="form-field__help">
      <slot name="help"></slot>
    </div>
  </div>
</template>

<script setup>
const props = defineProps({
  label: String,
  required: Boolean,
  error: String,
  modelValue: [String, Number, Boolean, Array, Object]
})

const emit = defineEmits(['update:modelValue'])

const fieldProps = computed(() => ({
  value: props.modelValue,
  'onUpdate:modelValue': (value) => emit('update:modelValue', value)
}))
</script>

使用例:

<template>
  <FormField 
    label="ユーザー名" 
    :required="true" 
    :error="errors.username"
    v-model="username"
  >
    <template #default="{ field }">
      <input 
        v-bind="field"
        type="text" 
        placeholder="ユーザー名を入力"
        class="form-input"
      />
    </template>
    
    <template #help>
      <p>3文字以上20文字以下で入力してください</p>
    </template>
  </FormField>
</template>

スロットのベストプラクティス

1. 適切なスロット名の使用

<!-- 良い例:意味のある名前 -->
<slot name="header"></slot>
<slot name="content"></slot>
<slot name="footer"></slot>

<!-- 悪い例:意味のない名前 -->
<slot name="slot1"></slot>
<slot name="slot2"></slot>

2. スコープ付きスロットでの型安全性

<!-- UserCard.vue -->
<template>
  <div class="user-card">
    <slot :user="user" :isLoading="isLoading"></slot>
  </div>
</template>

<script setup lang="ts">
interface User {
  id: number
  name: string
  email: string
}

defineProps<{
  user: User
  isLoading: boolean
}>()
</script>

3. スロットの存在チェック

<template>
  <div class="component">
    <div v-if="$slots.header" class="header">
      <slot name="header"></slot>
    </div>
    
    <div class="content">
      <slot></slot>
    </div>
    
    <div v-if="$slots.footer" class="footer">
      <slot name="footer"></slot>
    </div>
  </div>
</template>

まとめ

Vue3のスロット機能は、コンポーネントの再利用性と柔軟性を大幅に向上させる強力な機能です。この記事で紹介した内容をまとめると:

基本機能

  • デフォルトスロット: 基本的なコンテンツの受け渡し
  • 名前付きスロット: 複数のスロットを使った柔軟なレイアウト
  • スコープ付きスロット: 子から親へのデータの受け渡し

応用機能

  • 動的スロット: 実行時のスロット名決定
  • 条件付きスロット: 条件に応じたスロット表示
  • フォールバック: デフォルトコンテンツの設定

ベストプラクティス

  • 意味のあるスロット名の使用
  • 型安全性の確保
  • スロットの存在チェック

これらの機能を適切に活用することで、保守性が高く、再利用可能なVue3コンポーネントを構築できます。スロットはVue3の重要な機能の一つなので、ぜひ実際のプロジェクトで活用してみてください!

GitHubで編集を提案

Discussion