基礎からメモ: Vue.js CH9-p295 URLパラメータ付きの動的ページ

簡単な商品一覧ページのサンプルを作る。 src/views/Home.vuesrc/main.jssrc/App.vueは前回と同じ。

まず商品一覧ページと商品詳細ページのコンポーネントを作る。

商品一覧ページ

[src/views/ProductList.vue]

<template>
  <div class="product-list">
    <h1>商品一覧</h1>
  </div>
</template>

商品詳細ページ

[src/views/Product.vue]

<template>
  <div class="product">
    <h1>商品情報</h1>
 <p>このページは ID:{{ $route.params.id}} の詳細を表示する</p>
  </div>
</template>

$routeは、マッチしたルートの情報が入ったオブジェクト。router.jsで定義した各ルートのURLを構成するパラメータなどの情報。 localhost:8080/#/productにアクセスすると商品情報のページが、localhost:8080/#/product/1などにアクセスすると「このページは ID:1 の詳細を表示する」という表示をする。

[src/router.js]

import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '@/views/Home.vue'
import ProductList from '@/views/ProductList.vue'
import Product from '@/views/Product.vue'
Vue.use(VueRouter)

const router = new VueRouter({
  routes: [
    { path: '/', component: Home },
    { path: '/product', component: ProductList },
    { path: '/product/:id', component: Product }
  ]
})

export default router

propsでパラメータをコンポーネントに渡す

コンポーネント$routeを直接書くと、ルートとコンポーネントが密結合になるので避けたい。そこでパラメータはpropsとして渡す。

[src/router.js]

.........................
const router = new VueRouter({
  routes: [
    { path: '/', component: Home },
    { path: '/product', component: ProductList },
    { 
      path: '/product/:id',
      component: Product,
      // 関数でpropsを定義、第一引数に現在のルートオブジェクトが渡る
      props: route => ({
        id: Number(route.params.id)
      })
    }
  ]
})
......................

Product.vueを変更して、パラメータをprops経由で渡す仕組みにする。

[src/views/Product.vue]

<template>
  <div class="product">
    <h1>商品情報</h1>
    <p>このページは ID:{{ id }} の詳細を表示する</p>
  </div>
</template>

<script>
export default {
  props: {
    id: Number
  }
}
</script>

この変更でも「このページは ID:1 の詳細を表示する」という形で、商品情報のページは同様に表示される。

コンテンツ表示

商品IDを表示するだけでは味気ないので、商品の詳細を書ける配列リストを作り、各商品情報にアクセスすると詳細を表示できる形にする。

商品情報のリストを定義した products.js を作り、ProductList.vueで読み込む。

[src/api/products.js]

// 商品リスト
const database = [
  { id: 1, name: '商品A', price: 100, content: '商品A詳細' },
  { id: 2, name: '商品B', price: 200, content: '商品B詳細' },
  { id: 3, name: '商品C', price: 300, content: '商品C詳細' }
]

// インポート先で使用できる関数をオブジェクトとして定義
export default {
  fetch(id) { return database },
  find(id)  { return database.find(el => el.id === id) },
  asyncFind(id, callback) {
    setTimeout(() => {
      callback(database.find(el => el.id === id))
    }, 1000)
  }
}

商品リスト一覧の表示

ProductList.vue は商品リストから一覧を表示して、各商品詳細へのリンクから Product.vue に遷移できる仕組みにする。

[src/views/ProductList.vue]

<template>
  <div class="product-list">
    <h1>商品一覧</h1>
    <ul>
      <li v-for="{ id, name } in list" :key="id">
        <router-link :to="`/product/${id}`">{{ name }}</router-link>
      </li>
    </ul>
  </div>
</template>

<script>
import products from '@/api/products.js'
export default {
  computed: {
    list: () => products.fetch()
  }
}
</script>

各商品情報の表示

Product.vue では、propsidを監視し、変更があれば非同期通信で商品詳細を取得し表示する。

[src/views/Product.vue]

<template>
  <div class="product" v-if="item" key="product">
    <h1>商品情報</h1>
    <dl class="product-table">
      <dt>商品名</dt><dd>{{ item.name }}</dd>
      <dt>価格</dt><dd>{{ item.price }}</dd>
      <dt>商品説明</dt><dd>{{ item.content }}</dd>
    </dl>
  </div>
  <div v-else key="loading">商品情報を読み込んでいます....</div>
</template>

<script>
import products from '@/api/products.js'
export default {
  props: { id: Number },
  data() {
    return { item: null }
  },
  watch: {
    id: {
      handler() {
        products.asyncFind(this.id, item => {
          this.item = item
        })
      }, immediate: true // 初期読み込み時にも呼び出す
    }
  }
}
</script>

<style>
.product-table, .product-table * {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
.product-table {
  display: flex;
  flex-wrap: wrap;
  margin: 20px;
  width: 230px;
  background-color: #FFF;
  border: 1px solid #ddd;
}
.product-table dt {
  width: 40%;
  padding: 8px 10px;
  background-color: #f2f2f2;
  border-bottom: 1px solid #ddd;
  font-weight: bold;
}
.product-table dd {
  width: 60%;
  padding: 8px 0 8px 22px;
  border-bottom: 1px solid #E4E4E4;
}
</style>

商品一覧のリストからいずれかの商品情報ページへのリンクをクリックすると、監視されたIDに変更が生じる。その際products.jsに定義されたasyncFind関数を使って、非同期で商品情報を得る。asyncFind関数は1000ミリ秒ごとに該当する商品情報を渡しコールバックを実行する。コールバック関数は渡された商品情報を自身のitemデータに格納し、商品情報用のテンプレートが描画される。

URLのパラメータとクエリの変更はコンポーネントのライフサイクル初期化を行わないので、watchによるデータの監視という方法をとる必要がある。