テキストボックスのオートコンプリート / サジェストを実装する

オートコンプリート(サジェスト)を実装するコードを紹介します。

概要

AutoCompleteExtender を用いてテキストボックスのオートコンプリートを実装する」の記事では、Ajax Control ToolkitのAutoCompleteExtenderを用いてオートコンプリートを実装する手順を紹介しました。この記事では、Webフォームではなく、htmlファイルでオートコンプリートを実装したい場合や、Ajax Control Toolkitを用いずにオートコンプリートを実装するコードを紹介します。

事前準備

データベースサーバーの導入

オートコンプリートされるデータを格納するためのデータベースサーバーを準備します。今回はSQL Serverを準備しました。

テーブルの作成とデータの追加

データベースのテーブルにデータを追加します。具体的な手順はこちらの記事を参照してください。

DynamicJSONのインストール

今回、サーバー側のプログラムでJSONを出力するためにDynamicJSONを用いています。DynamicJSONをあらかじめダウンロードします。

サーバー側の実装

オートコンプリートの候補データを返すサーバー側を実装します。クライアント側からXMLHttpRequestでアクセスすることを想定しているため、サーバー側のレスポンスは、データを扱いやすいJSON形式で返すことにします。JSON形式の詳細については「JSONの書式」の記事を参照してください。

今回はサーバー側はC#のジェネリックハンドラで実装しています。JSON形式を返すサーバー側プログラムであれば他の言語でも構いません。

AutoCompleteHandler.ashx.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Data.SqlClient;
using Codeplex.Data;

namespace AutoCompleteDemo
{
  /// <summary>
  /// AutoCompleteHandler の概要の説明です
  /// </summary>
  public class AutoCompleteHandler : IHttpHandler
  {
    //SQL Server 接続文字列
    string ConnectionString = "Data Source=xxx.xxx.xxx.xxx;Initial Catalog=iPentecSandBox;Connect Timeout=60;Persist Security Info=True;User ID=zzzzzz;Password=zzzzzz";

    public void ProcessRequest(HttpContext context)
    {
      string prefixText = context.Request.QueryString["q"];

      List<String> list = new List<String>();
      SqlConnection con = new SqlConnection(ConnectionString);
      con.Open();
      try {
        SqlCommand com = new SqlCommand("select * from AutoCompleteDemoItem where itemtext like @query", con);
        SqlParameter param = new SqlParameter();
        param.ParameterName = "@query";
        param.Value = prefixText + "%";
        com.Parameters.Add(param);

        SqlDataReader sdr = com.ExecuteReader();
        while (sdr.Read() == true) {
          list.Add(((string)sdr["itemtext"]).Trim());
        }
      }
      finally {
        con.Close();
      }

      var obj = new
      {
        Text = list.ToArray(),
      };
      context.Response.ContentType = "application/javascript";
      context.Response.Write(DynamicJson.Serialize(obj));
    }

    public bool IsReusable
    {
      get
      {
        return false;
      }
    }
  }
}

解説

クライアント側からテキストボックスに入力された文字列がqパラメータに渡されます。
パラメータを取得し、データベースを検索し該当する候補のワードを取得します。取得したワードは"Text"キーに配列で格納します。JSONの書き出しにはDynamicJsonを用いています。JSONの書き出しの詳細については。「DynamicJSONを利用したJSONの作成・書き出し」の記事を参照してください。

クライアント側の実装

クライアント側のHTMLファイルを作成します。

