Chapter 07

リストレンダリング

OJK
OJK
2021.11.15に更新

今回は HTML に繰り返し構文を持ち込む方法について学びます。ウェブアプリケーションではデータをリスト表示することが多いのでこの機能は必須です。

雛形コード
index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>petite-vue入門</title>
</head>
<body>

  <p>Petite Vue!!</p>

  <script src="https://unpkg.com/petite-vue"></script>
  <script src="script.js"></script>
</body>
</html>
script.js
'use strict';

PetiteVue.createApp({

}).mount();

リストレンダリング

まずは素の JavaScript を使って配列の内容をリスト表示してみましょう。

HTML
<ul></ul>
JS
const animals = ['dog', 'cat', 'pig'];

const ul = document.querySelector('ul');
for (const anml of animals) {
  const li = document.createElement('li');
  li.textContent = anml;
  ul.appendChild(li);
}

配列を走査するループの中で次の 3 つの処理を行っています。

  1. HTML 要素の作成
  2. その要素への値を設定
  3. その要素を DOM ツリーに追加

petite-vue の v-for ディレクティブを使うと、この処理が次のように記述できます。

HTML
<ul>
  <li v-for="anml in animals">{{anml}}</li>
</ul>
JS
PetiteVue.createApp({
  animals: ['dog', 'cat', 'pig'],
}).mount();

詳細は後ほど述べますが、繰り返しの構文が JavaScript ではなく HTML 側に移動しているのがわかります。JavaScript 側に li 要素を生成する記述はなく、HTML 側に li 要素が書かれていて、その li 要素の中に v-for というディレクティブが見えます。この petite-vue の書き方のほうが直感的でわかりやすいという人も多いかと思います。

v-for ディレクティブ

さて、すでにサンプルコードに登場した v-for ディレクティブ の構文を示しておきます。

v-for ディレクティブ
<HTML要素名 v-for="ループ変数 in 配列/オブジェクト">

繰り返し描画したい HTML 要素に v-for を追加します。v-for に指定する値はオブジェクトの for-in 構文によく似ていますが、ループ変数の前に let や const は不要です。for-in ですが、配列にもオブジェクトにも使えます(素の JavaScript の場合、配列の走査は for-of です)。

「ループ変数」というのは本講座シリーズの造語です。『文系 大学生のための JavaScript 入門』では count で宣言しているので「ループ定数」としていました。Vue のドキュメントでは「エイリアス/alias」と書かれていましたが、この表現も一般的ではないようです。どなたか正式名称をご存知でしたら教えてください。

in の後にあるデータプロパティが配列かオブジェクトかでループ変数に代入される値が異なります。また、in の前に ( ) で囲んで複数の変数名を列挙することで様々な値が取得できます。

配列の場合
<HTML要素名 v-for="配列の要素 in 配列">
<HTML要素名 v-for="(配列の要素, インデックス) in 配列">
オブジェクトの場合
<HTML要素名 v-for="プロパティ値 in オブジェクト">
<HTML要素名 v-for="(プロパティ値, プロパティ名) in オブジェクト">
<HTML要素名 v-for="(プロパティ値, プロパティ名, インデックス) in オブジェクト">

以下、具体例を列挙しておきます。

なお、コード短縮のため、配列やオブジェクトはデータプロパティとして定義せず、ディレクティブの中で直接指定しています。ただし、createApp メソッドは呼び出さなければ petite-vue が実行されないので注意です。

JS
PetiteVue.createApp({}).mount();

前述の v-for 構文に挙げた順に例を挙げていきます。ul 要素は省略しています。

配列(要素のみ)
<li v-for="item in ['A', 'B', 'C']">{{item}}</li>

配列(要素+インデックス)
<li v-for="(item, i) in ['A', 'B', 'C']">
  {{i}}. {{item}}
</li>

オブジェクト(プロパティ値のみ)
<li v-for="val in { name: 'OJK', job: '先生'}">
  {{val}}
</li>

オブジェクト(プロパティ値+プロパティ名)
<li v-for="(val, prop) in { name: 'OJK', job: '先生'}">
  {{prop}}: {{val}}
</li>

オブジェクト(プロパティ値+プロパティ名+インデックス)
<li v-for="(val, prop, i) in { name: 'OJK', job: '先生'}">
  {{i}}. {{prop}}: {{val}}
</li>

in の後には整数値を取ることもできます。その場合は、1 からその値までの整数をループ変数に渡します。

v-for in 整数値
<li v-for="n in 10">{{n}}</li>

v-for を付けた HTML 要素の子孫要素もまとめてリスト表示されます。複数の兄弟要素をまとめてリスト表示したいときに適当な親要素がなければ、v-if と同様に template 要素が使えます。

div要素で囲う
<div v-for="item in ['A', 'B', 'C']">
  <span>{{item}}</span>
  <button @click="console.log(item)">表示</button>
</div>
template要素で囲う
<template v-for="item in ['A', 'B', 'C']">
  <span>{{item}}</span>
  <button @click="console.log(item)">表示</button>
