都道府県連動の市区町村セレクトボックス

#はじめに
登録フォームであったり一覧画面の検索ボックスであったり、都道府県や市区町村のセレクトボックスを設ける事もあるかと思います。
その場合、当然ながら都道府県と市区町村は連動していて欲しい・・・

昔から変わらず在る要望なので巷にはサンプルソースが溢れているのですが、

+ HTMLとJavaScriptでなるべく手軽に仕上げたい
+ ただしフレームワークはPHPだ
+ しかも都道府県だけはDBから書き出したい
+ とは言え選択のたびにサーバーと通信したくはないのでAjaxは嫌だ
+ ちなみにこの実装にかけられる工数（予算）はほとんどない

といった事を考えていった結果、気軽に流用できずにカスタマイズを行った挙句、工数がかさんでしまっているので、
ここに備忘録として残したいと思います。

#市区町村の情報
頻繁に変わるものでもないし気にしないどこ・・・
と深く触れずに実装を進めていったところ、突然クライアントから質問を受けるのです。

「市区町村の合併が発生した時はどうなりますか？」

どうなるも何も、そのままです・・・などとはなかなか言えないものです。
とは言え、さほど更新頻度が高いものでもないのに管理機能を設けるのは如何なものか。

ここは総務省のサイトから取得した「全国地方公共団体コード」をちょっと加工するだけでデータが準備できるようにしてみましょう。

https://www.soumu.go.jp/denshijiti/code.html


利用するのは「都道府県コード及び市区町村コード」のExcelファイルです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/186115/a177da32-97ca-8f94-13be-d0337d30e379.png)
Excelのままでは扱えないのでCSV形式で出力します。

このままCSVで使いたいところですが、プログラムでの扱いやすさを考えてJSON形式に変換します。
幸い世の中にはCSVファイルをアップロードするとJSONに変換してくれるサイトが山ほどありますので、ありがたく利用させていただきましょう。

JSON変換するとヘッダー行がキーとなります。
「団体コード」といった日本語のままでは都合が悪いので、下記の様に英数字に置き換えます。

|元の文字|置き換え後の文字|
|:-----------|:------------|
|団体コード|groupCode|
|都道府県名（漢字） |prefectureName|
|市区町村名（漢字）|cityName|
|都道府県名（ｶﾅ）|prefectureKana|
|市区町村名（ｶﾅ）|cityKana|

こうして出来上がったJSONがこちら。
これで市区町村の元データは準備万端です。この程度であれば保守運用費の中で十分対応できそうですし、クライアントにやっていただけるかもしれません。

```json:prefectureCityCode.json
[
{"groupCode":"010006","prefectureName":"北海道","cityName":"","prefectureKana":"ﾎｯｶｲﾄﾞｳ","cityKana":""},
{"groupCode":"011002","prefectureName":"北海道","cityName":"札幌市","prefectureKana":"ﾎｯｶｲﾄﾞｳ","cityKana":"ｻｯﾎﾟﾛｼ"},
{"groupCode":"012025","prefectureName":"北海道","cityName":"函館市","prefectureKana":"ﾎｯｶｲﾄﾞｳ","cityKana":"ﾊｺﾀﾞﾃｼ"},
{"groupCode":"012033","prefectureName":"北海道","cityName":"小樽市","prefectureKana":"ﾎｯｶｲﾄﾞｳ","cityKana":"ｵﾀﾙｼ"},
{"groupCode":"012041","prefectureName":"北海道","cityName":"旭川市","prefectureKana":"ﾎｯｶｲﾄﾞｳ","cityKana":"ｱｻﾋｶﾜｼ"},
{"groupCode":"012050","prefectureName":"北海道","cityName":"室蘭市","prefectureKana":"ﾎｯｶｲﾄﾞｳ","cityKana":"ﾑﾛﾗﾝｼ"},
{"groupCode":"012068","prefectureName":"北海道","cityName":"釧路市","prefectureKana":"ﾎｯｶｲﾄﾞｳ","cityKana":"ｸｼﾛｼ"},
{"groupCode":"012076","prefectureName":"北海道","cityName":"帯広市","prefectureKana":"ﾎｯｶｲﾄﾞｳ","cityKana":"ｵﾋﾞﾋﾛｼ"},
...
{"groupCode":"473812","prefectureName":"沖縄県","cityName":"竹富町","prefectureKana":"ｵｷﾅﾜｹﾝ","cityKana":"ﾀｹﾄﾐﾁｮｳ"},
{"groupCode":"473821","prefectureName":"沖縄県","cityName":"与那国町","prefectureKana":"ｵｷﾅﾜｹﾝ","cityKana":"ﾖﾅｸﾞﾆﾁｮｳ"}
]
```