index.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
  <title></title>
  <link rel="stylesheet" type="text/css" href="default.css" />
  <script type="text/javascript">
  function getJson() {
    //var xmlhttp = createXMLHttpRequest(); //旧バージョンのIEなどに対応する場合
    var xmlhttp = new XMLHttpRequest();

    xmlhttp.onreadystatechange = function () {
      if (xmlhttp.readyState == 4) {
        if (xmlhttp.status == 200) {
          var data = JSON.parse(xmlhttp.responseText);
          ShowCompletePopup(data);
        } else {
          alert("status = " + xmlhttp.status);
        }
      }
    }
    var textBox = document.getElementById("Text1");
    var textbox_text = textBox.value;
    if (textbox_text != "") {
      xmlhttp.open("GET", "AutoCompleteHandler.ashx?q=" + textbox_text);
      xmlhttp.send();
    } else {
      CloseCompletePopup();
    }
  }

  function createXMLHttpRequest() {
    if (window.XMLHttpRequest) { return new XMLHttpRequest() }
    if (window.ActiveXObject) {
      try { return new ActiveXObject("Msxml2.XMLHTTP.6.0") } catch (e) { }
      try { return new ActiveXObject("Msxml2.XMLHTTP.3.0") } catch (e) { }
      try { return new ActiveXObject("Microsoft.XMLHTTP") } catch (e) { }
    }
    return false;
  }

  function ShowCompletePopup(data) {
    if (0 < data.Text.length) {
      var textBox = document.getElementById("Text1");
      var textbox_width = textBox.offsetWidth;
      var textbox_left = textBox.offsetLeft;
      var textbox_bottom = textBox.offsetTop + textBox.offsetHeight;

      var elem = document.getElementById("AutoCompleteList");
      elem.style.width = textbox_width + "px";
      elem.style.left = textbox_left + "px";
      elem.offsetTop = textbox_bottom;
      elem.style.visibility = "visible";
      elem.innerHTML = "";

      for (i = 0; i < data.Text.length; i++) {
        elem.innerHTML += "<li onclick=\"CandidateItemClick('" + data.Text[i] + "')\">" + data.Text[i] + "</li>";
      }
    }
  }

  function CloseCompletePopupDelay(time) {
    setTimeout('CloseCompletePopup();', time);
  }

  function CloseCompletePopup() {
    var elem = document.getElementById("AutoCompleteList");
    elem.style.visibility = "hidden";
  }

  function CandidateItemClick(ItemText) {
    var textBox = document.getElementById("Text1");
    textBox.innerText = ItemText;
    CloseCompletePopup();
  }
  </script>
</head>
<body>
  <div>
    <input id="Text1" type="text" onkeyup="getJson();" onblur="CloseCompletePopupDelay(500);" />
    <ul id="AutoCompleteList" class="AutoCompleteListFrame"></ul>
  </div>
</body>
</html>

解説

  <input id="Text1" type="text" onkeyup="getJson();" onblur="CloseCompletePopupDelay(500);" />
  <ul id="AutoCompleteList" class="AutoCompleteListFrame"></ul>
テキストボックスを配置します。キーが離れた際にgetJson()関数を呼び出し、テキストボックスに入力された文字をサーバー側に送り候補の一覧を取得します。また、テキストボックスがフォーカスを失った場合に候補のポップアップ枠を消すため、onblurイベントではポップアップ枠を消去する関数(CloseCompletePopupDelay)を呼び出します。
変換候補の枠はテキストボックスの直後のul id="AutoCompleteList"のタグに相当します。デフォルトでは非表示になっており、表示される際にスタイルのプロパティで表示、非表示を切り替えます。

補足:onkeyup 以外のイベント(onkeydown, onkeypress)を用いないのか?

onkeyup以外のonkeydown, onkeypressイベントを用いた場合、イベントが発生した際、入力された文字がテキストボックスに反映されていません。そのため、初回のonkeydownイベントではテキストボックスの内容が空になっています。また、"Pengu"までテキストが入力された状態で"i"キーを押した場合、onkeydown, onkeypressイベントではテキストボックス内の文字列は"Pengu"となっており、"i"が入力内容に反映されていない状態となります。onkeydown, onkeypressイベントでは押されたキーを取得できるため、テキストボックス内の文字列に押されたキーの文字を結合する方法で対応できますが、今回はコードをシンプルにするために、onkeyup イベントを用いました。

補足:onblurでポップアップ枠を閉じる際にディレイがあるのはなぜ?

