Skip to content

アプリを作ってみよう

商品リストを作ってみる

みなさんにはこの節の最後に Todo リストを作ってもらうのですが、商品リストをテーマに、Todo リストに必要な Vue の機能をピックアップしていきます。

こんな感じのを作っていきます。

必要な要素を考える

上の gif のようなアプリを実現するためには何が必要か考えてみましょう。

  • 商品リストのコンポーネントを作る
  • 商品のリストデータを保存する
  • 商品のリストデータを表示する
  • 商品を追加できる
  • 商品を削除できる
  • 同じ名前の商品は登録できない
  • 商品の値段が 500 円以上だったら赤くする
  • 商品の値段が 10000 円以上だったら「高額商品」と表示する

こんな感じでしょうか。
それでは上から順番に実装していきましょう。

商品リストのコンポーネントを作る

componentsディレクトリにItemList.vueというファイルを作成します。

src/components/ItemList.vue

中身はコンポーネントに最低限必要な部分だけ書きます。

vue
<!-- src/components/ItemList.vue -->
<script setup lang="ts"></script>

<template>
  <div>ItemList</div>
</template>

<style></style>

src/App.vue

vue
<!-- src/App.vue -->
<script setup lang="ts">
import ClickCounter from './components/ClickCounter.vue'
import ItemList from './components/ItemList.vue'
import WelcomeMessage from './components/WelcomeMessage.vue'
</script>

<template>
  <main>
    <WelcomeMessage msg="Webエンジニアになろう講習会へようこそ" />
    <ClickCounter />
    <ItemList />
  </main>
</template>

<style scoped>
main {
  padding: 2rem;
}
</style>

表示されました。 こうすることで、後はItemList.vueの中身を書き変えればよくなります。

商品のリストデータを保存する

商品リストのデータを保存するのに適当な変数の型は何でしょうか?
商品「リスト」なので配列がよさそうです。
というわけで、配列を使ってデータを保持することにします。
今は商品の追加ができないので、とりあえずダミーデータを入れておきます。

参考: Array | MDN
参考:JavaScript オブジェクトの基本 - ウェブ開発を学ぶ | MDN

vue
<!-- src/components/ItemList.vue -->
<script setup lang="ts">
import { ref } from 'vue'

interface Item { 
  name: string
  price: number
} 

const items = ref<Item[]>([ 
  { name: 'たまご', price: 100 }, 
  { name: 'りんご', price: 160 } 
]) 
</script>

<template>
  <div>ItemList</div>
</template>

<style></style>

4~7 行目は TypeScript の記法で、Itemという型をinterfaceを用いて定義しています。
そして ref のジェネリクスにItem[]を渡すことで、items変数をItem型の配列のrefとして扱えるようにしています。

参考:ジェネリクス (generics) | TypeScript 入門『サバイバル TypeScript』
参考:インターフェース (interface) | TypeScript 入門『サバイバル TypeScript』

商品のリストデータを表示する

先ほど定義したリストの情報を表示していきます。
Vue ではリストデータをtemplateタグ内で for 文のように書くv-forという構文があります。
v-forを使うときには:keyを設定しなければいけません(理由(やや難): 優先度 A: 必須 | Vue)。

参考: リストレンダリング | Vue

これを使ってデータを表示してみます。

vue
<!-- src/components/ItemList.vue -->
<script setup lang="ts">
import { ref } from 'vue'

interface Item {
  name: string
  price: number
}

const items = ref<Item[]>([
  { name: 'たまご', price: 100 },
  { name: 'りんご', price: 160 }
])
</script>

<template>
  <div>
    <div>ItemList</div>
    <ul>
      <li v-for="item in items" :key="item.name">
        <div>名前: {{ item.name }}</div>
        <div>{{ item.price }} 円</div>
      </li>
    </ul>
  </div>
</template>

<style></style>

表示できました。

商品を追加する

Vue では入力欄に入力された文字列とコンポーネントの変数を結びつけることができます。
参考: フォーム入力バインディング | Vue

これを使って商品を追加できるようにしてみます。

vue
<!-- src/components/ItemList.vue -->
<script setup lang="ts">
import { ref } from 'vue'

interface Item {
  name: string
  price: number
}