#サンプルソース その1

先ほど準備したJSONファイルから市区町村のセレクトボックスを生成できるようにします。
大阪都構想がとん挫した今、都道府県が変わる事まで考慮する必要はなさそうです。たかだか47個ですし、コードの単純化のためにも都道府県はHTMLで記載します。

なお、都道府県セレクトボックスのvalue値は「北海道」などの都道府県名にしていますが、場合によっては「01」や「13」などの数値にしたいかもしれません。
あえて市区町村の絞込にはvalue値は使わず、data属性を利用するようにしました。

また、検索ボックスであれば初期値を設定させたい場合もあるでしょう。
都道府県のセレクトボックスは「selected」を出力すれば良いですが、市区町村も設定できるように初期値用の変数「initPrefVal」と「initCityVal」を用意します。

```html:index2.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Prefecture and city selection</title>
  </head>
  <body>
    <div class="">
      <select name="select_prefecture" id="select_prefecture" class="">
        <option value="">都道府県を選択</option>
        <option value="北海道" data-pref-id="01">北海道</option>
        <option value="青森県" data-pref-id="02">青森県</option>
        <option value="岩手県" data-pref-id="03">岩手県</option>
        <option value="宮城県" data-pref-id="04">宮城県</option>
        <option value="秋田県" data-pref-id="05">秋田県</option>
        <option value="山形県" data-pref-id="06">山形県</option>
        <option value="福島県" data-pref-id="07">福島県</option>
        <option value="茨城県" data-pref-id="08">茨城県</option>
        <option value="栃木県" data-pref-id="09">栃木県</option>
        <option value="群馬県" data-pref-id="10">群馬県</option>
        <option value="埼玉県" data-pref-id="11">埼玉県</option>
        <option value="千葉県" data-pref-id="12">千葉県</option>
        <option value="東京都" data-pref-id="13">東京都</option>
        <option value="神奈川県" data-pref-id="14">神奈川県</option>
        <option value="新潟県" data-pref-id="15">新潟県</option>
        <option value="富山県" data-pref-id="16">富山県</option>
        <option value="石川県" data-pref-id="17">石川県</option>
        <option value="福井県" data-pref-id="18">福井県</option>
        <option value="山梨県" data-pref-id="19">山梨県</option>
        <option value="長野県" data-pref-id="20">長野県</option>
        <option value="岐阜県" data-pref-id="21">岐阜県</option>
        <option value="静岡県" data-pref-id="22">静岡県</option>
        <option value="愛知県" data-pref-id="23">愛知県</option>
        <option value="三重県" data-pref-id="24">三重県</option>
        <option value="滋賀県" data-pref-id="25">滋賀県</option>
        <option value="京都府" data-pref-id="26">京都府</option>
        <option value="大阪府" data-pref-id="27">大阪府</option>
        <option value="兵庫県" data-pref-id="28">兵庫県</option>
        <option value="奈良県" data-pref-id="29">奈良県</option>
        <option value="和歌山県" data-pref-id="30">和歌山県</option>
        <option value="鳥取県" data-pref-id="31">鳥取県</option>
        <option value="島根県" data-pref-id="32">島根県</option>
        <option value="岡山県" data-pref-id="33">岡山県</option>
        <option value="広島県" data-pref-id="34">広島県</option>
        <option value="山口県" data-pref-id="35">山口県</option>
        <option value="徳島県" data-pref-id="36">徳島県</option>
        <option value="香川県" data-pref-id="37">香川県</option>
        <option value="愛媛県" data-pref-id="38">愛媛県</option>
        <option value="高知県" data-pref-id="39">高知県</option>
        <option value="福岡県" data-pref-id="40">福岡県</option>
        <option value="佐賀県" data-pref-id="41">佐賀県</option>
        <option value="長崎県" data-pref-id="42">長崎県</option>
        <option value="熊本県" data-pref-id="43">熊本県</option>
        <option value="大分県" data-pref-id="44">大分県</option>
        <option value="宮崎県" data-pref-id="45">宮崎県</option>
        <option value="鹿児島県" data-pref-id="46">鹿児島県</option>
        <option value="沖縄県" data-pref-id="47">沖縄県</option>
      </select>
      <select name="select_city" id="select_city" class="">
        <option value="">市区町村を選択</option>
      </select>
    </div>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
    <script src="./prefectureCity.js"></script>
    <script>
      $(function() {
        let initPrefVal = '東京都'; //selectedを付与したい都道府県
        let initCityVal = '渋谷区'; //selectedを付与したい市区町村
        let valType = 'name';  // 'name' or 'code'

        // セレクトボックスに初期値を設定
        if (initPrefVal) {
          getCitySelection('#select_prefecture', '#select_city', './prefectureCityCode.json', valType, initPrefVal, initCityVal);
        }

        // 都道府県選択時に市区町村リストを作成
        $('#select_prefecture').on('change', function() {
          getCitySelection('#select_prefecture', '#select_city', './prefectureCityCode.json', valType);
        });
      });

    </script>
  </body>
</html>
```

