メインコンテンツへスキップ
見出し画像

Vue.js 親子コンポーネント 0から完全ガイド 子から親をマスターしよう編 #一部無料


    1. そもそも「コンポーネント」って何?

    🏗️ 日常生活で例えると

    コンポーネント = 部品

    家を建てる時...
    🏠 家全体 ← これが「アプリケーション」
    ├── 🚪 ドア ← これが「コンポーネント」
    ├── 🪟 窓 ← これが「コンポーネント」
    ├── 🔥 キッチン ← これが「コンポーネント」
    └── 🛏️ ベッドルーム ← これが「コンポーネント」
    
    それぞれの部品(コンポーネント)は:
    ✅ 独立して作れる
    ✅ 何度でも使い回せる
    ✅ 他の部品と組み合わせられる
    

    💻 ウェブサイトで例えると

    ウェブサイト
    ├── ヘッダー(ロゴ、メニュー)
    ├── サイドバー(ナビゲーション)
    ├── メインコンテンツ
    │   ├── 記事カード
    │   ├── 記事カード  
    │   └── 記事カード
    └── フッター(リンク、著作権)
    

    Vue.js では、これらの「部品」がコンポーネントです!

    2. 「親子関係」って何?

    👨‍👩‍👧‍👦 家族で例えると

    👨 お父さん(親)
    ├── 👧 娘(子)
    ├── 👦 息子(子)
    └── 👶 赤ちゃん(子)
    
    お父さんが子供たちに:
    📢 「お小遣いをあげる」(親→子にデータを渡す)
    👂 「子供の話を聞く」(子→親にメッセージを送る)
    

    💻 Vue.js で例えると

    📄 App.vue(親コンポーネント)
    ├── 📝 Header.vue(子コンポーネント)
    ├── 📝 MainContent.vue(子コンポーネント)
    └── 📝 Footer.vue(子コンポーネント)
    
    親が子に:
    📤 データを渡す(プロパティ)
    📥 子からの連絡を受け取る(イベント)
    

    3. 一番簡単な例から始めよう

    ステップ1: 子コンポーネントを作る

    まず、一番シンプルな子コンポーネントを作りましょう。

    <!-- ChildComponent.vue(子コンポーネント) -->
    <template>
      <div class="child">
        <h2>私は子コンポーネントです!</h2>
        <p>こんにちは!</p>
      </div>
    </template>
    
    <script>
    export default {
      name: 'ChildComponent'  // コンポーネントの名前
    }
    </script>
    
    <style>
    .child {
      border: 2px solid blue;
      padding: 10px;
      margin: 10px;
    }
    </style>
    

    解説:

    • <template>: HTMLの部分(見た目)

    • <script>: JavaScriptの部分(動作)

    • <style>: CSSの部分(スタイル)

    ステップ2: 親コンポーネントで子を使う

    <!-- ParentComponent.vue(親コンポーネント) -->
    <template>
      <div class="parent">
        <h1>私は親コンポーネントです!</h1>
        
        <!-- 子コンポーネントを使用 -->
        <ChildComponent />
        <ChildComponent />
        <ChildComponent />
      </div>
    </template>
    
    <script>
    // 子コンポーネントをインポート(読み込み)
    import ChildComponent from './ChildComponent.vue'
    
    export default {
      name: 'ParentComponent',
      components: {
        ChildComponent  // 子コンポーネントを登録
      }
    }
    </script>
    
    <style>
    .parent {
      border: 2px solid red;
      padding: 20px;
    }
    </style>
    

    表示結果:

    ┌─────────────────────────────────────┐
    │ 私は親コンポーネントです!              │
    │ ┌─────────────────────────────────┐   │
    │ │ 私は子コンポーネントです!        │   │
    │ │ こんにちは!                   │   │
    │ └─────────────────────────────────┘   │
    │ ┌─────────────────────────────────┐   │
    │ │ 私は子コンポーネントです!        │   │
    │ │ こんにちは!                   │   │
    │ └─────────────────────────────────┘   │
    │ ┌─────────────────────────────────┐   │
    │ │ 私は子コンポーネントです!        │   │
    │ │ こんにちは!                   │   │
    │ └─────────────────────────────────┘   │
    └─────────────────────────────────────┘
    

    4. データを親から子に渡してみよう(Props)

    🎁 「プロパティ」って何?

    プロパティ(Props)= 親から子への贈り物

    👨 お父さん: 「娘ちゃん、お小遣い1000円だよ」
    👧 娘: 「ありがとう!1000円もらった!」
    
    Vue.js では:
    👨 親コンポーネント: 「子よ、"Hello"というデータだよ」
    👧 子コンポーネント: 「"Hello"を受け取りました!」
    

    実際のコード例

    子コンポーネント(データを受け取る側)

    <!-- ChildComponent.vue -->
    <template>
      <div class="child">
        <h2>私は子コンポーネントです!</h2>
        <p>親からもらったメッセージ: {{ message }}</p>
        <p>親からもらった数字: {{ number }}</p>
      </div>
    </template>
    
    <script>
    export default {
      name: 'ChildComponent',
      props: {
        // 親から受け取るデータを定義
        message: String,  // 文字列型のデータ
        number: Number    // 数値型のデータ
      }
    }
    </script>
    

    解説:

    • props: 親から受け取るデータの設定

    • String, Number: データの型を指定

    • {{ message }}: テンプレート内でデータを表示

    親コンポーネント(データを渡す側)

    <!-- ParentComponent.vue -->
    <template>
      <div class="parent">
        <h1>私は親コンポーネントです!</h1>
        
        <!-- 子にデータを渡す -->
        <ChildComponent 
          :message="greeting" 
          :number="favoriteNumber" 
        />
        
        <ChildComponent 
          :message="anotherGreeting" 
          :number="anotherNumber" 
        />
      </div>
    </template>
    
    <script>
    import ChildComponent from './ChildComponent.vue'
    
    export default {
      name: 'ParentComponent',
      components: {
        ChildComponent
      },
      data() {
        return {
          greeting: 'こんにちは!元気ですか?',
          favoriteNumber: 42,
          anotherGreeting: 'おはようございます!',
          anotherNumber: 100
        }
      }
    }
    </script>
    

    重要なポイント:

    • :message="greeting": :(コロン)を付けると動的なデータ

    • message="こんにちは": :なしだと固定の文字列

    表示結果:

    ┌─────────────────────────────────────┐
    │ 私は親コンポーネントです!              │
    │ ┌─────────────────────────────────┐   │
    │ │ 私は子コンポーネントです!        │   │
    │ │ 親からもらったメッセージ:         │   │
    │ │ こんにちは!元気ですか?          │   │
    │ │ 親からもらった数字: 42           │   │
    │ └─────────────────────────────────┘   │
    │ ┌─────────────────────────────────┐   │
    │ │ 私は子コンポーネントです!        │   │
    │ │ 親からもらったメッセージ:         │   │
    │ │ おはようございます!             │   │
    │ │ 親からもらった数字: 100          │   │
    │ └─────────────────────────────────┘   │
    └─────────────────────────────────────┘
    

    5. 子から親にメッセージを送ってみよう(Events)

    📢 「イベント」って何?

    イベント = 子から親への報告

    👧 娘: 「お父さん、宿題終わったよ!」
    👨 お父さん: 「よく頑張ったね!ご褒美をあげよう」
    
    Vue.js では:
    👧 子コンポーネント: 「親よ、ボタンが押されました!」
    👨 親コンポーネント: 「了解!何か処理をしよう」
    

    実際のコード例

    子コンポーネント(メッセージを送る側)

    <!-- ChildComponent.vue -->
    <template>
      <div class="child">
        <h2>私は子コンポーネントです!</h2>
        <p>親からもらったメッセージ: {{ message }}</p>
        
        <!-- ボタンを押すと親にメッセージを送る -->
        <button @click="sendMessageToParent">親にメッセージを送る</button>
        <button @click="sendNumberToParent">親に数字を送る</button>
      </div>
    </template>
    
    <script>
    export default {
      name: 'ChildComponent',
      props: {
        message: String
      },
      methods: {
        sendMessageToParent() {
          // 親に「hello-parent」というイベントとメッセージを送る
          this.$emit('hello-parent', '子からのメッセージです!')
        },
        sendNumberToParent() {
          // 親に「number-sent」というイベントと数字を送る
          this.$emit('number-sent', 123)
        }
      }
    }
    </script>
    

    解説:

    • this.$emit('イベント名', データ): 親にメッセージを送る

    • @click: ボタンがクリックされた時の処理

    親コンポーネント(メッセージを受け取る側)

    <!-- ParentComponent.vue -->
    <template>
      <div class="parent">
        <h1>私は親コンポーネントです!</h1>
        <p>子からのメッセージ: {{ childMessage }}</p>
        <p>子からの数字: {{ childNumber }}</p>
        
        <!-- 子からのイベントを受け取る -->
        <ChildComponent 
          :message="greeting"
          @hello-parent="receiveChildMessage"
          @number-sent="receiveChildNumber"
        />
      </div>
    </template>
    
    <script>
    import ChildComponent from './ChildComponent.vue'
    
    export default {
      name: 'ParentComponent',
      components: {
        ChildComponent
      },
      data() {
        return {
          greeting: 'こんにちは!',
          childMessage: 'まだメッセージがありません',
          childNumber: 0
        }
      },
      methods: {
        receiveChildMessage(message) {
          // 子から受け取ったメッセージを保存
          this.childMessage = message
          console.log('子からメッセージが届きました:', message)
        },
        receiveChildNumber(number) {
          // 子から受け取った数字を保存
          this.childNumber = number
          console.log('子から数字が届きました:', number)
        }
      }
    }
    </script>
    

    重要なポイント:

    • @hello-parent="receiveChildMessage": @でイベントを受け取る

    • receiveChildMessage(message): メソッドの引数で子からのデータを受け取る

    6. 実用的な例:カウンターアプリ

    実際に動くアプリを作ってみましょう!

    子コンポーネント(カウンターボタン)

    <!-- CounterButton.vue -->
    <template>
      <div class="counter-button">
        <h3>{{ title }}</h3>
        <p>現在の値: {{ currentValue }}</p>
        <button @click="increment">+1する</button>
        <button @click="decrement">-1する</button>
        <button @click="reset">リセット</button>
      </div>
    </template>
    
    <script>
    export default {
      name: 'CounterButton',
      props: {
        title: String,        // ボタンのタイトル
        currentValue: Number  // 現在の値
      },
      methods: {
        increment() {
          // 親に「値を増やして」と連絡
          this.$emit('increment')
        },
        decrement() {
          // 親に「値を減らして」と連絡
          this.$emit('decrement')
        },
        reset() {
          // 親に「値をリセットして」と連絡
          this.$emit('reset')
        }
      }
    }
    </script>
    
    <style>
    .counter-button {
      border: 1px solid #ccc ;
      padding: 15px;
      margin: 10px;
      border-radius: 5px;
    }
    
    button {
      margin: 5px;
      padding: 5px 10px;
    }
    </style>
    

    親コンポーネント(カウンター管理)

    <!-- CounterApp.vue -->
    <template>
      <div class="counter-app">
        <h1>カウンターアプリ</h1>
        <p>合計: {{ total }}</p>
        
        <!-- 複数のカウンターボタン -->
        <CounterButton
          title="カウンター1"
          :currentValue="counter1"
          @increment="incrementCounter1"
          @decrement="decrementCounter1"
          @reset="resetCounter1"
        />
        
        <CounterButton
          title="カウンター2"
          :currentValue="counter2"
          @increment="incrementCounter2"
          @decrement="decrementCounter2"
          @reset="resetCounter2"
        />
        
        <CounterButton
          title="カウンター3"
          :currentValue="counter3"
          @increment="incrementCounter3"
          @decrement="decrementCounter3"
          @reset="resetCounter3"
        />
        
        <button @click="resetAll">全てリセット</button>
      </div>
    </template>
    
    <script>
    import CounterButton from './CounterButton.vue'
    
    export default {
      name: 'CounterApp',
      components: {
        CounterButton
      },
      data() {
        return {
          counter1: 0,
          counter2: 0,
          counter3: 0
        }
      },
      computed: {
        // 全カウンターの合計を計算
        total() {
          return this.counter1 + this.counter2 + this.counter3
        }
      },
      methods: {
        // カウンター1の操作
        incrementCounter1() {
          this.counter1++
        },
        decrementCounter1() {
          this.counter1--
        },
        resetCounter1() {
          this.counter1 = 0
        },
        
        // カウンター2の操作
        incrementCounter2() {
          this.counter2++
        },
        decrementCounter2() {
          this.counter2--
        },
        resetCounter2() {
          this.counter2 = 0
        },
        
        // カウンター3の操作
        incrementCounter3() {
          this.counter3++
        },
        decrementCounter3() {
          this.counter3--
        },
        resetCounter3() {
          this.counter3 = 0
        },
        
        // 全てリセット
        resetAll() {
          this.counter1 = 0
          this.counter2 = 0
          this.counter3 = 0
        }
      }
    }
    </script>
    
    <style>
    .counter-app {
      max-width: 600px;
      margin: 0 auto;
      padding: 20px;
    }
    </style>
    

    7. もっと実用的な例:Todo リスト

    子コンポーネント(Todo項目)

    <!-- TodoItem.vue -->
    <template>
      <div class="todo-item" :class="{ completed: todo.completed }">
        <input 
          type="checkbox" 
          :checked="todo.completed"
          @change="toggleComplete"
        />
        <span class="todo-text">{{ todo.text }}</span>
        <button @click="deleteTodo" class="delete-btn">削除</button>
      </div>
    </template>
    
    <script>
    export default {
      name: 'TodoItem',
      props: {
        todo: Object  // Todo項目のデータ
      },
      methods: {
        toggleComplete() {
          // 親に「完了状態を切り替えて」と連絡
          this.$emit('toggle-complete', this.todo.id)
        },
        deleteTodo() {
          // 親に「この項目を削除して」と連絡
          this.$emit('delete-todo', this.todo.id)
        }
      }
    }
    </script>
    
    <style>
    .todo-item {
      display: flex;
      align-items: center;
      padding: 10px;
      border-bottom: 1px solid #eee ;
    }
    
    .todo-text {
      flex: 1;
      margin: 0 10px;
    }
    
    .completed .todo-text {
      text-decoration: line-through;
      color: #999 ;
    }
    
    .delete-btn {
      background: #ff4444 ;
      color: white;
      border: none;
      padding: 5px 10px;
      border-radius: 3px;
      cursor: pointer;
    }
    </style>
    

    親コンポーネント(Todo管理)

    <!-- TodoApp.vue -->
    <template>
      <div class="todo-app">
        <h1>Todoリスト</h1>
        
        <!-- 新しいTodoを追加するフォーム -->
        <div class="add-todo">
          <input 
            v-model="newTodoText" 
            @keyup.enter="addTodo"
            placeholder="新しいTodoを入力..."
          />
          <button @click="addTodo">追加</button>
        </div>
        
        <!-- 統計情報 -->
        <div class="stats">
          <p>全体: {{ todos.length }}個</p>
          <p>完了: {{ completedCount }}個</p>
          <p>未完了: {{ remainingCount }}個</p>
        </div>
        
        <!-- Todo項目一覧 -->
        <div class="todo-list">
          <TodoItem
            v-for="todo in todos"
            :key="todo.id"
            :todo="todo"
            @toggle-complete="toggleTodoComplete"
            @delete-todo="deleteTodo"
          />
        </div>
        
        <!-- 一括操作ボタン -->
        <div class="bulk-actions">
          <button @click="markAllComplete">全て完了</button>
          <button @click="deleteCompleted">完了済みを削除</button>
          <button @click="deleteAll">全て削除</button>
        </div>
      </div>
    </template>
    
    <script>
    import TodoItem from './TodoItem.vue'
    
    export default {
      name: 'TodoApp',
      components: {
        TodoItem
      },
      data() {
        return {
          newTodoText: '',
          todos: [
            { id: 1, text: 'Vue.jsを学習する', completed: false },
            { id: 2, text: 'コンポーネントを理解する', completed: true },
            { id: 3, text: 'アプリを作ってみる', completed: false }
          ]
        }
      },
      computed: {
        completedCount() {
          return this.todos.filter(todo => todo.completed).length
        },
        remainingCount() {
          return this.todos.filter(todo => !todo.completed).length
        }
      },
      methods: {
        addTodo() {
          if (this.newTodoText.trim()) {
            const newTodo = {
              id: Date.now(), // 簡単なID生成
              text: this.newTodoText,
              completed: false
            }
            this.todos.push(newTodo)
            this.newTodoText = ''
          }
        },
        
        toggleTodoComplete(todoId) {
          const todo = this.todos.find(t => t.id === todoId)
          if (todo) {
            todo.completed = !todo.completed
          }
        },
        
        deleteTodo(todoId) {
          this.todos = this.todos.filter(t => t.id !== todoId)
        },
        
        markAllComplete() {
          this.todos.forEach(todo => {
            todo.completed = true
          })
        },
        
        deleteCompleted() {
          this.todos = this.todos.filter(todo => !todo.completed)
        },
        
        deleteAll() {
          if (confirm('本当に全て削除しますか?')) {
            this.todos = []
          }
        }
      }
    }
    </script>
    
    <style>
    .todo-app {
      max-width: 500px;
      margin: 0 auto;
      padding: 20px;
    }
    
    .add-todo {
      display: flex;
      margin-bottom: 20px;
    }
    
    .add-todo input {
      flex: 1;
      padding: 10px;
      border: 1px solid #ddd ;
      border-radius: 4px;
    }
    
    .add-todo button {
      padding: 10px 15px;
      margin-left: 10px;
      background: #007bff ;
      color: white;
      border: none;
      border-radius: 4px;
      cursor: pointer;
    }
    
    .stats {
      background: #f8f9fa ;
      padding: 10px;
      border-radius: 4px;
      margin-bottom: 20px;
    }
    
    .stats p {
      margin: 5px 0;
    }
    
    .todo-list {
      border: 1px solid #ddd ;
      border-radius: 4px;
      margin-bottom: 20px;
    }
    
    .bulk-actions button {
      margin: 5px;
      padding: 8px 12px;
      border: none;
      border-radius: 4px;
      cursor: pointer;
    }
    
    .bulk-actions button:nth-child(1) { background: #28a745 ; color: white; }
    .bulk-actions button:nth-child(2) { background: #ffc107 ; color: black; }
    .bulk-actions button:nth-child(3) { background: #dc3545 ; color: white; }
    </style>
    

    8. まとめ:親子コンポーネントの重要ポイント

    🎯 データの流れ

    親コンポーネント
    │
    ├── データ(data)
    │   ↓ Props(:propName)
    ├── 子コンポーネント1
    │   ↑ Events(@eventName)
    ├── メソッド(methods)
    │
    ├── データ(data)
    │   ↓ Props(:propName)
    ├── 子コンポーネント2
    │   ↑ Events(@eventName)
    └── メソッド(methods)
    

    💡 覚えておくべき文法

    <!-- 子コンポーネントの使用 -->
    <ChildComponent 
      :prop1="parentData1"      <!-- データを渡す(親→子) -->
      :prop2="parentData2"      <!-- データを渡す(親→子) -->
      @event1="parentMethod1"   <!-- イベントを受け取る(子→親) -->
      @event2="parentMethod2"   <!-- イベントを受け取る(子→親) -->
    />
    
    // 子コンポーネント内
    export default {
      props: {
        prop1: String,    // 親からのデータを受け取る
        prop2: Number
      },
      methods: {
        someMethod() {
          // 親にイベントを送る
          this.$emit('event1', 'some data')
        }
      }
    }
    

    🚀 実践のコツ

    1. 小さく始める: まず簡単な例から

    2. 1つずつ理解: PropsとEventsを別々に学習

    3. console.logを活用: データの流れを確認

    4. 実際に作ってみる: 手を動かして覚える

    ❌ よくある間違い

    <!-- ❌ 間違い -->
    <ChildComponent message="parentData" />    <!-- :が抜けている -->
    
    <!-- ✅ 正しい -->
    <ChildComponent :message="parentData" />   <!-- :を付ける -->
    
    <!-- ❌ 間違い -->
    <ChildComponent v-on:click="method" />     <!-- @を使おう -->
    
    <!-- ✅ 正しい -->
    <ChildComponent @click="method" />         <!-- @を使う -->
    

    これで Vue.js の親子コンポーネントの基礎は完璧です!実際にコードを書いて試してみてくださいね!

    あなたへのおすすめ