LoginSignup
22

More than 3 years have passed since last update.

Vue.jsチュートリアル 〜Vue.js + TypeScriptでTrelloもどきを作ろう①〜

Last updated at Posted at 2019-12-13

みなさん、お待たせしました。コスパエンジニアのyoshiharu2580です。
フロントエンド業務に就いてもうすぐ半年が経ちます。
まだまだ未熟者ですがこのあたりで少し初心に返るために、下のTrelloのようなタスク管理ツールを作成するチュートリアルを作りました。
ezgif.com-video-to-gif (1).gif
詳細な説明は公式ドキュメントに譲るとして、この記事ではとりあえず動かすことを目標とします。
Vue.js, TypeScriptが初めての方でも、なんとなく理解できましたら幸いです。

対象読者

  • Vue.jsを触ったことがない人
  • TypeScriptを触ったことがない人
  • HTML, CSS, JavaScriptが少しわかる人

初期セットアップ

vue-cliで環境を構築します。(公式ドキュメント)
※2019.11.28時点でv 4.0.5

$ yarn global add @vue/cli
$ vue create my-project
# 各項目の、:の後の内容を選択してください。

? Please pick a preset: Manually select features
? Check the features needed for your project: Babel, TS, CSS Pre-processors, Lin
ter
? Use class-style component syntax? Yes
? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfi
lls, transpiling JSX)? Yes
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported
by default): Sass/SCSS (with node-sass)
? Pick a linter / formatter config: Prettier
? Pick additional lint features: Lint on save, Lint and fix on commit
? Where do you prefer placing config for Babel, PostCSS, ESLint, etc.? In dedica
ted config files

プロジェクトが作成できたら起動してみましょう。

$ cd trello-clone
$ yarn serve

スクリーンショット 2019-11-22 23.01.12.png

画面が表示されました。とても簡単ですね。

ここでVue.jsとTypeScriptの簡単な概要を説明します。

Vue.jsの基本は、JavaScriptのデータとDOMを紐づけるだけのフレームワークです。
JavaScriptのリアクティブなデータが更新されると、それにあわせてDOMが自動的に更新されます。
まずは見た目を変えるためにデータを変えるということだけ覚えておきましょう。

TypeScriptはがあるJavaScriptです。
TypeScriptを導入することで、開発をわかりやすく安全にし、結果的に開発スピードを上げることができます。

さて、本題に戻ります。
初期セットアップとしてHello.vueを削除し、App.vueを以下のようにします。

App.vue
<template>
  <div id="app" />
</template>

<script lang="ts">
import { Component, Vue } from "vue-property-decorator";

@Component
export default class App extends Vue {}
</script>

まっさらになりましたね。それでは実際に作っていきましょう。

型定義とリストレンダリング

ここではタスクを表示するものをカード、カードを束ねるものをリストと呼ぶことにします。
今回のアプリの要件は以下の通りです。

  • リスト、カードの追加機能
  • リスト名、カードのテキストの編集機能
  • リスト、カードの削除機能
  • リスト、カードの移動機能

機能を持つUIの部品のことをコンポーネントといいます。
Vue.jsなどのコンポーネントベースのライブラリでは、このコンポーネントを組み合わせてUIを作っていきます。

コンポーネントの設計では、まずデータの型の考察から始めます。
要件を満たすには、リスト、カードのデータ型はそれぞれ以下のようになります。

  • リスト
    • 自身を特定するためにidを持つ
    • リスト名を持つ
    • カードの配列を持つ
  • カード
    • 自身を特定するためにidを持つ
    • カードのテキストを持つ

srcディレクトリ配下にtypes.tsというファイルを作成し、TypeScriptで型を書いてみましょう。
オブジェクトの型定義には基本interfaceを使います。
コンポーネント名をそれぞれCard, Listとするので、ここではバッティングを防ぐためにオブジェクトの型にプレフィックスとしてinterfaceの頭文字Iを付けることにします。

types.ts
export interface IList {
  /*
  ・idは途中で変えないので、readonly修飾子を付ける(値を変えるとエラーが発生)
  ・数値なのでnumber型
  */
  readonly id: number;
  name: string; // 文字列なのでstring型
  cards: ICard[]; // 配列を定義するには 要素[] とする
}

export interface ICard {
  readonly id: number;
  text: string;
}

モックデータを返すファクトリ関数を別ファイルに定義します。
関数に型をつける際はかっこの後に: 型名と書きます。
これで戻り値の構造が型と異なる場合にコンパイルエラーが発生します。安全ですね。

