Vue.js コンポーネント演習問題

2025-07-29

初級問題 (6問)

問題1

単一ファイルコンポーネント(.vueファイル)の基本的な構造を説明し、3つの主要セクションをコードで示してください。

問題2

以下のコンポーネントを単一ファイルコンポーネントとして完成させてください。コンポーネント名はButtonCounterで、クリックするとカウントが増えるボタンです。

<template>
  <!-- ここにテンプレートを記述 -->
</template>

<script>
<!-- ここにスクリプトを記述 -->
</script>

<style>
<!-- ここにスタイルを記述 -->
</style>

問題3

親コンポーネントから子コンポーネントにmessageというデータをpropsで渡すコードを書いてください。親コンポーネントのデータは"Hello Vue!"、子コンポーネントはこのメッセージを表示します。

問題4

子コンポーネントから親コンポーネントにイベントを発行するコードを書いてください。子コンポーネントにはボタンがあり、クリックするとincrementイベントを発行します。親コンポーネントはこのイベントを受け取ってカウンターを増やします。

問題5

以下の単一ファイルコンポーネントの間違いを指摘し、修正してください。

<template>
  <div>
    <h1>{{ title }}</h1>
    <p>{{ content }}</p>
  </div>
</template>

<script>
export default {
  data: {
    title: 'こんにちは',
    content: 'Vue.jsの世界へようこそ'
  }
}
</script>

問題6

コンポーネントにローカルスタイルを適用する際に、スタイルが他のコンポーネントに影響を与えないようにする方法をコードで示してください。

中級問題 (12問)

問題7

動的コンポーネントの使い方を示すコードを書いてください。2つのコンポーネント(ComponentAComponentB)を切り替えるボタンを作成してください。

問題8

スロットを使用して、親コンポーネントから子コンポーネントCardにコンテンツを渡すコードを書いてください。Cardコンポーネントはヘッダー、本文、フッターの3つのスロットを持っています。

問題9

propsのバリデーションを設定した子コンポーネントを作成してください。ageというpropは数値で必須、18以上である必要があります。

問題10

親子コンポーネント間で双方向バインディングを実現するコードを書いてください。子コンポーネントはv-modelに対応した入力コンポーネントです。

問題11

非同期コンポーネントの登録方法を示すコードを書いてください。AsyncComponentは必要になったときにのみ読み込まれるようにしてください。

問題12

子コンポーネントでv-forを使用してリストを表示し、各アイテムに一意のキーを設定するコードを書いてください。親コンポーネントからアイテムの配列をpropsで受け取ります。

問題13

コンポジションAPIを使用した単一ファイルコンポーネントの例を書いてください。counterというリアクティブなデータと、それを増減するメソッドを含めてください。

問題14

provide/injectを使用して、祖先コンポーネントから深くネストされた子コンポーネントにデータを渡すコードを書いてください。テーマ情報(theme: 'dark')を渡します。

問題15

カスタムイベントを使用して、孫コンポーネントから祖父母コンポーネントにイベントを伝播させるコードを書いてください。イベント名はnotifyで、メッセージを含みます。

問題16

子コンポーネントのルート要素にrefを設定し、親コンポーネントからその要素にアクセスするコードを書いてください。

問題17

関数型コンポーネントの例を作成してください。FunctionalButtonというコンポーネントで、クリックイベントを親に伝達します。

問題18

コンポーネントのライフサイクルフックを使用して、コンポーネントがマウントされた時と更新された時にコンソールにログを出力するコードを書いてください。

上級問題 (6問)

問題19

動的スロット名を使用した高度なスロットの使い方を示すコードを書いてください。親コンポーネントから動的にスロット名を決定して子コンポーネントにコンテンツを渡します。

問題20

レンダー関数を使用して単一ファイルコンポーネントを作成してください。h1要素とp要素を含むシンプルなコンポーネントにしてください。

問題21

カスタムディレクティブを単一ファイルコンポーネントで使用する例を示してください。要素をクリックしたときに背景色が変わるv-highlightディレクティブを作成してください。

問題22

プラグインとして利用可能なコンポーネントを作成してください。このコンポーネントはVue.use()で登録でき、グローバルコンポーネントとして使用できます。

問題23

スコープ付きスロットを使用して、子コンポーネントのデータを親コンポーネントのテンプレートで使用するコードを書いてください。子コンポーネントはアイテムのリストを持ち、親はその表示方法を制御します。

問題24

Vue 3のTeleport機能を使用して、モーダルコンポーネントをbody要素の直接の子としてレンダリングするコードを書いてください。


解答例

初級解答

解答1

<template>
  <!-- HTMLテンプレート -->
  <div>{{ message }}</div>
</template>