onblurイベントの時点で、即ポップアップ枠を非表示にする場合、ポップアップ枠内の候補がクリックされた瞬間にテキストボックスのblueイベントが実行されるため、ポップアップ枠の要素のクリックイベントが取得できない状態になります。blurイベントが発生した際に遅延をさせることで、ポップアップ枠のクリックイベントが取得できます。

getJson() 関数

  function getJson() {
    //var xmlhttp = createXMLHttpRequest(); //旧バージョンのIEなどに対応する場合
    var xmlhttp = new XMLHttpRequest();

    xmlhttp.onreadystatechange = function () {
      if (xmlhttp.readyState == 4) {
        if (xmlhttp.status == 200) {
          var data = JSON.parse(xmlhttp.responseText);
          ShowCompletePopup(data);
        } else {
          alert("status = " + xmlhttp.status);
        }
      }
    }
    var textBox = document.getElementById("Text1");
    var textbox_text = textBox.value;
    if (textbox_text != "") {
      xmlhttp.open("GET", "AutoCompleteHandler.ashx?q=" + textbox_text);
      xmlhttp.send();
    } else {
      CloseCompletePopup();
    }
  }
テキストボックスでキーが離された際に実行されます。テキストボックスに入力された値を取得し、AutoCompleteHandler.ashxファイルにアクセスします。パラメーター"q"にテキストボックスの入力内容を渡します。AutoCompleteHandler.ashxからは、JSON形式の戻り値を受け取ります。結果を受け取ると、JSON形式のレスポンスデータをパースし、ShowCompletePopup()関数を呼び出して候補を表示する枠を表示します。

ShowCompletePopup() 関数

  function ShowCompletePopup(data) {
    if (0 < data.Text.length) {
      var textBox = document.getElementById("Text1");
      var textbox_width = textBox.offsetWidth;
      var textbox_left = textBox.offsetLeft;
      var textbox_bottom = textBox.offsetTop + textBox.offsetHeight;

      var elem = document.getElementById("AutoCompleteList");
      elem.style.width = textbox_width + "px";
      elem.style.left = textbox_left + "px";
      elem.offsetTop = textbox_bottom;
      elem.style.visibility = "visible";
      elem.innerHTML = "";

      for (i = 0; i < data.Text.length; i++) {
        elem.innerHTML += "<li onclick=\"CandidateItemClick('" + data.Text[i] + "')\">" + data.Text[i] + "</li>";
      }
    }
  }
テキストボックスの直下にオートコンプリートの候補枠を表示します。候補枠の幅はテキストボックスの幅と同じにしています。また、位置は絶対座標での指定とし、テキストボックスの座標から計算して表示位置を決めています。
今回の例では候補の枠はul,liタグで記述しており、すでにulタグはHTMLファイル側に記述してあるため、候補のワードをliタグで書き出しています。候補のワードだけでなく、クリック時にCandidateItemClick()関数を呼び出すコードを合わせて出力しています。また、候補がなかった場合はポップアップ枠を表示しません。

CloseCompletePopup, CloseCompletePopupDelay 関数

  function CloseCompletePopupDelay(time) {
    setTimeout('CloseCompletePopup();', time);
  }

  function CloseCompletePopup() {
    var elem = document.getElementById("AutoCompleteList");
    elem.style.visibility = "hidden";
  }
ポップアップ枠のスタイルを"hidden"に変更し、枠を非表示にする関数です。CloseCompletePopupDelay関数は非表示にするまでのディレイ時間を指定できます。JavaScriptでの遅延処理の実行については「処理を遅延して実行する (一定時間経過後に処理を実行する)」の記事を参照してください。

CandidateItemClick() 関数

  function CandidateItemClick(ItemText) {
    var textBox = document.getElementById("Text1");
    textBox.innerText = ItemText;
    CloseCompletePopup();
  }
ポップアップ枠のオートコンプリートの候補をクリックしたときに呼び出される関数です。候補のワードをテキストボックスに設定します。

index.css

.AutoCompleteListFrame{
  border:solid 1px #000000;
  list-style:none;

  margin:0px;
  padding:0px;
  position:absolute;
  z-index:1000;
  width:200px;
  visibility:hidden;
}

