膨大な量のリストから1つだけ選択できるUIを考える

いろいろ作っていて四字熟語マスタが必要になってそこから1つ正解を選択することになった。

↓の都道府県みたいに47個だけならまだドロップダウンリスト(<select>)でOKかなと思う。

都道府県:

でも四字熟語ってもっと数が多いし、難しい漢字から探すのは大変すぎる。

そこから探せって言われてもなかなかの苦行なので、他に方法はないか探してみました。

目次

select multiple

<select>タグ内にmultipleを付けるだけ。

HTML
 <span>都道府県:</span>
  <select multiple style="border-width: 1px;">
    <option>北海道</option>
    <option>青森県</option>
    <option>秋田県</option>
    <option>岩手県</option>
    <option>山形県</option>
    <option>宮城県</option>
    <option>福島県</option>
    <option>茨城県</option>
    <option>栃木県</option>
    <option>群馬県</option>
    <option>埼玉県</option>
    <option>千葉県</option>
    <option>東京都</option>
    <option>神奈川県</option>
    <option>山梨県</option>
    <option>長野県</option>
    <option>新潟県</option>
    <option>富山県</option>
    <option>石川県</option>
    <option>福井県</option>
    <option>静岡県</option>
    <option>愛知県</option>
    <option>岐阜県</option>
    <option>三重県</option>
    <option>滋賀県</option>
    <option>京都府</option>
    <option>大阪府</option>
    <option>奈良県</option>
    <option>和歌山県</option>
    <option>兵庫県</option>
    <option>鳥取県</option>
    <option>島根県</option>
    <option>岡山県</option>
    <option>広島県</option>
    <option>山口県</option>
    <option>香川県</option>
    <option>愛媛県</option>
    <option>徳島県</option>
    <option>高知県</option>
    <option>福岡県</option>
    <option>大分県</option>
    <option>熊本県</option>
    <option>佐賀県</option>
    <option>長崎県</option>
    <option>宮崎県</option>
    <option>鹿児島県</option>
    <option>沖縄県</option>
  </select>
都道府県:

リストボックス形式になってくれてこれは良い!

と思ったけれど、multiple=複数の名のとおり、複数選択できてしまいます。

どうしても単一選択でないとダメだからこれは使えない。

気を付けて1つ選べば良い話かもしれないが、間違えて2つ以上選択するリスクもなくはないので、他の方法を探すことに。

datalist

HTMLには<datalist>タグというとても便利なものがあります。

HTML
  <span>都道府県:</span>
  <input type="text" name="todo_id" list="todo_list" style="border-width: 1px;">
  <datalist id="todo_list">
    <option>北海道</option>
    <option>青森県</option>
    <option>秋田県</option>
    <option>岩手県</option>
    <option>山形県</option>
    <option>宮城県</option>
    <option>福島県</option>
    <option>茨城県</option>
    <option>栃木県</option>
    <option>群馬県</option>
    <option>埼玉県</option>
    <option>千葉県</option>
    <option>東京都</option>
    <option>神奈川県</option>
    <option>山梨県</option>
    <option>長野県</option>
    <option>新潟県</option>
    <option>富山県</option>
    <option>石川県</option>
    <option>福井県</option>
    <option>静岡県</option>
    <option>愛知県</option>
    <option>岐阜県</option>
    <option>三重県</option>
    <option>滋賀県</option>
    <option>京都府</option>
    <option>大阪府</option>
    <option>奈良県</option>
    <option>和歌山県</option>
    <option>兵庫県</option>
    <option>鳥取県</option>
    <option>島根県</option>
    <option>岡山県</option>
    <option>広島県</option>
    <option>山口県</option>
    <option>香川県</option>
    <option>愛媛県</option>
    <option>徳島県</option>
    <option>高知県</option>
    <option>福岡県</option>
    <option>大分県</option>
    <option>熊本県</option>
    <option>佐賀県</option>
    <option>長崎県</option>
    <option>宮崎県</option>
    <option>鹿児島県</option>
    <option>沖縄県</option>
  </datalist>
都道府県:

上記のように<input>タグと組み合わせて使います。

<datalist>タグは見えないので、見た目はただの<input>タグでしかないですが、

例えば、上記のテキストボックスに「福」と入力すると、

「福島県」「福井県」「福岡県」と入力候補が出てきてくれます。

これは便利ですよね!

でも、この方法だと取得できるのはname属性でなく文字列(上記の例でいう都道府県)なのでこの方法も使えない。

うーん、惜しい。

実はできなくもないのですが、