</template>

template 要素の復習も兼ねて、上記の 2 つの例を実行して Chrome の Elements タブ(Firefox ならインスペクタータブ)で実行されている HTML コードを確認してみてください。

オブジェクトの配列のリスト表示

v-for 自体の解説は以上で終わりなのですが、典型的なデータ構造である「オブジェクトの配列」の例を見ておきましょう。

データは以下のものにします(以降のサンプルコードでは略します)。某ポケモンのデータなので怒られたら変更します。

JS
PetiteVue.createApp({
  list: [
    { name: 'ガーディ', type: 'ほのお', weight: 19.0, ability: 'もらいび' },
    { name: 'コイキング', type: 'みず', weight: 10.0, ability: 'すいすい' },
    { name: 'コダック', type: 'みず', weight: 19.6, ability: 'しめつけ' },
    { name: 'サンダース', type: 'でんき', weight: 24.5, ability: 'ちくでん' },
    { name: 'ピカチュウ', type: 'でんき', weight: 6.0, ability: 'せいでんき' },
    { name: 'ヒトカゲ', type: 'ほのお', weight: 8.5, ability: 'もうか' },
    { name: 'ブースター', type: 'ほのお', weight: 25.0, ability: 'もらいび' },
    { name: 'ミュウ', type: 'エスパー', weight: 4.0, ability: 'シンクロ' }
  ]
}).mount();

まず、これらのデータを下図のように table 要素に配置してみましょう。

先頭行は th 要素で項目を付けておき、2 行目の tr 要素からリスト表示にします。CSS は省略します。

HTML
<table>
  <tr>
    <th>名前</th><th>タイプ</th><th>重さ</th><th>特性</th>
  </tr>
  <tr v-for="pkmn in list">
    <td>{{pkmn.name}}</td>
    <td>{{pkmn.type}}</td>
    <td style="text-align: right">{{pkmn.weight}}kg</td>
    <td>{{pkmn.ability}}</td>
  </tr>
</table>

ループ変数 pkmn が個々のデータのオブジェクトになるので、そこからプロパティにアクセスします。「重さ」の項目にだけインラインスタイルを付けていますが、これも HTML /CSS の知識だけで可能ですね(JavaScript からやろうとすると結構面倒)。

次にデータの並び替えをやってみましょう。petite-vue 自体には並び替えの機能はないので、JavaScript の関数(メソッド)で行うことになります。sort メソッドはそれを呼び出した配列の並びを変更するのですが、ゲッターを使ってプロパティ名を変えたほうが HTML を読むときにはわかりやすいでしょう。

ポケモンの体重が軽い順に並べ替えます。
sort メソッドについては こちら などを参考にして読み解いてください。

JS
PetiteVue.createApp({
  list: [/* 略 */],
  get sortedList() {
    return this.list.sort((a, b) => a.weight - b.weight);
  },
}).mount();

HTML 側のデータプロパティを sortedList に変更します。

HTML
<tr v-for="pkmn in sortedList">

「体重」の項目を特別扱いしないのであれば、v-for の二重ループにしてコードを短くすることも可能です。ループ変数 pkmn はオブジェクトなので、v-for でプロパティ値を順に取り出せます。

v-forの二重ループ
<tr v-for="pkmn in list">
  <td v-for="val in pkmn">{{val}}</td>
</tr>

条件付きリストレンダリング

特定の条件を満たしたデータだけリスト表示したい…ということがあるかと思います。例えば、v-if と組み合わせると次のように書けそうです。

v-if + v-for(ダメな例)
<tr v-for="pkmn in list" v-if="pkmn.type == 'ほのお'">

ところがこれは動作しません。
同じ HTML 要素に v-for と v-if が同時に付けられた場合、v-if のほうが先に処理されるのでループ変数「pkmn」がまだ存在しないのです。

petite-vue の場合、ゲッターを使って「特定の条件を満たしたデータ」を用意するのがよいでしょう。配列の filter メソッドを使えば 1 行で書けます。

ゲッターを使った条件付きリストレンダリング
get selectedList() {
  return this.list.filter((el) => el.type == 'ほのお');
}

HTML 側のデータプロパティをゲッターに変更します。

HTML
<tr v-for="pkmn in selectedList">

ちなみに、配列の filter メソッドを使わずに同じ処理を記述すると以下のようにちょっと長くなります。配列のラムダ式タイプのメソッドはぜひ習得しておきましょう。

filterメソッドを使わないゲッター
get selectedList() {
  const newList = [];
  for (const el of this.list) {
    if (el.type == 'ほのお') {
      newList.push(el);
    }
  }
  return newList;
}
Vue を使う

本家 Vue の場合、template 要素で v-for を回し、tr 要素で v-if するという入れ子構成にもできます。残念ながら、これは petite-vue では動作しません。

template要素でv-forする方法
<template v-for="pkmn in list">
  <tr v-if="pkmn.type == 'ほのお'">
    <!-- 略 -->
  </tr>
</template>