本章では、Vue.jsのコンポーネントシステムについて学びます。単一ファイルコンポーネント(SFC)の構造と利点、Propsを使用したデータの受け渡し、Emitsを使用したイベントの発信方法について理解を深めます。また、コンポーネントのライフサイクルと再利用可能なコンポーネントの設計についても学びます。
コンポーネントは、Vue.jsアプリケーションの基本的な構成要素です。コンポーネントを使用することで、UIを独立した再利用可能なパーツに分割できます。大規模なアプリケーションでは、コンポーネントベースの開発が必須となります。
Vue.jsでは、単一ファイルコンポーネント(Single File Component, SFC)と呼ばれる特別なファイル形式を使用します。これは.vue
拡張子を持つファイルで、テンプレート、スクリプト、スタイルを1つのファイルにカプセル化します。
<template>
<!-- HTMLテンプレート -->
<div class="greeting">
<h1>{{ message }}</h1>
<button @click="greet">挨拶する</button>
</div>
</template>
<script setup>
// JavaScriptコード
import { ref } from 'vue'
const message = ref('こんにちは、Vue!')
function greet() {
alert('こんにちは!')
}
</script>
<style scoped>
/* CSSスタイル */
.greeting {
text-align: center;
padding: 20px;
}
h1 {
color: #42b983;
}
button {
background-color: #42b983;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
}
</style>
scoped
属性を使用して、スタイルをコンポーネント内に閉じ込められる
Vue 3では、<script setup>
構文を使用することで、より簡潔にComposition APIを記述できます。従来のsetup()
関数に比べて以下の利点があります:
グローバル登録を使用すると、アプリケーション全体でコンポーネントを使用できます。main.js
ファイルで行うことが一般的です。
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import GlobalComponent from './components/GlobalComponent.vue'
const app = createApp(App)
// グローバルコンポーネントの登録
app.component('GlobalComponent', GlobalComponent)
app.mount('#app')
ローカル登録では、コンポーネントを使用する親コンポーネント内でインポートして使用します。これが推奨される方法です。
<script setup>
// コンポーネントのインポート
import ChildComponent from './components/ChildComponent.vue'
</script>
<template>
<div>
<h2>親コンポーネント</h2>
<ChildComponent />
</div>
</template>
<script setup>
を使用する場合、インポートしたコンポーネントは自動的に登録され、テンプレートで使用できます。通常の<script>
ブロックを使用する場合は、components
オプションで明示的に登録する必要があります。
Propsは、親コンポーネントから子コンポーネントにデータを渡す方法です。子コンポーネントはPropsを通じて親からのデータを受け取り、表示や処理に使用できます。
<!-- UserCard.vue (子コンポーネント) -->
<script setup>
// defineProps を使用してプロパティを定義
const props = defineProps({
// 基本的な型チェック
username: String,
// 詳細な定義(型、必須フラグ、デフォルト値)
email: {
type: String,
required: true
},
// デフォルト値を持つプロパティ
role: {
type: String,
default: 'user'
},
// 配列のデフォルト値(ファクトリ関数が必要)
tags: {
type: Array,
default: () => []
},
// バリデーション関数
age: {
type: Number,
validator: (value) => value >= 0 && value < 120
}
})
// props はテンプレート内で直接使用可能
// また、propsはreactiveなので、値の変更を監視できます
<!-- ParentComponent.vue (親コンポーネント) -->
<script setup>
import { ref } from 'vue'
import UserCard from './components/UserCard.vue'
const currentUser = ref({
username: '山田太郎',
email: 'taro@example.com',
role: 'admin',
tags: ['開発', 'マネージャー'],
age: 32
})
</script>
<template>
<div>
<h2>ユーザー情報</h2>
<!-- 静的な値を渡す -->
<UserCard
username="ゲスト"
email="guest@example.com"
:age="25"
/>
<!-- 動的な値を渡す(v-bindを使用) -->
<UserCard
:username="currentUser.username"
:email="currentUser.email"
:role="currentUser.role"
:tags="currentUser.tags"
:age="currentUser.age"
/>
<!-- スプレッド構文を使用して全てのプロパティを渡す -->
<UserCard v-bind="currentUser" />
</div>
</template>
JavaScriptではキャメルケース(camelCase)、HTMLではケバブケース(kebab-case)を使用するのが一般的です。Vue.jsは自動的に変換を行います。
<!-- 子コンポーネント内 -->
<script setup>
defineProps({
postTitle: String, // camelCase
authorName: String // camelCase
})
</script>
<!-- 親コンポーネント内 -->
<template>
<BlogPost
post-title="Vue.jsの基本" <!-- kebab-case -->
author-name="山田太郎" <!-- kebab-case -->
/>
</template>
Vueでは以下の型がサポートされています:
String
Number
Boolean
Array
Object
Date
Function
Symbol
また、カスタムクラスやコンストラクタ関数も型として使用できます。
Propsは常に親から子への単方向のデータフローに従います。親のプロパティが更新されると、子のプロパティも更新されますが、その逆は起こりません。これにより、子コンポーネントが誤って親の状態を変更することを防ぎます。
子コンポーネント内でプロパティを変更しようとすると、Vueは警告を出します。プロパティを基にローカルの状態を作成したい場合は、以下の方法があります:
<script setup>
import { ref, computed } from 'vue'
const props = defineProps({
initialCount: {
type: Number,
default: 0
}
})
// 1. プロパティを基にローカルデータを作成
const count = ref(props.initialCount)
// 2. 計算プロパティを使用して変換
const formattedCount = computed(() => `カウント: ${props.initialCount}`)
</script>
Emitsは、子コンポーネントから親コンポーネントにイベントを発信する方法です。これにより、子は親に対して通知を行ったり、データを送信したりできます。
<!-- SearchForm.vue (子コンポーネント) -->
<script setup>
import { ref } from 'vue'
// emitsの定義
const emit = defineEmits([
'search', // 基本的なイベント
'reset', // 引数のないイベント
'update:query' // v-modelのための更新イベント
])
// v-modelで使用する検索クエリ
const searchQuery = ref('')
// 検索イベントの発信
function handleSearch() {
if (searchQuery.value.trim()) {
// イベント名と引数を指定して発信
emit('search', searchQuery.value)
}
}
// リセットイベントの発信
function handleReset() {
searchQuery.value = ''
emit('reset')
}
// 入力値の変更時にv-model更新イベントを発信
function updateQuery(event) {
const value = event.target.value
searchQuery.value = value
emit('update:query', value)
}
</script>
<template>
<div class="search-form">
<input
:value="searchQuery"
@input="updateQuery"
placeholder="検索ワードを入力..."
type="text"
>
<button @click="handleSearch">検索</button>
<button @click="handleReset">リセット</button>
</div>
</template>
Propsと同様に、Emitsもオブジェクト構文を使用してバリデーションを追加できます。
<script setup>
const emit = defineEmits({
// バリデーション関数
search: (query) => {
// 空の検索クエリを拒否
if (!query.trim()) {
console.warn('検索クエリが空です')
return false
}
return true
},
// ペイロードがオブジェクトであることを確認
'filter-change': (filters) => {
return typeof filters === 'object' && filters !== null
}
})
</script>
<!-- ParentComponent.vue (親コンポーネント) -->
<script setup>
import { ref } from 'vue'
import SearchForm from './components/SearchForm.vue'
const searchQuery = ref('')
const searchResults = ref([])
// 子コンポーネントからのイベントを処理
function handleSearch(query) {
console.log(`検索ワード: ${query}`)
// 検索処理を実行...
searchResults.value = [`${query}に関する結果1`, `${query}に関する結果2`]
}
function handleReset() {
searchResults.value = []
console.log('検索をリセットしました')
}
</script>
<template>
<div>
<h2>検索アプリ</h2>
<!-- イベントリスナーを設定 -->
<SearchForm
@search="handleSearch"
@reset="handleReset"
@update:query="searchQuery = $event"
/>
<!-- または、v-modelを使用(@update:queryと:queryをまとめたもの) -->
<SearchForm
v-model:query="searchQuery"
@search="handleSearch"
@reset="handleReset"
/>
<!-- 現在の検索クエリを表示 -->
<p v-if="searchQuery">現在の検索: {{ searchQuery }}</p>
<!-- 検索結果を表示 -->
<div v-if="searchResults.length">
<h3>検索結果</h3>
<ul>
<li v-for="(result, index) in searchResults" :key="index">
{{ result }}
</li>
</ul>
</div>
</div>
</template>
Vue.jsでは、コンポーネント間の通信に以下のパターンが一般的に使用されます。
最も基本的な通信パターンです。親から子にはpropsでデータを渡し、子から親にはイベント(emits)で通知します。
フォーム要素やカスタムコンポーネントで双方向バインディングを実現するためのパターンです。内部的には、プロパティとイベントの組み合わせです。
<!-- CustomInput.vue (子コンポーネント) -->
<script setup>
// modelValueはv-modelのデフォルトプロパティ名
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
function updateValue(event) {
emit('update:modelValue', event.target.value)
}
</script>
<template>
<input
:value="modelValue"
@input="updateValue"
class="custom-input"
>
</template>
<!-- 親コンポーネント -->
<template>
<CustomInput v-model="searchText" />
<!-- 上記は以下と同等 -->
<CustomInput
:modelValue="searchText"
@update:modelValue="searchText = $event"
/>
</template>
Vue 3では、1つのコンポーネントに対して複数のv-modelバインディングを設定できます。
<!-- UserForm.vue (子コンポーネント) -->
<script setup>
const props = defineProps({
firstName: String,
lastName: String
})
const emit = defineEmits([
'update:firstName',
'update:lastName'
])
function updateFirstName(event) {
emit('update:firstName', event.target.value)
}
function updateLastName(event) {
emit('update:lastName', event.target.value)
}
</script>
<template>
<div>
<input :value="firstName" @input="updateFirstName" placeholder="名">
<input :value="lastName" @input="updateLastName" placeholder="姓">
</div>
</template>
<!-- 親コンポーネント -->
<template>
<UserForm
v-model:firstName="user.firstName"
v-model:lastName="user.lastName"
/>
</template>
Vue.jsのコンポーネントには、作成から破棄までのライフサイクルがあります。各段階でフックを使用して、特定のタイミングでコードを実行できます。
<script setup>
import { onMounted, onUpdated, onUnmounted, onBeforeUnmount } from 'vue'
// コンポーネントがマウントされた(DOMに追加された)後に実行
onMounted(() => {
console.log('コンポーネントがマウントされました')
// 外部APIからデータを取得するなどの初期化処理
})
// コンポーネントが更新された後に実行
onUpdated(() => {
console.log('コンポーネントが更新されました')
})
// コンポーネントが削除される直前に実行
onBeforeUnmount(() => {
console.log('コンポーネントが間もなく削除されます')
// クリーンアップ処理の準備
})
// コンポーネントが削除された後に実行
onUnmounted(() => {
console.log('コンポーネントが削除されました')
// イベントリスナーの削除やタイマーのクリアなど
})
</script>
onBeforeMount
: マウント開始前onBeforeUpdate
: 再レンダリング前onErrorCaptured
: 子孫コンポーネントからのエラーをキャプチャonRenderTracked
: リアクティブ依存関係が初めて追跡されたとき(開発モードのみ)onRenderTriggered
: 再レンダリングがトリガーされたとき(開発モードのみ)onActivated
: <keep-alive>
で囲まれたコンポーネントがアクティブになったときonDeactivated
: <keep-alive>
で囲まれたコンポーネントが非アクティブになったときスロットを使用すると、親コンポーネントから子コンポーネントへHTMLコンテンツを渡すことができます。これにより、柔軟なコンポーネント設計が可能になります。
<!-- BaseCard.vue (子コンポーネント) -->
<template>
<div class="card">
<div class="card-header">
<h3>{{ title }}</h3>
</div>
<div class="card-body">
<!-- ここにスロットを配置 -->
<slot>デフォルトコンテンツ</slot>
</div>
</div>
</template>
<script setup>
defineProps({
title: {
type: String,
default: 'カード'
}
})
</script>
<style scoped>
.card {
border: 1px solid #ddd;
border-radius: 8px;
overflow: hidden;
}
.card-header {
background-color: #f5f5f5;
padding: 10px 15px;
border-bottom: 1px solid #ddd;
}
.card-body {
padding: 15px;
}
</style>
<!-- 親コンポーネント -->
<template>
<BaseCard title="プロフィール">
<!-- このコンテンツがスロットに挿入される -->
<p>名前: 山田太郎</p>
<p>職業: プログラマー</p>
</BaseCard>
<BaseCard title="通知">
<p>新しいメッセージが3件あります。</p>
</BaseCard>
<!-- デフォルトコンテンツが表示される -->
<BaseCard title="情報なし" />
</template>
複数のスロットを使用したい場合は、名前付きスロットを使用します。
<!-- BaseLayout.vue (子コンポーネント) -->
<template>
<div class="container">
<header class="header">
<slot name="header">デフォルトヘッダー</slot>
</header>
<main class="content">
<!-- デフォルトスロット(name="default"と同じ) -->
<slot>デフォルトコンテンツ</slot>
</main>
<footer class="footer">
<slot name="footer">デフォルトフッター</slot>
</footer>
</div>
</template>
<!-- 親コンポーネント -->
<template>
<BaseLayout>
<!-- v-slot:header または #header -->
<template #header>
<h1>ウェブサイトのタイトル</h1>
<nav>
<a href="#">ホーム</a>
<a href="#">概要</a>
<a href="#">お問い合わせ</a>
</nav>
</template>
<!-- デフォルトスロット(v-slot:default) -->
<p>メインコンテンツがここに入ります。</p>
<!-- v-slot:footer または #footer -->
<template #footer>
<p>© 2023 私のウェブサイト</p>
</template>
</BaseLayout>
</template>
子コンポーネントのデータを親コンポーネントのスロットコンテンツで使用したい場合は、スコープ付きスロットを使用します。
<!-- ItemList.vue (子コンポーネント) -->
<script setup>
const props = defineProps({
items: {
type: Array,
required: true
}
})
</script>
<template>
<ul class="item-list">
<li v-for="(item, index) in items" :key="index">
<!-- item と index を親コンポーネントに渡す -->
<slot :item="item" :index="index">
<!-- デフォルト表示 -->
{{ item.text }}
</slot>
</li>
</ul>
</template>
<!-- 親コンポーネント -->
<script setup>
import { ref } from 'vue'
const items = ref([
{ id: 1, text: '項目1', status: 'active' },
{ id: 2, text: '項目2', status: 'completed' },
{ id: 3, text: '項目3', status: 'active' }
])
</script>
<template>
<ItemList :items="items">
<!-- v-slot="slotProps" でスロットのプロパティを受け取る -->
<template v-slot="{ item, index }">
<div :class="{ completed: item.status === 'completed' }">
{{ index + 1 }}. {{ item.text }}
<span v-if="item.status === 'completed'">(完了)</span>
</div>
</template>
</ItemList>
</template>
効果的な再利用可能なコンポーネントを設計するためのベストプラクティスを紹介します。
コンポーネントは1つの責任を持つべきです。複数の役割を持つコンポーネントは、より小さなコンポーネントに分割することを検討しましょう。
コンポーネントは明確なインターフェースを持つべきです。必要なpropsとemitsを明示的に定義し、適切な検証とドキュメンテーションを提供しましょう。
<script setup>
// 明確なインターフェースの例
const props = defineProps({
// 必要なプロパティを明確に定義
label: {
type: String,
required: true,
default: '送信'
},
loading: {
type: Boolean,
default: false
},
variant: {
type: String,
default: 'primary',
validator: (value) => ['primary', 'secondary', 'danger'].includes(value)
}
})
// 発信するイベントを明確に定義
const emit = defineEmits({
click: null,
'focus': null
})
</script>
小さな単一目的のコンポーネントを組み合わせて、より複雑なUIを構築しましょう。
<!-- 小さなコンポーネントの組み合わせ例 -->
<template>
<Card>
<CardHeader>
<h2>{{ title }}</h2>
</CardHeader>
<CardBody>
<TextField
v-model="username"
label="ユーザー名"
required
/>
<PasswordField
v-model="password"
label="パスワード"
required
/>
</CardBody>
<CardFooter>
<Button
variant="secondary"
@click="cancel"
>
キャンセル
</Button>
<Button
variant="primary"
:loading="isSubmitting"
@click="submit"
>
送信
</Button>
</CardFooter>
</Card>
</template>
再利用可能なコンポーネントは、さまざまな状況に対応できるよう、十分に柔軟であるべきです。propsやスロットを使用して、外観や振る舞いをカスタマイズできるようにしましょう。
コンポーネントは、プレゼンテーション(見た目)とロジック(振る舞い)を明確に分離すべきです。これにより、保守性とテスト容易性が向上します。
<!-- コンポーザブル関数でロジックを分離する例 -->
<script setup>
import { ref } from 'vue'
import { useForm } from './composables/useForm'
// ロジックをコンポーザブル関数に抽出
const { formData, errors, validate, handleSubmit } = useForm({
initialData: {
username: '',
password: ''
},
validationRules: {
username: (value) => !!value || 'ユーザー名は必須です',
password: (value) => value.length >= 6 || 'パスワードは6文字以上必要です'
},
onSubmit: async (data) => {
// フォーム送信処理
console.log('送信データ:', data)
}
})
</script>
<template>
<!-- プレゼンテーション部分 -->
<form @submit.prevent="handleSubmit">
<div class="form-group">
<label for="username">ユーザー名</label>
<input
id="username"
v-model="formData.username"
type="text"
@blur="validate('username')"
>
<p v-if="errors.username" class="error">{{ errors.username }}</p>
</div>
<div class="form-group">
<label for="password">パスワード</label>
<input
id="password"
v-model="formData.password"
type="password"
@blur="validate('password')"
>
<p v-if="errors.password" class="error">{{ errors.password }}</p>
</div>
<button type="submit">送信</button>
</form>
</template>
これまで学んだ概念を組み合わせて、再利用可能なフォームコンポーネントを作成してみましょう。
<!-- BaseInput.vue -->
<script setup>
// プロパティの定義
const props = defineProps({
modelValue: {
type: [String, Number],
default: ''
},
label: {
type: String,
default: ''
},
placeholder: {
type: String,
default: ''
},
type: {
type: String,
default: 'text',
validator: (value) => [
'text', 'password', 'email', 'number', 'tel', 'url'
].includes(value)
},
required: {
type: Boolean,
default: false
},
disabled: {
type: Boolean,
default: false
},
error: {
type: String,
default: ''
}
})
// イベントの定義
const emit = defineEmits([
'update:modelValue',
'blur',
'focus',
'input'
])
// 入力値の更新
function updateValue(event) {
emit('update:modelValue', event.target.value)
}
// ブラーイベントの処理
function handleBlur(event) {
emit('blur', event)
}
// フォーカスイベントの処理
function handleFocus(event) {
emit('focus', event)
}
</script>
<template>
<div class="form-control" :class="{ 'has-error': error }">
<label v-if="label" :for="label">
{{ label }}
<span v-if="required" class="required">*</span>
</label>
<input
:id="label"
:value="modelValue"
@input="updateValue"
@blur="handleBlur"
@focus="handleFocus"
:type="type"
:placeholder="placeholder"
:required="required"
:disabled="disabled"
class="input"
>
<p v-if="error" class="error-message">{{ error }}</p>
</div>
</template>
<style scoped>
.form-control {
margin-bottom: 1rem;
}
label {
display: block;
margin-bottom: 0.5rem;
font-weight: bold;
}
.required {
color: #ff4c4c;
margin-left: 0.25rem;
}
.input {
width: 100%;
padding: 0.5rem;
font-size: 1rem;
border: 1px solid #ccc;
border-radius: 4px;
}
.has-error .input {
border-color: #ff4c4c;
}
.error-message {
margin-top: 0.25rem;
color: #ff4c4c;
font-size: 0.875rem;
}
</style>
<!-- BaseSelect.vue -->
<script setup>
// プロパティの定義
const props = defineProps({
modelValue: {
type: [String, Number],
default: ''
},
options: {
type: Array,
required: true
},
label: {
type: String,
default: ''
},
placeholder: {
type: String,
default: '選択してください'
},
required: {
type: Boolean,
default: false
},
disabled: {
type: Boolean,
default: false
},
error: {
type: String,
default: ''
},
valueKey: {
type: String,
default: 'value'
},
labelKey: {
type: String,
default: 'label'
}
})
// イベントの定義
const emit = defineEmits([
'update:modelValue',
'change'
])
// 値の更新
function updateValue(event) {
emit('update:modelValue', event.target.value)
emit('change', event.target.value)
}
</script>
<template>
<div class="form-control" :class="{ 'has-error': error }">
<label v-if="label" :for="label">
{{ label }}
<span v-if="required" class="required">*</span>
</label>
<select
:id="label"
:value="modelValue"
@change="updateValue"
:required="required"
:disabled="disabled"
class="select"
>
<option value="" disabled>{{ placeholder }}</option>
<!-- オプションが単純な配列の場合 -->
<option
v-for="option in options"
:key="typeof option === 'object' ? option[valueKey] : option"
:value="typeof option === 'object' ? option[valueKey] : option"
>
{{ typeof option === 'object' ? option[labelKey] : option }}
</option>
</select>
<p v-if="error" class="error-message">{{ error }}</p>
</div>
</template>
<style scoped>
.form-control {
margin-bottom: 1rem;
}
label {
display: block;
margin-bottom: 0.5rem;
font-weight: bold;
}
.required {
color: #ff4c4c;
margin-left: 0.25rem;
}
.select {
width: 100%;
padding: 0.5rem;
font-size: 1rem;
border: 1px solid #ccc;
border-radius: 4px;
background-color: white;
}
.has-error .select {
border-color: #ff4c4c;
}
.error-message {
margin-top: 0.25rem;
color: #ff4c4c;
font-size: 0.875rem;
}
</style>
<!-- BaseForm.vue -->
<script setup>
// イベントの定義
const emit = defineEmits([
'submit',
'reset'
])
// フォーム送信処理
function handleSubmit(event) {
emit('submit', event)
}
// リセット処理
function handleReset(event) {
emit('reset', event)
}
</script>
<template>
<form
@submit.prevent="handleSubmit"
@reset.prevent="handleReset"
class="form"
>
<!-- スロットでフォーム要素を受け取る -->
<slot></slot>
<!-- フッタースロット -->
<div class="form-footer">
<slot name="footer">
<button type="reset" class="btn btn-secondary">リセット</button>
<button type="submit" class="btn btn-primary">送信</button>
</slot>
</div>
</form>
</template>
<style scoped>
.form {
max-width: 100%;
}
.form-footer {
display: flex;
justify-content: flex-end;
gap: 1rem;
margin-top: 1.5rem;
}
.btn {
padding: 0.5rem 1rem;
font-size: 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
}
.btn-primary {
background-color: #42b983;
color: white;
}
.btn-secondary {
background-color: #f5f5f5;
color: #333;
}
</style>
<!-- UserForm.vue (実装例) -->
<script setup>
import { ref, reactive } from 'vue'
import BaseForm from './components/BaseForm.vue'
import BaseInput from './components/BaseInput.vue'
import BaseSelect from './components/BaseSelect.vue'
// フォームデータ
const formData = reactive({
name: '',
email: '',
role: '',
password: ''
})
// バリデーションエラー
const errors = reactive({
name: '',
email: '',
role: '',
password: ''
})
// 役割オプション
const roleOptions = [
{ value: 'user', label: '一般ユーザー' },
{ value: 'editor', label: '編集者' },
{ value: 'admin', label: '管理者' }
]
// 送信中フラグ
const isSubmitting = ref(false)
// バリデーション関数
function validate() {
let isValid = true
// 名前の検証
if (!formData.name.trim()) {
errors.name = '名前を入力してください'
isValid = false
} else {
errors.name = ''
}
// メールアドレスの検証
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!formData.email.trim()) {
errors.email = 'メールアドレスを入力してください'
isValid = false
} else if (!emailRegex.test(formData.email)) {
errors.email = '有効なメールアドレスを入力してください'
isValid = false
} else {
errors.email = ''
}
// 役割の検証
if (!formData.role) {
errors.role = '役割を選択してください'
isValid = false
} else {
errors.role = ''
}
// パスワードの検証
if (!formData.password) {
errors.password = 'パスワードを入力してください'
isValid = false
} else if (formData.password.length < 6) {
errors.password = 'パスワードは6文字以上にしてください'
isValid = false
} else {
errors.password = ''
}
return isValid
}
// フォーム送信
async function handleSubmit() {
if (!validate()) {
return
}
isSubmitting.value = true
try {
// APIリクエストをシミュレート
await new Promise(resolve => setTimeout(resolve, 1000))
console.log('送信データ:', formData)
alert('ユーザーが正常に登録されました!')
// フォームをリセット
Object.keys(formData).forEach(key => {
formData[key] = ''
})
} catch (error) {
console.error('エラー:', error)
} finally {
isSubmitting.value = false
}
}
// フォームリセット
function handleReset() {
// フォームデータをリセット
Object.keys(formData).forEach(key => {
formData[key] = ''
})
// エラーをリセット
Object.keys(errors).forEach(key => {
errors[key] = ''
})
}
</script>
<template>
<div class="user-form-container">
<h2>ユーザー登録</h2>
<BaseForm
@submit="handleSubmit"
@reset="handleReset"
>
<BaseInput
v-model="formData.name"
label="名前"
placeholder="山田太郎"
required
:error="errors.name"
/>
<BaseInput
v-model="formData.email"
label="メールアドレス"
type="email"
placeholder="example@example.com"
required
:error="errors.email"
/>
<BaseSelect
v-model="formData.role"
:options="roleOptions"
label="役割"
required
:error="errors.role"
/>
<BaseInput
v-model="formData.password"
label="パスワード"
type="password"
required
:error="errors.password"
/>
<template #footer>
<button type="reset" class="btn btn-secondary">
クリア
</button>
<button
type="submit"
class="btn btn-primary"
:disabled="isSubmitting"
>
{{ isSubmitting ? '送信中...' : '登録' }}
</button>
</template>
</BaseForm>
</div>
</template>
<style scoped>
.user-form-container {
max-width: 500px;
margin: 0 auto;
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
h2 {
margin-top: 0;
margin-bottom: 1.5rem;
color: #333;
}
.btn {
padding: 0.5rem 1rem;
font-size: 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
}
.btn-primary {
background-color: #42b983;
color: white;
}
.btn-primary:disabled {
background-color: #8ece8e;
cursor: not-allowed;
}
.btn-secondary {
background-color: #f5f5f5;
color: #333;
}
</style>
本章では、Vue.jsのコンポーネントシステムについて学びました。単一ファイルコンポーネント(SFC)の構造と利点、Propsを使用したデータの受け渡し、Emitsを使用したイベントの発信方法について理解を深めました。また、スロットを使用したコンテンツの配信、コンポーネントのライフサイクル、そして再利用可能なコンポーネントの設計についても学びました。
これらの概念を組み合わせることで、保守性が高く、再利用可能なコンポーネントを作成できます。実践的な例として、フォームコンポーネントを実装し、実際のアプリケーション開発でどのようにコンポーネントを組み合わせるかを学びました。
次の章では、Vue Routerを使用した画面遷移について学びます。