HTML
  <span>都道府県:</span>
  <input type="text" name="todo_id" list="todo_list" style="border-width: 1px;">
  <datalist id="todo_list">
    <option value="hokkaido">北海道</option>
    <option value="aomori">青森県</option>
    <option value="akita">秋田県</option>
    <option value="iwate">岩手県</option>
    <option value="yamagata">山形県</option>
    <option value="miyagi">宮城県</option>
    <option value="fukushima">福島県</option>
    <option value="ibaraki">茨城県</option>
    <option value="tochigi">栃木県</option>
    <option value="gunma">群馬県</option>
    <option value="saitama">埼玉県</option>
    <option value="chiba">千葉県</option>
    <option value="tokyo">東京都</option>
    <option value="kanagawa">神奈川県</option>
    <option value="yamanashi">山梨県</option>
    <option value="nagano">長野県</option>
    <option value="niigata">新潟県</option>
    <option value="toyama">富山県</option>
    <option value="ishikawa">石川県</option>
    <option value="fukui">福井県</option>
    <option value="shizuoka">静岡県</option>
    <option value="aichi">愛知県</option>
    <option value="gifu">岐阜県</option>
    <option value="mie">三重県</option>
    <option value="shiga">滋賀県</option>
    <option value="kyoto">京都府</option>
    <option value="osaka">大阪府</option>
    <option value="nara">奈良県</option>
    <option value="wakayama">和歌山県</option>
    <option value="hyogo">兵庫県</option>
    <option value="tottori">鳥取県</option>
    <option value="shimane">島根県</option>
    <option value="okayama">岡山県</option>
    <option value="hiroshima">広島県</option>
    <option value="yamaguchi">山口県</option>
    <option value="kagawa">香川県</option>
    <option value="ehime">愛媛県</option>
    <option value="tokushima">徳島県</option>
    <option value="kochi">高知県</option>
    <option value="fukuoka">福岡県</option>
    <option value="oita">大分県</option>
    <option value="kumamoto">熊本県</option>
    <option value="saga">佐賀県</option>
    <option value="nagasaki">長崎県</option>
    <option value="miyazaki">宮崎県</option>
      <option value="kagoshima">鹿児島県</option>
    <option value="okinawa">沖縄県</option>
  </datalist>
都道府県:

とvalueがメインで表示されてしまいます。

出来れば逆にしたいですね。。。

それに致命的なのは、<datalist>+<input>タグの組み合わせだと、あくまでも<input>タグに入力されたものが送信されるので、

POSTやGETされてきた入力値は、<datalist>から選択されたものの保証は無いんです。

<input>タグって自由に入力できてしまいますよね?

<datalist>には<input>タグの入力規制機能はありません。

なのでこれにちょっと手を加えたいと思います。

datalistを使った安全なidの送信方法

実は何でも入力できてしまうdatalist。

そんなdatalistを使った安全なidの送信方法をお伝えします。

テキストボックス+JavaScriptで送信する

まず、datalistに入力させたい文字列(例では都道府県)をvalueに入力し<option>と</option>の間は空にします。

それと同時に、datalistの検索用のテキストボックスとは別にidを送るためのhiddenにしたテキストボックスを設置します。

HTML
  <label>都道府県:</label>
  <input type="text" id="todo_search" list="todo_list">
  <input type="hidden" name="todo_id" id="todo_id">
  <datalist id="todo_list">
    <option value="北海道"></option>
    <option value="青森県"></option>
    <option value="秋田県"></option>
    <option value="岩手県"></option>
    <option value="山形県"></option>
    <option value="宮城県"></option>
    <option value="福島県"></option>
    <option value="茨城県"></option>
    <option value="栃木県"></option>
    <option value="群馬県"></option>
    <option value="埼玉県"></option>
    <option value="千葉県"></option>
    <option value="東京都"></option>
    <option value="神奈川県"></option>
    <option value="山梨県"></option>
    <option value="長野県"></option>
    <option value="新潟県"></option>
    <option value="富山県"></option>
    <option value="石川県"></option>
    <option value="福井県"></option>
    <option value="静岡県"></option>
    <option value="愛知県"></option>
    <option value="岐阜県"></option>
    <option value="三重県"></option>
    <option value="滋賀県"></option>
    <option value="京都府"></option>
    <option value="大阪府"></option>
    <option value="奈良県"></option>
    <option value="和歌山県"></option>
    <option value="兵庫県"></option>
    <option value="鳥取県"></option>
    <option value="島根県"></option>
    <option value="岡山県"></option>
    <option value="広島県"></option>
    <option value="山口県"></option>
    <option value="香川県"></option>
    <option value="愛媛県"></option>
    <option value="徳島県"></option>
    <option value="高知県"></option>
    <option value="福岡県"></option>
    <option value="大分県"></option>
    <option value="熊本県"></option>
    <option value="佐賀県"></option>
    <option value="長崎県"></option>
    <option value="宮崎県"></option>
    <option value="鹿児島県"></option>
    <option value="沖縄県"></option>
  </datalist>

また、JavaScript内にid表を作り、name(入力させたい文字列)と同じ文字が入力された場合だけ(完全一致チェック)