.AutoCompleteListFrame li {
  margin:0px;
  padding:0px;
}

.AutoCompleteListFrame li:hover {
  background-color:#b8ceff;
}

解説

ポップアップ枠はul,liタグで記述しているため"list-style:none;"を指定し、見出しが表示されないようにします。また、マージンを0にしています。テキストボックスの直下に表示させるため、絶対座標"position:absolute;"を指定しています。マウスが候補の要素にフォーカスした際に背景色が変わるようli:hoverのスタイルで、マウスオーバーした際に背景色を変更するスタイルを記述しています。

実行結果

プロジェクトを実行しHTMLファイルを表示します。下図の画面が表示されます。


テキストボックスに文字を入力するとオートコンプリートの候補のポップアップ枠が表示されます。



複数候補がある場合は、複数の要素が表示されます。


マウスポインタを要素の上に移動させると背景色が変わります。


クリックして要素を選択するとポップアップ枠が消え、選択した要素の値がテキストボックスに反映されます。

補足: JavaScript内にTextBoxのIDを埋め込まないコード

複数のテキストボックスでオートコンプリートを用いる場合、TextBoxのIDをJavaScript内に埋め込まないほうが良い場合があります。TextBoxのIDを引数で渡す場合は以下のコードになります。

index.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
  <title></title>
  <link rel="stylesheet" type="text/css" href="default.css" />
  <script type="text/javascript">
    function getJson(TextBoxID) {
    //var xmlhttp = createXMLHttpRequest(); //旧バージョンのIEなどに対応する場合
    var xmlhttp = new XMLHttpRequest();

    xmlhttp.onreadystatechange = function () {
      if (xmlhttp.readyState == 4) {
        if (xmlhttp.status == 200) {
          var data = JSON.parse(xmlhttp.responseText);
          ShowCompletePopup(TextBoxID, data);
        } else {
          alert("status = " + xmlhttp.status);
        }
      }
    }
    var textBox = document.getElementById(TextBoxID);
    var textbox_text = textBox.value;
    if (textbox_text != "") {
      xmlhttp.open("GET", "AutoCompleteHandler.ashx?q=" + textbox_text);
      xmlhttp.send();
    } else {
      CloseCompletePopup();
    }
  }

  function createXMLHttpRequest() {
    if (window.XMLHttpRequest) { return new XMLHttpRequest() }
    if (window.ActiveXObject) {
      try { return new ActiveXObject("Msxml2.XMLHTTP.6.0") } catch (e) { }
      try { return new ActiveXObject("Msxml2.XMLHTTP.3.0") } catch (e) { }
      try { return new ActiveXObject("Microsoft.XMLHTTP") } catch (e) { }
    }
    return false;
  }

  function ShowCompletePopup(TextBoxID, data) {
    if (0 < data.Text.length) {
      var textBox = document.getElementById(TextBoxID);
      var textbox_width = textBox.offsetWidth;
      var textbox_left = textBox.offsetLeft;
      var textbox_bottom = textBox.offsetTop + textBox.offsetHeight;

      var elem = document.getElementById("AutoCompleteList");
      elem.style.width = textbox_width + "px";
      elem.style.left = textbox_left + "px";
      elem.offsetTop = textbox_bottom;
      elem.style.visibility = "visible";
      elem.innerHTML = "";

      for (i = 0; i < data.Text.length; i++) {
        elem.innerHTML += "<li onclick=\"CandidateItemClick('" + TextBoxID + "','" + data.Text[i] + "')\">" + data.Text[i] + "</li>";
      }
    }
  }

  function CloseCompletePopupDelay(time) {
    setTimeout('CloseCompletePopup();', time);
  }

  function CloseCompletePopup() {
    var elem = document.getElementById("AutoCompleteList");
    elem.style.visibility = "hidden";
  }

  function CandidateItemClick(TextBoxID, ItemText) {
    var textBox = document.getElementById(TextBoxID);
    textBox.innerText = ItemText;
    CloseCompletePopup();
  }
  </script>