<script>
<!-- JavaScriptロジック -->
export default {
  data() {
    return {
      message: 'Hello Vue!'
    }
  }
}
</script>

<style>
<!-- CSSスタイル -->
div {
  color: blue;
}
</style>

解答2

<template>
  <button @click="count++">Clicked {{ count }} times</button>
</template>

<script>
export default {
  name: 'ButtonCounter',
  data() {
    return {
      count: 0
    }
  }
}
</script>

<style>
button {
  padding: 8px 16px;
  background-color: #42b983;
  color: white;
  border: none;
  border-radius: 4px;
}
</style>

解答3

親コンポーネント:

<template>
  <ChildComponent :message="parentMessage" />
</template>

<script>
import ChildComponent from './ChildComponent.vue'

export default {
  components: { ChildComponent },
  data() {
    return {
      parentMessage: 'Hello Vue!'
    }
  }
}
</script>

子コンポーネント:

<template>
  <div>{{ message }}</div>
</template>

<script>
export default {
  props: ['message']
}
</script>

解答4

子コンポーネント:

<template>
  <button @click="$emit('increment')">Increment</button>
</template>

親コンポーネント:

<template>
  <div>
    <p>Count: {{ count }}</p>
    <ChildComponent @increment="count++" />
  </div>
</template>

<script>
import ChildComponent from './ChildComponent.vue'

export default {
  components: { ChildComponent },
  data() {
    return {
      count: 0
    }
  }
}
</script>

解答5

<template>
  <div>
    <h1>{{ title }}</h1>
    <p>{{ content }}</p>
  </div>
</template>

<script>
export default {
  data() {  // dataは関数にする必要がある
    return {
      title: 'こんにちは',
      content: 'Vue.jsの世界へようこそ'
    }
  }
}
</script>

解答6

<style scoped>
  /* scoped属性を追加することでスタイルのスコープを限定 */
  h1 {
    color: red;
  }
</style>

中級解答

解答7

<template>
  <div>
    <button @click="currentComponent = 'ComponentA'">Show A</button>
    <button @click="currentComponent = 'ComponentB'">Show B</button>
    <component :is="currentComponent"></component>
  </div>
</template>

<script>
import ComponentA from './ComponentA.vue'
import ComponentB from './ComponentB.vue'

export default {
  components: { ComponentA, ComponentB },
  data() {
    return {
      currentComponent: 'ComponentA'
    }
  }
}
</script>

解答8

子コンポーネント (Card.vue):

<template>
  <div class="card">
    <header>
      <slot name="header"></slot>
    </header>
    <main>
      <slot></slot>  <!-- デフォルトスロット -->
    </main>
    <footer>
      <slot name="footer"></slot>
    </footer>
  </div>
</template>

親コンポーネント:

<template>
  <Card>
    <template v-slot:header>
      <h2>タイトル</h2>
    </template>

    <p>メインコンテンツ</p>

    <template v-slot:footer>
      <button>詳細</button>
    </template>
  </Card>
</template>

解答9

<template>
  <div v-if="age !== null">
    Age: {{ age }}
  </div>
</template>

<script>
export default {
  props: {
    age: {
      type: Number,
      required: true,
      validator: value => value >= 18
    }
  }
}
</script>

解答10

子コンポーネント:

<template>
  <input 
    :value="modelValue"
    @input="$emit('update:modelValue', $event.target.value)"
  />
</template>

<script>
export default {
  props: ['modelValue'],
  emits: ['update:modelValue']
}
</script>

親コンポーネント:

<template>
  <CustomInput v-model="message" />
  <p>Message: {{ message }}</p>
</template>

<script>
import CustomInput from './CustomInput.vue'

export default {
  components: { CustomInput },
  data() {
    return {
      message: ''
    }
  }
}
</script>

解答11

const AsyncComponent = () => ({
  component: import('./AsyncComponent.vue'),
  loading: LoadingComponent,
  error: ErrorComponent,
  delay: 200,
  timeout: 3000
})

export default {
  components: {
    AsyncComponent
  }
}

解答12

子コンポーネント:

<template>
  <ul>
    <li v-for="item in items" :key="item.id">
      {{ item.name }}
    </li>
  </ul>
</template>

<script>
export default {
  props: {
    items: {
      type: Array,
      required: true
    }
  }
}
</script>

解答13

<template>
  <div>
    <p>Count: {{ counter }}</p>
    <button @click="increment">+</button>
    <button @click="decrement">-</button>
  </div>
</template>

<script>
import { ref } from 'vue'

export default {
  setup() {
    const counter = ref(0)

    const increment = () => counter.value++
    const decrement = () => counter.value--

    return {
      counter,
      increment,
      decrement
    }
  }
}
</script>

解答14

祖先コンポーネント:

<template>
  <ParentComponent />
</template>

<script>
import { provide } from 'vue'
import ParentComponent from './ParentComponent.vue'

export default {
  components: { ParentComponent },
  setup() {
    provide('theme', 'dark')
  }
}
</script>

子孫コンポーネント:

<template>
  <div :class="theme">
    <!-- コンテンツ -->
  </div>
</template>

<script>
import { inject } from 'vue'

export default {
  setup() {
    const theme = inject('theme', 'light') // デフォルト値 'light'

    return {
      theme
    }
  }
}
</script>

解答15

孫コンポーネント:

<template>
  <button @click="notifyGrandparent">Notify</button>
</template>

<script>
export default {
  methods: {
    notifyGrandparent() {
      this.$emit('notify', 'Hello from grandchild!')
    }
  }
}
</script>

子コンポーネント:

<template>
  <GrandchildComponent @notify="$emit('notify', $event)" />
</template>

親コンポーネント:

<template>
  <ChildComponent @notify="handleNotify" />
</template>

<script>
import ChildComponent from './ChildComponent.vue'

export default {
  components: { ChildComponent },
  methods: {
    handleNotify(message) {
      console.log(message) // "Hello from grandchild!"
    }
  }
}
</script>

解答16

子コンポーネント:

<template>
  <div ref="root">Child Component</div>
</template>

親コンポーネント:

<template>
  <ChildComponent ref="child" />
</template>

<script>
import ChildComponent from './ChildComponent.vue'

export default {
  components: { ChildComponent },
  mounted() {
    console.log(this.$refs.child.$refs.root) // 子のdiv要素にアクセス
  }
}
</script>

解答17

<template functional>
  <button 
    class="functional-btn"
    v-bind="attrs"
    v-on="listeners"
  >
    <slot></slot>
  </button>
</template>

<style>
.functional-btn {
  padding: 8px 16px;
  background-color: #eee;
}
</style>

解答18

<template>
  <div>Lifecycle Hooks Example</div>
</template>

<script>
export default {
  mounted() {
    console.log('Component mounted')
  },
  updated() {
    console.log('Component updated')
  }
}
</script>

上級解答

解答19

親コンポーネント:

<template>
  <DynamicSlotComponent>
    <template v-slot:[slotName]>
      Dynamic Slot Content
    </template>
  </DynamicSlotComponent>
</template>

<script>
import DynamicSlotComponent from './DynamicSlotComponent.vue'

export default {
  components: { DynamicSlotComponent },
  data() {
    return {
      slotName: 'header' // 動的に変更可能
    }
  }
}
</script>

子コンポーネント:

<template>
  <div>
    <slot name="header"></slot>
  </div>
</template>

解答20

<script>
export default {
  render() {
    return this.$h('div', [
      this.$h('h1', 'Title'),
      this.$h('p', 'Content')
    ])
  }
}
</script>

解答21

<template>
  <div v-highlight>Click me to highlight</div>
</template>

<script>
export default {
  directives: {
    highlight: {
      mounted(el) {
        el.addEventListener('click', () => {
          el.style.backgroundColor = 'yellow'
        })
      }
    }
  }
}
</script>

解答22

// GlobalComponent.vue
<template>
  <div>Global Component</div>
</template>

<script>
export default {
  name: 'GlobalComponent'
}
</script>

// プラグインインストール用ファイル
export default {
  install(app) {
    app.component('GlobalComponent', require('./GlobalComponent.vue').default)
  }
}

解答23

子コンポーネント:

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

<script>
export default {
  props: {
    items: {
      type: Array,
      required: true
    }
  }
}
</script>

親コンポーネント:

<template>
  <ItemList :items="items">
    <template v-slot:default="slotProps">
      {{ slotProps.item.name }} - {{ slotProps.item.price }}円
    </template>
  </ItemList>
</template>

<script>
import ItemList from './ItemList.vue'

export default {
  components: { ItemList },
  data() {
    return {
      items: [
        { id: 1, name: 'Item 1', price: 1000 },
        { id: 2, name: 'Item 2', price: 2000 }
      ]
    }
  }
}
</script>

解答24

<template>
  <teleport to="body">
    <div class="modal" v-if="show">
      <div class="modal-content">
        <slot></slot>
        <button @click="show = false">Close</button>
      </div>
    </div>
  </teleport>
</template>

<script>
export default {
  data() {
    return {
      show: false
    }
  },
  methods: {
    open() {
      this.show = true
    },
    close() {
      this.show = false
    }
  }
}
</script>

<style>
.modal {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0,0,0,0.5);
  display: flex;
  justify-content: center;
  align-items: center;
}

.modal-content {
  background: white;
  padding: 20px;
  border-radius: 5px;
}
</style>