idをhiddenにしたテキストのvalueに自動入力します。

つまり、入力された文字は送られることはなく、サーバーに送られるのはidだけになります。

JavaScript
document.addEventListener("DOMContentLoaded", function(e) {

const todofuken = [
  {id: 'hokkaido', name: "北海道"},
  {id: 'aomori', name: "青森県"},
  {id: 'akita', name: "秋田県"},
  {id: 'iwate', name: "岩手県"},
  {id: 'yamagata', name: "山形県"},
  {id: 'miyagi', name: "宮城県"},
  {id: 'fukushima', name: "福島県"},
  {id: 'ibaraki', name: "茨城県"},
  {id: 'tochigi', name: "栃木県"},
  {id: 'gunma', name: "群馬県"},
  {id: 'saitama', name: "埼玉県"},
  {id: 'chiba', name: "千葉県"},
  {id: 'tokyo', name: "東京都"},
  {id: 'kanagawa', name: "神奈川県"},
  {id: 'yamanashi', name: "山梨県"},
  {id: 'nagano', name: "長野県"},
  {id: 'niigata', name: "新潟県"},
  {id: 'toyoma', name: "富山県"},
  {id: 'ishikawa', name: "石川県"},
  {id: 'fukui', name: "福井県"},
  {id: 'shizuoka', name: "静岡県"},
  {id: 'aichi', name: "愛知県"},
  {id: 'gifu', name: "岐阜県"},
  {id: 'mie', name: "三重県"},
  {id: 'shiga', name: "滋賀県"},
  {id: 'kyoto', name: "京都府"},
  {id: 'osaka', name: "大阪府"},
  {id: 'nara', name: "奈良県"},
  {id: 'wakayama', name: "和歌山県"},
  {id: 'hyogo', name: "兵庫県"},
  {id: 'tottori', name: "鳥取県"},
  {id: 'shimane', name: "島根県"},
  {id: 'okayama', name: "岡山県"},
  {id: 'hiroshima', name: "広島県"},
  {id: 'yamaguchi', name: "山口県"},
  {id: 'kagawa', name: "香川県"},
  {id: 'ehime', name: "愛媛県"},
  {id: 'tokushima', name: "徳島県"},
  {id: 'kochi', name: "高知県"},
  {id: 'fukuoka', name: "福岡県"},
  {id: 'oita', name: "大分県"},
  {id: 'kumamoto', name: "熊本県"},
  {id: 'saga', name: "佐賀県"},
  {id: 'nagasaki', name: "長崎県"},
  {id: 'miyazaki', name: "宮崎県"},
  {id: 'kagoshima', name: "鹿児島県"},
  {id: 'okinawa', name: "沖縄県"}
];

const todoSearch = document.getElementById("todo_search");
const todoId = document.getElementById("todo_id");

if (!todoSearch || !todoId) return;

todoSearch.addEventListener("input", function() {

  const match = todofuken.find(t => t.name === this.value);

  if (match) {
    todoId.value = match.id;
  } else {
    todoId.value = ""; // 不正入力防止
  }
})
});

さらに堅牢にするならJavaScriptのsubmitで規制をかけます。

HTML
<form id="todo_form" method="post">
...
  <button type="submit">送信</button>
</form>
JavaScript
const form = document.getElementById("todo_form");

form.addEventListener("submit", function(e) {
  if (!todoId.value) {
    e.preventDefault();
    alert("正しい都道府県を選択してください");
  }
});

上記を合わせるとこんな感じになります。

See the Pen safe datalist by gulf_stream (@Hitomi-Kuge) on CodePen.

サーバー側でホワイトリストを作って規制する

念のため、送信先のPHP側でもホワイトリストを作ってバリデーションをしておきます。

PHP
$allowed = [
    'hokkaido','aomori','akita','iwate','yamagata','miyagi','fukushima',
    'ibaraki','tochigi','gunma','saitama','chiba','tokyo','kanagawa',
    'yamanashi','nagano','niigata','toyama','ishikawa','fukui',
    'shizuoka','aichi','gifu','mie','shiga','kyoto','osaka','nara',
    'wakayama','hyogo','tottori','shimane','okayama','hiroshima',
    'yamaguchi','kagawa','ehime','tokushima','kochi','fukuoka',
    'oita','kumamoto','saga','nagasaki','miyazaki','kagoshima','okinawa'
];

$todo_id = $_POST['todo_id'] ?? '';

if (!in_array($todo_id, $allowed, true)) {
    die('不正な入力です');
}

これでdatalistからidを安全に送信することが出来ます。

おわりに

今回は、如何に膨大なリストから1つだけ対象の項目を選択するUIを作るかについてご紹介しました。

実はdatalistは初めて知ったのですが、何でも入力できてしまう落とし穴があったんですね。

今回の記事が少しでも参考になれば嬉しいです!

  • URLをコピーしました!
目次