ch01 — 静的データで Vue 基礎
テクノロジ系 / L09 app-countries — Vue 国図鑑アプリ
ch01 — 静的データで Vue 基礎
Vue.js で世界の国を探索するアプリを作ろう
作るもの
「世界の国を調べられる国図鑑Webアプリ」
- 250カ国以上の国旗・人口・地域を一覧表示
- 国名で検索・地域でフィルタ
- カードをクリックして詳細を見る
- お気に入り登録
4日間のロードマップ
| Day | テーマ | ゴール |
|---|---|---|
| ch01 | Vue基礎 | 静的データでVue文法をマスター |
| ch02 | Router/Pinia/fetch | 本物のAPIから国データを取得 |
| ch03 | API設計体験 | 一覧→詳細画面の連携 |
| ch04 | UI完成 | 検索・お気に入り・認証連携 |
サンプルコードの使い方
サンプルコード/ ├── 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-if | else 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-forはv-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)
})selectedRegion(v-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" />