連動するプルダウンリストをVue.jsを使って実装する ( with PHP )

みなさんこんにちは

プルダウンリストAを選択すると、プルダウンリストBが自動的に切り替わる...
例えば、部署を選んだら、その下の課が選択できるようになるみたいな、そんな連動するプルダウンリストを作りたくなったのです。
初めは、まあ、jQueryでいいかって思ったのですけど、コードがモコモコと増えていきまして、すごい汚い状態になったので、却下して、ふんわりとVue.jsで実装しようとしたのだけど、ググってもあまり出てこなかったので、とりあえずリファレンス見ながら実装してたら、普通に書けちゃったので、忘れないうちに記事にしちゃおうと。

あ、SPAの話じゃないっすよ、普通にMPAでの話っす。
あと、タグのPHPに不穏な空気を感じたりしないこと。

# TL;DR

- MPAなので、データはグローバルにjsonでベタがき
- Vue.jsのデータバインディングの仕組みを使って、連動先のリストを切り替える
- 複数の連動プルダウンリストもちょっと書き換えるだけで簡単に扱える
- コンポーネントは使ってない

# 目標

以下のようなデータ構造になっていたとして、

```
└─A
 │ ├A1
 │ ├A2
 │ └A3
 ├B
 │ ├B1
 │ ├B2
 │ └B3
 └C
 　  ├C1
 　  ├C2
 　  └C3
```
一つ目のプルダウンが選択されると、その下部に属する構造が二つ目のプルダウンで選択できるようにしたい。

![pulldown.jpg](https://qiita-image-store.s3.amazonaws.com/0/102182/e4d6c8f7-6997-28f3-1748-64f14ecc81cc.jpeg)

# 実装してみる
## 実装
実装はアホみたいに簡単です。
雑ですが、全部載せて見ます。

```index.php
<?php
$data = [
    'A' => ['A1', 'A2', 'A3'],
    'B' => ['B1', 'B2', 'B3'],
    'C' => ['C1', 'C2', 'C3']
];
?>
<html>
<header>
    <meta charset="utf-8">
    <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB" crossorigin="anonymous">
</header>
<body>
<div id="app">
    <div class="row">
        <div class="offset-1">
            <h2>連動プルダウン</h2>
        </div>
    </div>
    <div class="row">
        <div class="col-3 offset-1 form-group">
            <select class="form-control" v-model="selected">
                <option v-for="parent in parents">
                    {{ parent }}
                </option>
            </select>
        </div>
        <div class="col-3 form-group">
            <select class="form-control" >
                <option v-for="child in children">
                    {{ child }}
                </option>
            </select>
        </div>
    </div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
<script>
var children_group = <?php echo json_encode($data); ?>;
var parents = Object.keys(children_group)

var app = new Vue({
    el: '#app',
    data: {
        selected: '',
        parents: parents
    },
    computed: {
        children: function () {
            return children_group[this.selected]
        }
    }
})
</script>
</body>
</html>
```
ど安定の素PHPですね。
これのあるディレクトリで、

```
$ php -S localhost:8888
```
とでもしてやれば、ブラウザで確認できます。
PHPアレルギーで、どうしても使いたくないって人は、jsonの部分を

```
{"A":["A1","A2","A3"],"B":["B1","B2","B3"],"C":["C1","C2","C3"]}
```
に書き換えて、上部のPHPを削除すればいいです。

## 解説
一応解説しておきます。

まず、一つ目のselect要素に`v-model`で`selected`を指定しておき、option要素で`parents`を展開します。ここで、`parents`はグローバル変数で定義している`parents`をそのまま使っています。

次に、二つ目のselect要素のoption要素に`children`を展開しています。
`children`はVueの中で`computed`に定義されていて、一つ目のselect要素が変更されるたびに、選択された要素を使って`children = children_group[this.selected]`が代入されるようになります。
これで、連動するプルダウンリストが実現できます。

# 複数要素のプルダウンリスト
プルダウンリストが複数それぞれ独立で動く場合を考えて見ましょう。
今回実現したいのは、以下のような場合です。

![multiple.jpg](https://qiita-image-store.s3.amazonaws.com/0/102182/8fca63d6-8f12-090f-012b-365de106ba34.jpeg)

## 実装
早速実装して見ましょう。

```multiple.php
<?php
$data = [
    'A' => ['A1', 'A2', 'A3'],
    'B' => ['B1', 'B2', 'B3'],
    'C' => ['C1', 'C2', 'C3']
];
$number = 4;
?>
<html>
<header>
    <meta charset="utf-8">
    <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB" crossorigin="anonymous">
</header>
<body>
<div id="app">
    <div class="row">
        <div class="offset-1">
            <h2>連動プルダウン</h2>
        </div>
    </div>
    <?php for ($i = 0; $i < $number; $i++) { ?>
    <div class="row">
    <div class="offset-1">
        <h3><?php echo $i + 1 ?>番目</h3>
    </div>
    </div>
    <div class="row">
        <div class="col-3 offset-1 form-group">
            <select class="form-control" v-model="selected[<?php echo $i ?>]">
                <option v-for="parent in parents">
                    {{ parent }}
                </option>
            </select>
        </div>
        <div class="col-3 form-group">
            <select class="form-control" >
                <option v-for="child in children(<?php echo $i ?>)">
                    {{ child }}
                </option>
            </select>
        </div>
    </div>
    <?php }?>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
<script>
var children_group = <?php echo json_encode($data); ?>;
var parents = Object.keys(children_group)
var default_selected = <?php echo json_encode(array_fill(0, $number - 1, '')); ?>;

var app = new Vue({
    el: '#app',
    data: {
        selected: default_selected,
        parents: parents,
        children: function (id) {
            return children_group[this.selected[id]]
        }
    }
})
</script>
</body>
</html>
```
どうでしょうか？こんなんでもちゃんと動きます。

## 解説
この実装のポイントだけかいつまんで解説します。

### selected が配列化した
select要素が複数出現しているため、`v-model`でバインディングされている`selected`が配列化しています。
これでとりあえず、何番目のプルダウンが変更されたかがわかるようになります。

```php
<select class="form-control" v-model="selected[<?php echo $i ?>]">
```
デフォルト値は全部空文字で定義しておきます。

### children が関数化した
もともと`computed`にあった`children`を`data`に持ってきて、関数化しています。
これで、プルダウンリスで選択中のものが変更されるごとに、どこで変更が発生して、どのリストを変更すれば良いかが、決定できます。

# まとめ
連動するプルダウンリストをVue.jsの力を使って実現しました。
グローバル変数にドカドカ値が投入されたりと、気にする人にとっては悪夢のような状態かもしれませんが、この程度の規模であれば気にすることもないでしょう。
コンポーネントを使ってみようかとも考えましたが、そこまでする必要もないなって思って先送りしています。

なんにせよ、Vueを使うことで、かなりあっさりと実装できたんじゃないかなって思います。

今回はこんなところです。