initialData.ts
import { IList } from "@/types";

export function createInitialLists(): IList[] {
  return [
    {
      id: 1,
      name: "リスト1",
      cards: [
        {
          id: 1,
          text: "タスク1"
        },
        {
          id: 2,
          text: "タスク2"
        }
      ]
    },
    {
      id: 2,
      name: "リスト2",
      cards: [
        {
          id: 3,
          text: "タスク3"
        },
        {
          id: 4,
          text: "タスク4"
        }
      ]
    }
  ];
}

App.vueのような、拡張子が.vueのファイルをSFC(単一ファイルコンポーネント)といい、この中でコンポーネントを定義します。
templateタグ内にHTML、scriptタグ内にJavaScript、styleタグ内にcssを記述します。
scriptタグの属性にlang="ts"とすることでTypeScriptが使えるようになります。

Vueのリアクティブなデータの中で最も基本的なものがdataです。
クラスのプロパティを定義することでdataを登録することができます。
先ほど作成したcreateInitialListsを実行した返り値をlistsに代入しましょう。
dataには型を付けなくてもいいですが、付けることをおすすめします。

App.vue
〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜
  <script lang="ts">
  import { Component, Vue } from "vue-property-decorator";
+ import { IList } from "@/types";
+ import { createInitialLists } from "@/initialData.ts";

  @Component
  export default class App extends Vue {
+   lists: IList[] = createInitialLists();
  }
  </script>

これでlistsというdataにモックデータを設定することができました。

次に、カードのコンポーネント名をCard、リストのコンポーネント名をListとして新しく作成しましょう。
こちらをcomponentsディレクトリ配下に以下の内容であらかじめ作成しておきます。

List.vue
<template>
  <div />
</template>

<script lang="ts">
import { Component, Vue } from "vue-property-decorator";

@Component
export default class List extends Vue {}
</script>
Card.vue
<template>
  <div />
</template>

<script lang="ts">
import { Component, Vue } from "vue-property-decorator";

@Component
export default class Card extends Vue {}
</script>

作成したListコンポーネントをApp.vueで使えるようにしましょう。
@ComponentデコレータのcomponentsオプションにListを登録すると、template内でListというカスタム要素として使用することができます。

リストレンダリング(配列の数だけDOM要素を描画)をするにはv-forを使います。
リストレンダリングしたい要素の属性にv-for="要素 in 配列"を追加します。
こうすることで、配列の要素を属性, テキスト, 子要素のそれぞれで使うことができるようになります。
ここではv-for="list in lists"としましょう。

v-forとセットでkey属性も追加します。
:key="一意な値"とすることでレンダリングを最適化することができます。
ここでは:key="list.id"として、属性値にリストのidを与えましょう。

:属性名(任意)="値"をコンポーネントの属性に追加することで、渡された子コンポーネントではその属性名で値を受け取れるようになります。
属性値でJavaScriptの式を使うにはv-bindディレクティブ(省略記法は:)を使います。
Listコンポーネントでlistのデータを使うために、属性名をわかりやすく値の変数名と同じlistにして、値にlistを代入しましょう。

App.vue
<template>
  <div id="app">
+   <List v-for="list in lists" :key="list.id" :list=list />
  </div>
</template>

<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
+ import List from "@/components/List.vue";
import { IList } from "@/types";
import { createInitialLists } from "@/initialData.ts";

+ @Component({
+   components: {
+    List
+  }
+ })
export default class App extends Vue {
  lists: IList[] = createInitialLists();
}
</script>

List.vueを見ていきましょう。
子コンポーネント内で親から渡されたデータ(これをpropsといいます)を受け取るには、@Propデコレータを使って定義します。

@Propデコレータの引数にはオプションのオブジェクトを渡すことができます。
そのオプションのtypeプロパティではざっくりとした型を付与することができます。
listはオブジェクトなのでObjectを指定しましょう。他にはこんな値があります。