const items = ref<Item[]>([
  { name: 'たまご', price: 100 },
  { name: 'りんご', price: 160 }
])
const newItemName = ref('') 
const newItemPrice = ref(0) 

const addItem = () => { 
  items.value.push({ name: newItemName.value, price: newItemPrice.value }) 
} 
</script>

<template>
  <div>
    <div>ItemList</div>
    <ul>
      <li v-for="item in items" :key="item.name">
        <div>名前: {{ item.name }}</div>
        <div>{{ item.price }} 円</div>
      </li>
    </ul>
    <div>
      <label>
        名前
        <input v-model="newItemName" type="text" />
      </label>
      <label>
        価格
        <input v-model="newItemPrice" type="number" />
      </label>
      <button @click="addItem">追加</button>
    </div>
  </div>
</template>

<style></style>

参考: アロー関数式 | MDN

できました!

練習問題 1:商品リストに機能を追加

このままだとボタンを連打して商品の追加ができてしまいます。

  • ボタンを押したら入力欄を空にする機能
  • 入力欄が空だったらボタンを押しても追加されないようにする機能

を追加してみましょう。

追加した商品を削除する

各商品に削除ボタンを追加します。deleteItem関数では、filterを使って指定した名前以外の商品だけを残すことで削除を実現しています。

参考: Array.prototype.filter() | MDN

vue
<!-- src/components/ItemList.vue -->
<script setup lang="ts">
import { ref } from 'vue'

interface Item {
  name: string
  price: number
}

const items = ref<Item[]>([
  { name: 'たまご', price: 100 },
  { name: 'りんご', price: 160 },
])
const newItemName = ref('')
const newItemPrice = ref(0)

const addItem = () => {
  items.value.push({ name: newItemName.value, price: newItemPrice.value })
}

const deleteItem = (name: string) => { 
  items.value = items.value.filter((item) => item.name !== name) 
} 
</script>

<template>
  <div>
    <div>ItemList</div>
    <ul>
      <li v-for="item in items" :key="item.name">
        <div>名前: {{ item.name }}</div>
        <div>{{ item.price }} 円</div>
        <button @click="deleteItem(item.name)">削除</button>
      </li>
    </ul>
    <div>
      <label>
        名前
        <input v-model="newItemName" type="text" />
      </label>
      <label>
        価格
        <input v-model="newItemPrice" type="number" />
      </label>
      <button @click="addItem">追加</button>
    </div>
  </div>
</template>

<style></style>

同じ名前が複数あるとどうなるか

Vue はv-forでリストを描画するとき、:keyの値を使って各要素を識別します。
:keyに重複した値が渡されると Vue はそれぞれの要素を区別できなくなるため、意図しない動作を引き起こす可能性があります。

:key="item.name"としているため、同じ名前の商品が 2 つ存在すると:keyが重複してしまいます。また、deleteItemも名前で検索して見つかった項目を削除しているため、削除ボタンを押すと同名の商品が全部消えてしまいます。

試しに同じ名前の商品を 2 つ追加して削除してみましょう。

同じ名前の商品を登録できないようにする

Array.prototype.someを使って、追加しようとしている商品名がすでにリストに存在するか確認します。存在する場合はreturnで追加処理を中断します。

参考: Array.prototype.some() | MDN

vue
<!-- src/components/ItemList.vue -->
<script setup lang="ts">
import { ref } from 'vue'

interface Item {
  name: string
  price: number
}

const items = ref<Item[]>([
  { name: 'たまご', price: 100 },
  { name: 'りんご', price: 160 },
])
const newItemName = ref('')
const newItemPrice = ref(0)

const addItem = () => {
  if (items.value.some((item) => item.name === newItemName.value)) return
  items.value.push({ name: newItemName.value, price: newItemPrice.value })
}

const deleteItem = (name: string) => {
  items.value = items.value.filter((item) => item.name !== name)
}
</script>

<template>
  <div>
    <div>ItemList</div>
    <ul>
      <li v-for="item in items" :key="item.name">
        <div>名前: {{ item.name }}</div>
        <div>{{ item.price }} 円</div>
        <button @click="deleteItem(item.name)">削除</button>
      </li>
    </ul>
    <div>
      <label>
        名前
        <input v-model="newItemName" type="text" />
      </label>
      <label>
        価格
        <input v-model="newItemPrice" type="number" />
      </label>
      <button @click="addItem">追加</button>
    </div>
  </div>