実際に処理を行うJavaScriptです。
「全国地方公共団体コード仕様」によれば、全国地方公共団体コードの上位2桁の数字（北海道なら01、東京都なら13）は各都道府県を識別する数字になっているようです。

選択されたoptionタグのdata属性から、都道府県を識別する数字（01や13）を取得し、傘下の市区町村だけをセレクトボックスのoptionタグとして追加してやります。

```javascript:prefectureCity.js
function getCitySelection(prefSelectionId, citySelectionId, jsonPath, valType='code', initPrefVal=null, initCityVal=null, selectedPrefCode=null) {
  // 市区町村リストのリセット
  $(citySelectionId + ' option:nth-child(n+2)').remove();

  // 都道府県コードの取得
  let prefCode = '01';
  if (selectedPrefCode) {
    prefCode = selectedPrefCode;
  } else if (initPrefVal) {
    prefCode = ('00' + $(prefSelectionId + ' option[value="' + initPrefVal + '"]').data('pref-id')).slice(-2);
    $(prefSelectionId + ' option[value="' + initPrefVal + '"]').prop('selected', true);
  } else {
    prefCode = ('00' + $(prefSelectionId + ' option:selected').data('pref-id')).slice(-2);
  }

  // JSONファイルから市区町村を取得
  $.getJSON(jsonPath, function (data) {
    $.grep(data,
      function (obj, idx) {
        // 都道府県コード参加の市区町村コードを取得
        if (obj.groupCode.startsWith(prefCode) && obj.cityName) {
          let selected = '';
          if (initCityVal && (
            (valType === 'code' && obj.groupCode.startsWith(prefCode + ('00' + initCityVal).slice(-3)))
            || (valType === 'name' && obj.cityName === initCityVal))
          ) {
            // 市区町村の初期値を設定
            selected = ' selected';
          }
          if (valType === 'code') {
            $(citySelectionId).append('<option value="' + obj.groupCode.slice(2, 5) + '"' + selected + '>' + obj.cityName + '</option>');
          } else {
            $(citySelectionId).append('<option value="' + obj.cityName + '"' + selected + '>' + obj.cityName + '</option>');
          }
        }
      }
    );
  });
}
```

#サンプルソース その2
コメントで下記のようなご指摘をいただきました。

> 都道府県のoptionは固定で持たせるにしても、47もあるのであればJavaScript側で要素を追記するほうが管理も楽ではないでしょうか。

た、確かに・・・:sweat_smile:！

冷静に考えると、JSONファイルに都道府県が含まれているのに、都道府県だけべた書きというのも変でしたね。
という事で、市区町村のセレクトボックスに加えて都道府県のセレクトボックスもJSONファイルから生成した場合のサンプルソースも追記します。