requiredプロパティは、そのpropsが必須かどうかを指定します。
required: trueにしてpropsが渡されなかった場合はエラーが発生します。
できる限り
required: true`にしましょう。

App.vueでlistとして渡したので、受け取る際の変数名はlistにします。
変数名の後には型を付与することができます。

List.vue
<template>
  <div>
+   {{ list.name }} <!-- JSの式を二重中括弧で囲うとテキスト展開される(マスタッシュ構文) -->
  </div>
</template>

<script lang="ts">
+ import { Component, Vue, Prop } from "vue-property-decorator"; // Propを追加
+ import { IList } from "@/types";

@Component
export default class List extends Vue {
+ @Prop({ type: Object, required: true })
+ list!: IList;
}
</script>

リスト名が表示されました。

listの後に!が付いていますね。
これはTypeScriptのNon-null assertion operatorというものです。

まずunion型について説明します。
union型とは複数の型の可能性があるということを表します。
型と型を|で繋ぐように書き、例えば、string | numberは文字列型か数値型の可能性があるということです。

propsは渡されない可能性があるので、undefinedの可能性があります。
これをオプショナルなプロパティといい、undefinedとのunion型と推論されます。
オプショナルなプロパティがundefinedではないことを表現するためにNon-null assertion operatorを使います。

@Propデコレータのオプションで{ required: true }としており、親からデータが渡されないとエラーが発生することが担保されているので!を変数名の後ろに付けます。

同様にList.vue内でもCardをリストレンダリングしましょう。

List.vue
<template>
  <div>
    {{ list.name }}
+   <Card v-for="card in list.cards" :key="card.id" :card="card" />
  </div>
</template>

<script lang="ts">
import { Component, Vue, Prop } from "vue-property-decorator";
+ import Card from "@/components/Card.vue";
import { IList } from "@/types";

+ @Component({
+   components: {
+     Card
+   }
+ })
export default class List extends Vue {
  @Prop({ type: Object, required: true })
  list!: IList;
}
</script>

あわせてCard.vueも変更します。

Card.vue
<template>
+ {{ card.text }}
</template>

<script lang="ts">
import { Component, Vue, Prop } from "vue-property-decorator";
+ import { ICard } from "@/types";

@Component
export default class Card extends Vue {
+ @Prop({ type: Object, required: true })
+ card!: ICard;
}
</script>

リストレンダリングができました。
cssを使ってわかりやすくしましょう。(cssに関しては、各章の終わりの「この時点でのコミット」を参考にしてください。)
この時点でのコミット

リストとカードの追加機能

次はリストの追加機能です。
フォームにリスト名を入力してEnterキーを押したら、新しくリストが追加されるようにしましょう。

ブラウザでは、ユーザーがDOM要素に対してクリックなどのアクションを起こした際などにイベントが発生します。
このイベントが発生した時に関数を呼び出すことができます。呼び出す側のことをイベントリスナ、呼び出される関数のことを特にイベントハンドラといいます(とします)。
これらを実際にDOM要素に登録するには、v-onディレクティブ(省略記法は@)を使って`@イベント名="イベントハンドラ"とします。
これで、登録したDOM要素でそのイベントが発生したときにイベントハンドラが呼び出されます。

なので、イベントハンドラ内でdataを更新する処理を書けば、

  1. ユーザーがアクションを起こすとイベントが発生し、イベントハンドラが呼び出される
  2. イベントハンドラ内でdataを更新する
  3. dataが更新されるとVue.jsがDOMを更新する

という流れを作ることができます。

dataを変更する処理は、基本dataが存在するコンポーネント内で定義します。
listsがあるのはApp.vueなのでApp.vueのクラス内のメソッドに定義します。

テキスト入力の際に発生するイベントにはinputイベントchangeイベントがあります。
inputイベントは入力する度に発生し、changeイベントはinput要素からフォーカスを外した際やEnterキーを押した際などに発生します。
ここではchangeイベントリスナを選択します。
changeイベントリスナにaddListメソッドをイベントハンドラとして紐づけたものを、inputタグに登録しましょう。

App.vue
<template>
  <div id="app">
    <template v-for="list in lists">
      <div class="list-container" :key="list.id">
        <List :list="list" />
      </div>
    </template>
+   <input type="text" @change="addList" />
  </div>
</template>

<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
import List from "@/components/List.vue";
import { IList } from "@/types";
import { createInitialLists } from "@/initialData.ts";

@Component({
  components: {
    List
  }
})
export default class App extends Vue {
  lists: IList[] = createInitialLists();
  // 値を返さない関数の返り値の型としてvoid型を付与
+ addList(): void {}
}
</script>
〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜

ここでリストの型についておさらいしましょう。リストの型は以下の通りです。

types.ts
export interface IList {
  readonly id: number;
  name: string;
  cards: ICard[];
}
〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜

これにより、新しくリストを追加するには、

  • 作成する度に一意のidが付けられ、
  • フォームに入力した値がリスト名になり、
  • 空のカードの配列を持った、

新しいリストをlistsに追加すればいいことになります。

まずidです。idは、リストを作成した数+1とします。
この値は更新されるので、listCreatedCountとしてdataに登録します。
初期値としてcreateInitialLists()の戻り値の配列の要素数である2を代入します。(厳密にはcreateInitialLists().lengthなどにした方がいいかもしれません。)

次にリスト名についてです。
イベントハンドラをDOM要素に登録した際に@change="addList"と、addListに引数を与えていないので、メソッドの第一引数にイベントオブジェクト(発生したイベントの詳細な情報が詰まったオブジェクト)が渡されます。(addList(event): void {})

このchangeイベントのイベントオブジェクトに型を付けましょう。
どのイベントにどの型をつければいいかは、TypeScriptの開発元であるMicrosoftのこちらのサイトに載っています。(直接型定義を見ても構いません。GlobalEventHandlersEventMap型)
見てみると、
スクリーンショット 2019-12-01 20.47.59.png
型名はEventですね。この型はグローバルに登録されている(tsconfig.jsonというTypeScriptの設定ファイルで設定している)ので、importせずにそのまま使うことができます。
eventに型Eventを付与しましょう。(addList(event: Event }): void {})

このイベントオブジェクトの中に、フォームに入力した値が入っています。
イベントオブジェクトのcurrentTargetプロパティが、イベントリスナが実際に登録されたDOM要素で、この中のvalueプロパティが目当てのそれです。
しかし、Event型にはcurrentTargetプロパティがありません。
Eventloadイベントなど、DOM要素以外で発生するイベントの型でも使われているからです。

とりあえず、現状このEvent型にはcurrentTargetプロパティがないので、currentTarget: HTMLInputElement;HTMLInputElementはinput要素の型)というプロパティをEvent型に追加します。(新しく型を作ってもいいと思います。)
あるオブジェクトの型にプロパティを追加するには、そのプロパティを持つオブジェクト型を&を使って繋げます。
このようにしてできた型をintersection型といいます。
(event: Event & { currentTarget: HTMLInputElement; })

それではここまでのコードを見てみましょう。

App.vue
〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜
<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
import List from "@/components/List.vue";
import { IList } from "@/types";
import { createInitialLists } from "@/initialData";

@Component({
  components: {
    List
  }
})
export default class App extends Vue {
  lists: IList[] = createInitialLists();
+ listCreatedCount = 2;

+ addList(event: Event & { currentTarget: HTMLInputElement }): void {
+   const newList = {
+     id: this.listCreatedCount + 1,
+     name: event.currentTarget.value,
+     cards: []
+   };
+   this.lists.push(newList);
    // listsに追加されたため、listCreatedCountをインクリメント
+   ++this.listCreatedCount;
    // フォームの値をリセットするために空文字を代入
+   event.currentTarget.value = "";
+ }
}
</script>
〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜

リストを追加できました。

同様にカードを追加するためのコードを書いていきましょう。
List.vue内にカードを追加するinput要素を追加し、input要素のchangeイベントリスナにaddCardメソッドを登録しましょう。

List.vue
<template>
  <div class="list">
    {{ list.name }}
    <Card v-for="card in list.cards" :key="card.id" class="card" :card="card" />
+   <input type="text" @change="addCard" />
  </div>
</template>

listsApp.vueにあるので、カードを追加する処理はApp.vueに書きます。

App.vue
〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜
<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
import List from "@/components/List.vue";
import { IList } from "@/types";
import { createInitialLists } from "@/initialData";

@Component({
  components: {
    List
  }
})
export default class App extends Vue {
〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜
+ addCard(): void {}
}
</script>
〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜

このままではList.vue内でaddCardを定義していないため、エラーが発生してしまいます。
子コンポーネントで発生したイベントと親コンポーネントで定義したメソッドを紐付けるには、どのようにすればいいのでしょうか。
この場合には以下のようにします。

  1. 親コンポーネント内で、子コンポーネントのカスタム要素の属性に、独自で定義したカスタムイベントのイベントリスナにイベントハンドラを登録する。
  2. 子コンポーネント内のイベントハンドラで、そのカスタムイベントを発生(emit)させる。

以上のようにすることで、子コンポーネントで発生したイベントと親コンポーネントで定義したメソッドを紐付けることができます。

まず1から。
HTMLでは大文字は小文字に変換されてしまうため、カスタムイベントのイベントリスナ名は基本ケバブケースにします。
App.vueでメソッド名をaddCardとしたので、カスタムイベント名をadd-cardとしましょうか。(@add-card="addCard"
これで親コンポーネント側はOKです。

App.vue
<template>
  <div id="app">
    <template v-for="list in lists">
      <div class="list-container" :key="list.id">
+       <List :list="list" @add-card="addCard" />
      </div>
    </template>
    <input type="text" @change="addList" />
  </div>
</template>
〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜

次に2について。
メソッド名をカスタムイベント名のキャメルケースにし、@Emitデコレータを付与することで、そのカスタムイベントをemitすることができます。
カスタムイベント名がadd-cardなのでメソッド名をaddCardとします。

List.vue
<template>
  <div class="list">
    {{ list.name }}
    <Card v-for="card in list.cards" :key="card.id" class="card" :card="card" />
+   <input type="text" @change="addCard" />
  </div>
</template>

<script lang="ts">
+ import { Component, Vue, Prop, Emit } from "vue-property-decorator";
import Card from "@/components/Card.vue";
import { IList } from "@/types";

@Component({
  components: {
    Card
  }
})
export default class List extends Vue {
  @Prop({ type: Object, required: true })
  list!: IList;

+ @Emit()
+ addCard(event: Event & { currentTarget: HTMLInputElement }): void {}
}
</script>
〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜

ここで大事なことがあります。
子コンポーネントでemitするメソッドの返り値が、親コンポーネントで受け取れるイベントオブジェクトになるということです。
これでカードを追加するために必要なデータを送ることができます。

カードを追加するために必要なデータは、追加するリストのidとカードのテキストです。
これをまずIAddCardEventとして型定義しましょう。
型を定義して子コンポーネント側のメソッドの返り値の型親コンポーネント側メソッドの引数の型として付与すれば、齟齬がなく型安全になります。

List.vue
〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜
<script lang="ts">
+ export interface IAddCardEvent {
+   listId: number;
+   text: string;
+ }

+ import { Component, Vue, Prop, Emit } from "vue-property-decorator";
import Card from "@/components/Card.vue";
import { IList } from "@/types";

@Component({
  components: {
    Card
  }
})
export default class List extends Vue {
  @Prop({ type: Object, required: true })
  list!: IList;

  @Emit()
+ addCard(event: Event & { currentTarget: HTMLInputElement }): IAddCardEvent {
    // 次の処理でリセットしてしまうので変数に格納
+   const text = event.currentTarget.value;
    // フォームの値をリセット
+   event.currentTarget.value = "";
    // 返す内容が複数あるのでオブジェクトで返す
+   return {
+     listId: this.list.id,
+     text
+   };
  }
}
</script>
〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜

この型をApp.vue側のメソッドの第一引数に付与しましょう。
これで親コンポーネント側でも、このカスタムイベントのイベントオブジェクトの型がIAddCardEventであることが約束されました。
(実際にはここまでする必要は無いかもしれません。将来、わざわざ型定義しなくても推論されるようになるといいですね。)

また、

App.vue
〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜
<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
import List from "@/components/List.vue";
import { IList } from "@/types";
import { createInitialLists } from "@/initialData";
+ import { IAddCardEvent } from "@/components/List.vue";

@Component({
  components: {
    List
  }
})
export default class App extends Vue {
  lists: IList[] = createInitialLists();
  listCreatedCount = 2;
+ cardCreatedCount = 4;
〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜
+ addCard({ listId, text }: IAddCardEvent): void {
+   const list = this.lists.find(list => list.id === listId);
    /*
    findは見つからなかった場合undefinedを返す可能性があるので、その場合は早期リターンする
    (ここではlist: IList | undefined)
    */
+   if (list === undefined) return;
+   const newCard = {
+     id: this.cardCreatedCount + 1,
+     text
+   };
    // ここではlist: IList
+   list.cards.push(newCard);
+   
+   ++this.cardCreatedCount;
+ }
}
</script>
〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜

これでカードの追加ができるようになりました。

ここでカスタムイベントのおさらいをしておきましょう。

  1. ユーザーがイベントを発火
  2. そのイベントに紐づけられた子コンポーネントのイベントハンドラが呼び出される
  3. その子コンポーネントのイベントハンドラがカスタムイベントをemit(発火)する
  4. そのカスタムイベントに紐づけられた親コンポーネントのイベントハンドラが呼び出される

という処理の順番になります。

この時点でのコミット

こちらに続く

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22