</head>
<body>
  <div>
    <input id="Text1" type="text" onkeyup="getJson('Text1');" onblur="CloseCompletePopupDelay(500);" />
    <ul id="AutoCompleteList" class="AutoCompleteListFrame"></ul>
  </div>
</body>
</html>

補足: ASP.NET WebフォームのTextBoxに実装する場合

ASP.NETのWebフォームのTextBoxコントロールに実装する場合のコードを紹介します。
先のHTMLのテキストボックスでの実装とロジックは同じですが、ASP.NETのTextBoxにはonkeyup, onblurイベントをタグに直接記述できないため、コードビハインド側でAttributesプロパティにアクセスして属性を追加する必要があります。

コード

default.aspx

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="default.aspx.cs" Inherits="AutoCompleteDemo._default" %>

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
  <title></title>
  <link rel="stylesheet" type="text/css" href="default.css" />

<script type="text/javascript">
    function getJson() {
      //var xmlhttp = createXMLHttpRequest(); //旧バージョンのIEなどに対応する場合
      var xmlhttp = new XMLHttpRequest();
 
      xmlhttp.onreadystatechange = function () {
        if (xmlhttp.readyState == 4) {
          if (xmlhttp.status == 200) {
            var data = JSON.parse(xmlhttp.responseText);
            ShowCompletePopup(data);
          } else {
            alert("status = " + xmlhttp.status);
          }
        }
      }
      var textBox = document.getElementById("TextBox1");
      var textbox_text = textBox.value;
      if (textbox_text != "") {
        xmlhttp.open("GET", "AutoCompleteHandler.ashx?q=" + textbox_text);
        xmlhttp.send();
      } else {
        CloseCompletePopup();
      }
    }
 
    function createXMLHttpRequest() {
      if (window.XMLHttpRequest) { return new XMLHttpRequest() }
      if (window.ActiveXObject) {
        try { return new ActiveXObject("Msxml2.XMLHTTP.6.0") } catch (e) { }
        try { return new ActiveXObject("Msxml2.XMLHTTP.3.0") } catch (e) { }
        try { return new ActiveXObject("Microsoft.XMLHTTP") } catch (e) { }
      }
      return false;
    }

    function ShowCompletePopup(data) {
      if (0 < data.Text.length) {
        var textBox = document.getElementById("TextBox1");
        var textbox_width = textBox.offsetWidth;
        var textbox_left = textBox.offsetLeft;
        var textbox_bottom = textBox.offsetTop + textBox.offsetHeight;

        var elem = document.getElementById("AutoCompleteList");
        elem.style.width = textbox_width + "px";
        elem.style.left = textbox_left + "px";
        elem.offsetTop = textbox_bottom;
        elem.style.visibility = "visible";
        elem.innerHTML = "";

        for (i = 0; i < data.Text.length; i++) {
          elem.innerHTML += "<li onclick=\"CandidateItemClick('" + data.Text[i] + "')\">" + data.Text[i] + "</li>";
        }
      }
    }


    function CloseCompletePopupDelay(time) {
      setTimeout('CloseCompletePopup();', time);
    }

    function CloseCompletePopup() {
      var elem = document.getElementById("AutoCompleteList");
      elem.style.visibility = "hidden";
    }

    function CandidateItemClick(ItemText) {
      var textBox = document.getElementById("TextBox1");
      textBox.innerText = ItemText;
      CloseCompletePopup();
    }
  </script>

</head>
<body>
    <form id="form1" runat="server">
    <div>
      <asp:TextBox ID="TextBox1" runat="server" ></asp:TextBox>
      <ul id="AutoCompleteList" class="AutoCompleteListFrame"></ul>
    </div>
    </form>
</body>
</html>

default.aspx.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace AutoCompleteDemo
{
  public partial class _default : System.Web.UI.Page
  {
    protected void Page_Load(object sender, EventArgs e)
    {
      TextBox1.Attributes.Add("onblur", "CloseCompletePopupDelay(500);");
      TextBox1.Attributes.Add("onkeyup", "getJson();");

      //下記コードでも可
      //TextBox1.Attributes["onblur"] += "CloseCompletePopupDelay(500);";
      //TextBox1.Attributes["onkeyup"] += "getJson();";

    }
  }
}

default.css

.AutoCompleteListFrame{
  border:solid 1px #000000;
  list-style:none;

  margin:0px;
  padding:0px;
  position:absolute;
  z-index:1000;
  width:200px;
  visibility:hidden;
}

.AutoCompleteListFrame li {
  margin:0px;
  padding:0px;
}

.AutoCompleteListFrame li:hover {
  background-color:#b8ceff;
}

補足: TextBoxのIDをJavaScript内に埋め込まない場合

TextBoxのIDを引数で渡す場合はdefault.aspxファイルを以下に変更します。

default.aspx

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="default-opt.aspx.cs" Inherits="AutoCompleteDemo.default_opt" %>

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title></title>
  <link rel="stylesheet" type="text/css" href="default.css" />

  <script type="text/javascript">
    function getJson(TextBoxID) {
    //var xmlhttp = createXMLHttpRequest(); //旧バージョンのIEなどに対応する場合
    var xmlhttp = new XMLHttpRequest();

    xmlhttp.onreadystatechange = function () {
      if (xmlhttp.readyState == 4) {
        if (xmlhttp.status == 200) {
          var data = JSON.parse(xmlhttp.responseText);
          ShowCompletePopup(TextBoxID, data);
        } else {
          alert("status = " + xmlhttp.status);
        }
      }
    }
    var textBox = document.getElementById(TextBoxID);
    var textbox_text = textBox.value;
    if (textbox_text != "") {
      xmlhttp.open("GET", "AutoCompleteHandler.ashx?q=" + textbox_text);
      xmlhttp.send();
    } else {
      CloseCompletePopup();
    }
  }

  function createXMLHttpRequest() {
    if (window.XMLHttpRequest) { return new XMLHttpRequest() }
    if (window.ActiveXObject) {
      try { return new ActiveXObject("Msxml2.XMLHTTP.6.0") } catch (e) { }
      try { return new ActiveXObject("Msxml2.XMLHTTP.3.0") } catch (e) { }
      try { return new ActiveXObject("Microsoft.XMLHTTP") } catch (e) { }
    }
    return false;
  }

  function ShowCompletePopup(TextBoxID, data) {
    if (0 < data.Text.length) {
      var textBox = document.getElementById(TextBoxID);
      var textbox_width = textBox.offsetWidth;
      var textbox_left = textBox.offsetLeft;
      var textbox_bottom = textBox.offsetTop + textBox.offsetHeight;

      var elem = document.getElementById("AutoCompleteList");
      elem.style.width = textbox_width + "px";
      elem.style.left = textbox_left + "px";
      elem.offsetTop = textbox_bottom;
      elem.style.visibility = "visible";
      elem.innerHTML = "";

      for (i = 0; i < data.Text.length; i++) {
        elem.innerHTML += "<li onclick=\"CandidateItemClick('" + TextBoxID + "','" + data.Text[i] + "')\">" + data.Text[i] + "</li>";
      }
    }
  }

  function CloseCompletePopupDelay(time) {
    setTimeout('CloseCompletePopup();', time);
  }

  function CloseCompletePopup() {
    var elem = document.getElementById("AutoCompleteList");
    elem.style.visibility = "hidden";
  }

  function CandidateItemClick(TextBoxID, ItemText) {
    var textBox = document.getElementById(TextBoxID);
    textBox.innerText = ItemText;
    CloseCompletePopup();
  }
  </script>

</head>
<body>
    <form id="form1" runat="server">
    <div>
      <asp:TextBox ID="TextBox1" runat="server"></asp:TextBox>
      <ul id="AutoCompleteList" class="AutoCompleteListFrame"></ul>
    </div>
    </form>
</body>
</html>
著者
iPentecのメインプログラマー
C#, ASP.NET の開発がメイン、少し前まではDelphiを愛用
掲載日: 2014-06-10
iPentec all rights reserverd.