</template>

<style></style>

TIP

他の方法として、アイテムが追加されたときに自動で一意な ID を振る方法もあります。興味がある人は実装してみてください。

商品の値段が 500 円以上だったら赤くする

Vue では、ある特定の条件が満たされたときに class を追加するという機構を持たせることができます。
これを使って、条件が満たされたときだけ CSS を当てるといったことができます。

参考: CSS の基本 | MDN
参考: クラスとスタイルのバインディング | Vue

vue
<!-- src/components/ItemList.vue -->
<script setup lang="ts">
import { ref } from 'vue'

interface Item {
  name: string
  price: number
}

const items = ref<Item[]>([
  { name: 'たまご', price: 100 },
  { name: 'りんご', price: 160 },
])
const newItemName = ref('')
const newItemPrice = ref(0)

const addItem = () => {
  if (items.value.some((item) => item.name === newItemName.value)) return
  items.value.push({ name: newItemName.value, price: newItemPrice.value })
}

const deleteItem = (name: string) => {
  items.value = items.value.filter((item) => item.name !== name)
}
</script>

<template>
  <div>
    <div>ItemList</div>
    <ul>
      <li v-for="item in items" :key="item.name" :class="{ over500: item.price >= 500 }">
        <div>名前: {{ item.name }}</div>
        <div>{{ item.price }} 円</div>
        <button @click="deleteItem(item.name)">削除</button>
      </li>
    </ul>
    <div>
      <label>
        名前
        <input v-model="newItemName" type="text" />
      </label>
      <label>
        価格
        <input v-model="newItemPrice" type="number" />
      </label>
      <button @click="addItem">追加</button>
    </div>
  </div>
</template>

<style>
.over500 { 
  color: red; 
} 
</style>

商品の値段が 10000 円以上だったら「高額商品」と表示する

Vue では、ある特定の条件を満たした場合のみ、対象コンポーネントを表示するという機能をv-ifという構文を使って実現できます。

参考: 条件付きレンダリング | Vue

これを使って商品の値段が 10000 円以上だったら「高額商品」と表示するという機能を実現してみましょう。

vue
<!-- src/components/ItemList.vue -->
<script setup lang="ts">
import { ref } from 'vue'

interface Item {
  name: string
  price: number
}

const items = ref<Item[]>([
  { name: 'たまご', price: 100 },
  { name: 'りんご', price: 160 },
])
const newItemName = ref('')
const newItemPrice = ref(0)

const addItem = () => {
  if (items.value.some((item) => item.name === newItemName.value)) return
  items.value.push({ name: newItemName.value, price: newItemPrice.value })
}

const deleteItem = (name: string) => {
  items.value = items.value.filter((item) => item.name !== name)
}
</script>

<template>
  <div>
    <div>ItemList</div>
    <ul>
      <li v-for="item in items" :key="item.name" :class="{ over500: item.price >= 500 }">
        <div>名前: {{ item.name }}</div>
        <div>{{ item.price }} 円</div>
        <div v-if="item.price >= 10000">高額商品</div>
        <button @click="deleteItem(item.name)">削除</button>
      </li>
    </ul>
    <div>
      <label>
        名前
        <input v-model="newItemName" type="text" />
      </label>
      <label>
        価格
        <input v-model="newItemPrice" type="number" />
      </label>
      <button @click="addItem">追加</button>
    </div>
  </div>
</template>

<style>
.over500 {
  color: red;
}
</style>

これで商品リストが完成しました!

今回の商品リストの全体像は以下のブランチに入っているので、参考にしてみてください。
traPtitech/naro-template-frontend at itemlist-sample

Todo リストを作る

ここまで紹介してきた機能を使うことで Todo リストが作れるはずです。 頑張りましょう!

練習問題 2:Todo リストを作る

Todo リストを作りましょう。

必要な機能は以下の通りです。

  • タスクは未完または完了済みの状態を持つ。
  • タスクはタスク名を持つ。
  • 未完タスクのリストと完了済みタスクのリストが表示される。
  • タスクを完了させることができる。
  • タスクの追加ができる。

以上の機能が実現されていれば後は自由です。 スタイルが気になる人は CSS なども書きましょう。

分からないことがあれば遠慮なく TA に質問してください。