```html:index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Prefecture and city selection</title>
  </head>
  <body>
    <div class="">
      <select name="select_prefecture" id="select_prefecture" class="">
        <option value="">都道府県を選択</option>
      </select>
      <select name="select_city" id="select_city" class="">
        <option value="">市区町村を選択</option>
      </select>
    </div>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
    <script src="./prefectureCity.js"></script>
    <script>
      $(function() {
        let initPrefVal = '東京都'; //selectedを付与したい都道府県
        let initCityVal = '渋谷区'; //selectedを付与したい市区町村
        let valType = 'name';  // 'name' or 'code'

        // 都道府県リストを作成
        getPrefectureSelection('#select_prefecture', '#select_city', './prefectureCityCode.json', valType, initPrefVal, initCityVal);

        // 都道府県選択時に市区町村リストを作成
        $('#select_prefecture').on('change', function() {
          getCitySelection('#select_prefecture', '#select_city', './prefectureCityCode.json', valType);
        });
      });

    </script>
  </body>
</html>
```

```javascript:prefectureCity.js
function getPrefectureSelection(prefSelectionId, citySelectionId, jsonPath, valType='code', initPrefVal=null, initCityVal=null) {
  // JSONファイルから都道府県を取得
  $.getJSON(jsonPath, function (data) {
    $.grep(data,
      function (obj, idx) {
        // Get the prefecture.
        if (!obj.cityName) {
          let selected = '';
          if (initPrefVal && (
            (valType === 'code' && obj.groupCode.startsWith(('00' + initPrefVal).slice(-2)))
            || (valType === 'name' && obj.prefectureName === initPrefVal))
          ) {
            selected = ' selected';
            // 都道府県選択時に市区町村リストを作成
            getCitySelection(prefSelectionId, citySelectionId, jsonPath, valType, initPrefVal, initCityVal, obj.groupCode.slice(0, 2));
          }
          if (valType === 'code') {
            $(prefSelectionId).append('<option value="' + obj.groupCode.slice(0, 2) + '" data-pref-id="' + obj.groupCode.slice(0, 2) + '"' + selected + '>' + obj.prefectureName + '</option>');
          } else {
            $(prefSelectionId).append('<option value="' + obj.prefectureName + '" data-pref-id="' + obj.groupCode.slice(0, 2) + '"' + selected + '>' + obj.prefectureName + '</option>');
          }
        }
      }
    );
  });
}

function getCitySelection(prefSelectionId, citySelectionId, jsonPath, valType='code', initPrefVal=null, initCityVal=null, selectedPrefCode=null) {
  // 市区町村リストのリセット
  $(citySelectionId + ' option:nth-child(n+2)').remove();

  // 都道府県コードの取得
  let prefCode = '01';
  if (selectedPrefCode) {
    prefCode = selectedPrefCode;
  } else if (initPrefVal) {
    prefCode = ('00' + $(prefSelectionId + ' option[value="' + initPrefVal + '"]').data('pref-id')).slice(-2);
    $(prefSelectionId + ' option[value="' + initPrefVal + '"]').prop('selected', true);
  } else {
    prefCode = ('00' + $(prefSelectionId + ' option:selected').data('pref-id')).slice(-2);
  }

  // JSONファイルから市区町村を取得
  $.getJSON(jsonPath, function (data) {
    $.grep(data,
      function (obj, idx) {
        // 都道府県コード参加の市区町村コードを取得
        if (obj.groupCode.startsWith(prefCode) && obj.cityName) {
          let selected = '';
          if (initCityVal && (
            (valType === 'code' && obj.groupCode.startsWith(prefCode + ('00' + initCityVal).slice(-3)))
            || (valType === 'name' && obj.cityName === initCityVal))
          ) {
            // 市区町村の初期値を設定
            selected = ' selected';
          }
          if (valType === 'code') {
            $(citySelectionId).append('<option value="' + obj.groupCode.slice(2, 5) + '"' + selected + '>' + obj.cityName + '</option>');
          } else {
            $(citySelectionId).append('<option value="' + obj.cityName + '"' + selected + '>' + obj.cityName + '</option>');
          }
        }
      }
    );
  });
}
```

「その1」と「その2」のソースでjsファイルを共用しているので、無駄な変数やコードがありますが、あしからず。:joy:

#GitHub
自分用の備忘録ですが、サンプルソースをGithubに上げています。
[prefecture-city-selection-html](https://github.com/fakefurcoronet/prefecture-city-selection-html.git)

そのままだとローカル環境ではCORSの問題で動かないので、サーバーにアップして確認するかオプション`--disable-web-security --user-data-dir="C://Chrome dev session"`を指定したChromeで確認してください。

#おわりに
毎回「もうちょっと手軽なやり方があったのでは」と思いつつも、今の自分の中で思いつく限界がこちら:innocent: