採点されません。匿名です。 間違えることは学習の一部です。読むだけでもかまいません。

ch01 — 静的データで Vue 基礎

テクノロジ系 / L09 app-countries — Vue 国図鑑アプリ

ch01 — 静的データで Vue 基礎

Vue.js で世界の国を探索するアプリを作ろう

作るもの

「世界の国を調べられる国図鑑Webアプリ」

  • 250カ国以上の国旗・人口・地域を一覧表示
  • 国名で検索・地域でフィルタ
  • カードをクリックして詳細を見る
  • お気に入り登録

4日間のロードマップ

Dayテーマゴール
ch01Vue基礎静的データでVue文法をマスター
ch02Router/Pinia/fetch本物のAPIから国データを取得
ch03API設計体験一覧→詳細画面の連携
ch04UI完成検索・お気に入り・認証連携

サンプルコードの使い方

サンプルコード/
├── day1/
│   ├── starter/   ← ここから始める (穴あきコード)
│   └── answer/    ← 完成形 (ヒントに使う)
├── day2/ ...
├── day3/ ...
├── day4/ ...
└── 完成品/        ← ch04 完成形の全コード
# starter を起動する例
cd day1/starter
npm install
npm run dev

ch01

Vue.js の基礎を体で覚える

ch01 で作るもの

静的データ (APIなし) で国カードを表示・絞り込みできるUI

機能Vueの概念
国名・人口を表示{{ }} テンプレート構文
国カードを並べるv-for
地域で絞り込むv-model / computed
クリック反応@click / emit
コンポーネント分割SFC / props

Step 1: {{ }} でデータを表示する

<script setup>
import { ref } from "vue"

const countries = ref([
  { name: "Japan",  region: "Asia",    population: 123210000 },
  { name: "France", region: "Europe",  population: 68070000  },
  { name: "Brazil", region: "Americas",population: 215313000 },
])
</script>

<template>
  <h1>🌍 国図鑑</h1>
  <p>{{ countries[0].name }}</p>
  <p>人口: {{ countries[0].population.toLocaleString() }} 人</p>
</template>

💡 <code>ref()</code> で作った値は <code>.value</code> でアクセス。テンプレート内では自動展開される。

Step 2: v-for でリスト表示

<template>
  <div class="grid">
    <div v-for="country in countries" :key="country.name" class="card">
      <h2>{{ country.name }}</h2>
      <p>地域: {{ country.region }}</p>
      <p>人口: {{ country.population.toLocaleString() }} 人</p>
    </div>
  </div>
</template>

<style scoped>
.grid { display: flex; flex-wrap: wrap; gap: 16px; }
.card { border: 1px solid #ddd; border-radius: 8px; padding: 16px; width: 200px; }
</style>

🔑 <code>:key</code> は一意な値を指定する。重複するとDOMの更新が乱れる。

Step 3: v-if で条件表示

<template>
  <p v-if="selected">選択した国: <strong>{{ selected }}</strong></p>
  <p v-else>国を選んでください</p>
</template>
ディレクティブ用途
v-if条件が true のときだけ表示
v-else-ifelse if
v-elseそれ以外
v-show表示/非表示切り替え (DOM自体は残る)

Step 4: @click + ref でボタン反応

<script setup>
import { ref } from "vue"

const selected = ref(null)

function onSelect(name) {
  selected.value = name
}
</script>

<template>
  <p v-if="selected">選択: {{ selected }}</p>
  <div v-for="country in countries" :key="country.name"
       class="card" @click="onSelect(country.name)">
    {{ country.name }}
  </div>
</template>

Step 5: computed で地域フィルタ

<script setup>
import { ref, computed } from "vue"

const selectedRegion = ref("すべて")
const regions = ["すべて", "Asia", "Europe", "Americas", "Africa"]

const filtered = computed(() => {
  if (selectedRegion.value === "すべて") return countries.value
  return countries.value.filter(c => c.region === selectedRegion.value)
})
</script>

<template>
  <select v-model="selectedRegion">
    <option v-for="r in regions" :key="r">{{ r }}</option>
  </select>
  <div v-for="country in filtered" :key="country.name">...</div>
</template>

💡 <code>computed</code> は依存する <code>ref</code> が変わると自動で再計算される。

Step 6: コンポーネントを作る

CountryCard.vue を作って国カードを分離する。

<!-- CountryCard.vue -->
<script setup>
const props = defineProps({
  country: { type: Object, required: true },
})
const emit = defineEmits(["select"])

function handleClick() {
  emit("select", props.country.name)
}
</script>

<template>
  <div class="card" @click="handleClick">
    <img :src="country.flag" :alt="country.name" />
    <h2>{{ country.name }}</h2>
    <p>{{ country.region }}</p>
  </div>
</template>

Step 7: 親コンポーネントで使う

<!-- App.vue -->
<script setup>
import CountryCard from "./components/CountryCard.vue"

function onSelect(name) { selected.value = name }
</script>

<template>
  <div class="grid">
    <CountryCard
      v-for="country in filtered"
      :key="country.name"
      :country="country"
      @select="onSelect"
    />
  </div>
</template>

props: 親 → 子へデータを渡す (:country="country")

emit: 子 → 親へイベントを上げる (@select="onSelect")

ch01 まとめ

概念構文
データ宣言const x = ref(初期値)
テンプレート表示{{ x }}
リストv-for="item in list" :key="..."
条件v-if / v-else
クリック@click="handler"
双方向バインドv-model="x"
算出プロパティconst y = computed(() => ...)
コンポーネントdefineProps / defineEmits

考えてみよう

既定では正解・不正解の判定はしません。自分のペースで「答えを見る」を。採点してほしい場合だけ「採点してほしい」を押してください。

ヒント

  • v-forv-for="要素 in 配列" の形でリストの各要素を取り出す。
  • :key には各要素を一意に識別できる値を渡す(ここでは country.name)。
  • テンプレート内では {{ country.name }} のように {{ }} で値を表示する。
  • 数値を 3 桁区切りにするには .toLocaleString() を使う。

解説

v-for は配列を繰り返し描画する Vue のディレクティブ。

<li v-for="country in countries" :key="country.name">
  {{ country.name }}
</li>
  • country in countries で配列 countries の各要素を country として取り出す。
  • :key は Vue が「どの要素がどれか」を追跡するための目印。これがないと、要素の並び替え・追加・削除で表示が崩れることがある。配列のインデックスではなく、要素ごとに一意で変わらない値(ここでは国名)を使う。
  • リストの各行で {{ }} を使い、その要素のプロパティを表示できる。

国図鑑アプリでは、API から取得した数百件の国データをこの v-for で一覧表示する。ch01 では API の代わりに静的な配列で v-for の形を体に入れる。

ヒント

  • computed は「他のリアクティブな値から計算で導く値」。元の値が変わると自動で再計算される。
  • 「未選択なら全件」は、空文字が falsy であることを使って早期 return できる(if (!selectedRegion.value) ...)。
  • それ以外は、配列の filter で「region が選択値と一致する国だけ」を残す。条件式は自分で書く。
  • computed の中では ref の値に .value でアクセスする。

解説

computed は「元データから派生する値」を宣言的に書くための仕組み。

const filteredCountries = computed(() => {
  if (!selectedRegion.value) return countries.value
  return countries.value.filter((c) => c.region === selectedRegion.value)
})
  • selectedRegionv-model<select> と双方向バインド)が変わると、filteredCountries自動で再計算される。テンプレートはこの computed を v-for するだけでよい。
  • 「未選択なら全件」を if (!selectedRegion.value) return countries.value で表現する。空文字 '' は falsy なのでこの分岐に入る。
  • フィルタ自体は普通の JavaScript の filter。Vue 固有なのは「依存する値が変わったら勝手に再計算される」点。

methods で関数を呼ぶ方法もあるが、computed は結果をキャッシュし、依存が変わったときだけ再計算するので一覧フィルタのような用途で効率的。国図鑑では地域フィルタ+検索語の組み合わせを computed で表現していく。

ヒント

  • defineProps({ country: { type: Object, required: true } }) で親から country を受け取る。戻り値を props に代入するとスクリプト内で props.country として使える。
  • defineEmits(['select']) で発火できるイベント名を宣言する。戻り値を emit に代入する。
  • クリックで親に通知するには emit('select', props.country) を呼ぶ。
  • テンプレートでは {{ props.country.name }} のようにプロパティを表示する。

解説

コンポーネントを分けると「データを渡す(props)」「イベントを返す(emit)」という親子の役割分担が生まれる。

<script setup>
const props = defineProps({
  country: { type: Object, required: true },
})
const emit = defineEmits(['select'])
</script>

<template>
  <button @click="emit('select', props.country)">
    {{ props.country.name }}
  </button>
</template>
  • props(下り): 親 → 子へデータを渡す。子は props を読み取り専用として使う(子で書き換えない)。type / required を宣言すると、渡し忘れや型違いを開発時に検知できる。
  • emit(上り): 子 → 親へ「何か起きた」を通知する。子は値を直接親に書き込まず、イベントを発火するだけ。どう反応するかは親が決める。

この「props は下り、emit は上り」という一方向の流れが Vue(と多くの UI フレームワーク)の基本。カードを独立コンポーネントにしておくと、一覧画面でも詳細画面でも同じ CountryCard を再利用できる。

親側はこう使う(参考):

<CountryCard
  v-for="c in countries" :key="c.name"
  :country="c"
  @select="selected = $event"
/>