TECH::EXPERT【43日目】

【学習内容】

・インクリメンタルサーチ

【インクリメンタルサーチ】

いつぞやの記事でインクリメンタルサーチは後日やります!と宣言してかれこれ2週間くらい放置していました。

たまたまチームの方がこのパートの学習を進めていたのと、chat-spaceの実装でもうすぐインクリさんが登場するみたいなので、ここらで1回負債を返済しておきます。

なお、理解度を深めるために1行1行翻訳をつけていきたいと思います。

<HTML>

<!DOCTYPE html>
<html>
 <head>
   <meta charset="utf-8" />
   <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
   <script src="main.js"></script>
   <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
   <link rel="stylesheet" type="text/css" href="style.css">
 </head>
 <body>
   <div class="form-group">
     <input type='text' id="keyword" class="form-control" placeholder="好きなフルーツを入力してください">
     <button type="button" id="submit" class="btn">検索</button>
   </div>
   <p id="result"></p>
 </body>
</html>

id = “keyword”は検索窓です。
id = “result”は結果を表示する場所です。
id = “submit”送信ボタンは送信ボタンです。

①検索ボタンを押した時に入力されたものを表示しよう
<main.js>

$(function() {
 $("#submit").on("click", function() {
   var input = $("#keyword").val();
   $("#result").text(input);
 });
});

<翻訳>
送信ボタンをクリックすると、
検索窓に入力された文字列の値を変数inputに代入して、
結果を表示する場所にinputの情報をテキスト形式で表示します。


②入力された値と配列の中の要素と一致したものを表示しよう
var fruitsの中にたくさんの果物が入っているので、検索窓に入力した値と果物の名前が一致した場合、結果を表示する。というプログラムです。

<main.js>

var fruits = ['apple', 'apricot', 'avocado', 'blueberry', 'cherry', 'coconut', 'cranberry', 'dragonfruit', 'durian', 'grape', 'grapefruit', 'guava', 'kiwi fruit', 'lemon', 'lime', 'lychee', 'mango', 'melon', 'watermelon', 'miracle fruit', 'orange', 'bloodorange','clementine','mandarine','tangerine','papaya','passionfruit','peach','pear','persimmon','physalis','plum/prune','pineapple','pomegranate','raspberry','rambutan','star fruit','strawberry'];
$(function() {
 $("#submit").on("click", function() {
   var input = $("#keyword").val();
   $.each(fruits, function(i, fruit) {
     if (input === fruit) {
       $("#result").text(input);
       return false;
     } else {
       $("#result").text("一致する果物はありませんでした。");
     }
   });
 });
});

<翻訳>

送信ボタンをクリックすると、
検索窓に入力された文字列の値を変数inputに代入します。
配列の要素を一つずつ取り出してそれぞれ比較します。
もし、検索窓に入力された文字列の値と、fruitの名前が一致したら、
結果を表示する場所にinputの情報をテキスト形式で表示します。
表示できたらfalseで処理をやめます。
もし、検索窓に入力された文字列の値と、fruitの名前が一致しなかった場合は、
結果を表示する場所に一致する果物はありませんでした。と表示します。


③一部の言葉で検索できるようにしよう

ここからは検索結果が複数個表示される可能性があるので、ulタグを使用して検索結果をliタグで表示します。

<!DOCTYPE html>
<html>
 <head>
   <meta charset="utf-8" />
   <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
   <script src="main.js"></script>
   <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
   <link rel="stylesheet" type="text/css" href="style.css">
 </head>
 <body>
   <div class="form-group">
     <input type='text' id="keyword" class="form-control" placeholder="好きなフルーツを入力してください">
     <button type="button" id="submit" class="btn">検索</button>
   </div>
   <ul id="list"></ul>
 </body>
</html>


<main.js>

var fruits = ['apple', 'apricot', 'avocado', 'blueberry', 'cherry', 'coconut', 'cranberry', 'dragonfruit', 'durian', 'grape', 'grapefruit', 'guava', 'kiwi fruit', 'lemon', 'lime', 'lychee', 'mango', 'melon', 'watermelon', 'miracle fruit', 'orange', 'bloodorange','clementine','mandarine','tangerine','papaya','passionfruit','peach','pear','persimmon','physalis','plum/prune','pineapple','pomegranate','raspberry','rambutan','star fruit','strawberry'];
$(function() {
 var list = $("#list");
 function appendList(word) {
   var item = $('<li class="list">').append(word);
   list.append(item);
 }
 $("#submit").on("click", function() {
   var input = $("#keyword").val();
   var reg = new RegExp("^" + input);
   $(".list").remove();
   $.each(fruits, function(i, fruit) {
     if (fruit.match(reg)) {
       appendList(fruit);
     }
   });
   if ($(".list").length === 0) {
     appendList("一致する果物はありませんでした");
   }
 });
});


<翻訳>

送信ボタンをクリックすると、
検索窓に入力された文字列の値を変数inputに代入します。
正規表現で前方一致で検索できるように、文字列の頭に”^”をつけて、var regに代入します。
検索結果は一度リフレッシュします。(前の検索結果を削除するため)

配列の要素を1つ1つ出して、それぞれの要素と、ver regに一致するものがあるか調べます。
一致したものがあった場合はappendListというメソッドを発動します。(引数は配列の要素とマッチした要素だけです)

appendListの内容は、マッチした要素(fruit)をwordとして、id = “list”の中に新規でliタグを追加します。
(liが1行増えるということです)

もし、一致したものがなかった場合は、一致する果物はありませんでしたと表示します。


④複数の言葉で検索できるようにしよう

var fruits = ['apple', 'apricot', 'avocado', 'blueberry', 'cherry', 'coconut', 'cranberry', 'dragonfruit', 'durian', 'grape', 'grapefruit', 'guava', 'kiwi fruit', 'lemon', 'lime', 'lychee', 'mango', 'melon', 'watermelon', 'miracle fruit', 'orange', 'bloodorange','clementine','mandarine','tangerine','papaya','passionfruit','peach','pear','persimmon','physalis','plum/prune','pineapple','pomegranate','raspberry','rambutan','star fruit','strawberry'];
$(function() {
 var list = $("#list");
 function appendList(word) {
   var item = $('<li class="list">').append(word);
   list.append(item);
 }
 function editElement(element) {
   var result = "^" + element;
   return result;
 }
 $("#submit").on("click", function() {
   var input = $("#keyword").val();
   var inputs = input.split(" ");
   var newInputs = inputs.map(editElement);
   var reg = RegExp(newInputs.join("|"));
   $(".list").remove();
   $.each(fruits, function(i, fruit) {
     if (fruit.match(reg)) {
       appendList(fruit);
     }
   });
   if ($(".list").length === 0) {
     appendList("一致する果物はありませんでした");
   }
 });
});


<解説>
ここでは例えば「ap k」と入力された文字列を最終的に「^ap ^k」として「apから始まる要素」と「kから始まる要素」にマッチする単語を表示させます。

そのために「ap k」を” ”(スペース)で区切って、「ap」と「k」という2つの要素にした後、それぞれの頭に「^」をつけて「^ap」と「^k」にします。

そのあと、2つの要素を再度くっつけて「^ap^k」という1つの要素にしています。

<翻訳>
送信ボタンをクリックすると、
検索窓に入力された文字列の値を変数inputに代入します。

入力された文字列を” ”(スペース)で区切って2つの要素に分けた結果をinputsに代入します。(複数個あるからinputsです)

.mapメソッドを使って2つにわけた要素それぞれにeditElementを適用します。

editElementの役割は、それぞれの要素の頭に「^」をくっつける役割です。

頭に「^」をくっつけた2つの要素をnewInputsに代入します。(編集後のinputsだからnewinputsです)

その後、.joinを使って2つの要素を1つにがっちゃんこします。(「^ap^k」みたいに1つになります)

そのあと正規表現にかけて、もし配列fruitと一致したらlistに加えます。

もし、一致するものがなかったら、一致する果物はありませんでしたと表示します。


⑤複数の言葉で検索できるようにしよう
このステップがインクリメンタルサーチの本髄です。

大きく4ステップに分かれるので、順番に説明していきます。

STEP1 値が入力されるたびに配列から一致する要素がないか検索できるようにする

var fruits = ['apple', 'apricot', 'avocado', 'blueberry', 'cherry', 'coconut', 'cranberry', 'dragonfruit', 'durian', 'grape', 'grapefruit', 'guava', 'kiwi fruit', 'lemon', 'lime', 'lychee', 'mango', 'melon', 'watermelon', 'miracle fruit', 'orange', 'bloodorange','clementine','mandarine','tangerine','papaya','passionfruit','peach','pear','persimmon','physalis','plum/prune','pineapple','pomegranate','raspberry','rambutan','star fruit','strawberry'];
$(function() {
 var list = $("#list");
 function appendList(word) {
   var item = $('<li class="list">').append(word);
   list.append(item);
 }
 function editElement(element) {
   var result = "^" + element;
   return result;
 }
 $("#keyword").on("keyup", function() {
   var input = $("#keyword").val();
   var inputs = input.split(" ");
   var newInputs = inputs.map(editElement);
   var reg = RegExp(newInputs.join("|"));
   $(".list").remove();
   $.each(fruits, function(i, fruit) {
     if (fruit.match(reg)) {
       appendList(fruit);
     }
   });
   if ($(".list").length === 0) {
     appendList("一致する果物はありませんでした");
   }
 });
});

<解説>
イベント発火の条件を変えるだけなので、翻訳は割愛します。
いままでは送信ボタンを押したら、結果が表示されるという内容のプログラムでしたが、文字を入力するたびに発火させたいので、


$("#keyword").on("submit", function()

から

$("#keyword").on("keyup", function()

に変更します。


STEP2 文字を入力しないキーを押した時は処理を実行しないようにしよう

var fruits = ['apple', 'apricot', 'avocado', 'blueberry', 'cherry', 'coconut', 'cranberry', 'dragonfruit', 'durian', 'grape', 'grapefruit', 'guava', 'kiwi fruit', 'lemon', 'lime', 'lychee', 'mango', 'melon', 'watermelon', 'miracle fruit', 'orange', 'bloodorange','clementine','mandarine','tangerine','papaya','passionfruit','peach','pear','persimmon','physalis','plum/prune','pineapple','pomegranate','raspberry','rambutan','star fruit','strawberry'];
$(function() {
 var list = $("#list");
 var preWord;
 function appendList(word) {
   var item = $('<li class="list">').append(word);
   list.append(item);
 }
 function editElement(element) {
   var result = "^" + element;
   return result;
 }
 $("#keyword").on("keyup", function() {
   var input = $("#keyword").val();
   var inputs = input.split(" ");
   var newInputs = inputs.map(editElement);
   var word = newInputs.join("|");
   var reg = RegExp(word);
   $(".list").remove();
   if (word != preWord) {
     $.each(fruits, function(i, fruit) {
       if (fruit.match(reg)) {
         appendList(fruit);
       }
     });
     if ($(".list").length === 0) {
       appendList("一致する果物はありませんでした");
     }
   }
   preWord = word;
 });
});

<解説>
新たにwordとpreWordという変数を用紙しました。

preWordは一時的に入力した文字を保管しておく仮置き場所のようなものです。

例えばユーザーが検索窓に「a」と打ったときにはaから始まるfruitがたくさん表示されます。

そのあとユーザーがもう1文字「p」を打ったとしたら検索窓には「ap」という文字列が入ります。

この時点です、文字列「a」は既に過去の文字列で、「ap」が最新の文字列になります。
このときの「a」をpreWord、「ap」をwordとします。

つまり、ユーザーが1文字打つごとにpreWordとwordは更新され続けるということです。

   if (word != preWord) {
     $.each(fruits, function(i, fruit) {
       if (fruit.match(reg)) {
         appendList(fruit);
       }
     });

つまり、ここの処理はユーザーが文字が新しく打ったかどうかを判定していて、ユーザーが新たに1文字入力した場合もしくは1文字削除した場合に、この中身を実行します。
(処理の中身はこれまでと一緒なので割愛します)

STEP3 入力された値が削除されてなくなった時に要素が表示されないようにしよう

var fruits = ['apple', 'apricot', 'avocado', 'blueberry', 'cherry', 'coconut', 'cranberry', 'dragonfruit', 'durian', 'grape', 'grapefruit', 'guava', 'kiwi fruit', 'lemon', 'lime', 'lychee', 'mango', 'melon', 'watermelon', 'miracle fruit', 'orange', 'bloodorange','clementine','mandarine','tangerine','papaya','passionfruit','peach','pear','persimmon','physalis','plum/prune','pineapple','pomegranate','raspberry','rambutan','star fruit','strawberry'];
$(function() {
 var list = $("#list");
 var preWord;
 function appendList(word) {
   var item = $('<li class="list">').append(word);
   list.append(item);
 }
 function editElement(element) {
   var result = "^" + element;
   return result;
 }
 $("#keyword").on("keyup", function() {
   var input = $("#keyword").val();
   var inputs = input.split(" ");
   var newInputs = inputs.map(editElement);
   var word = newInputs.join("|");
   var reg = RegExp(word);
   $(".list").remove();
   if (word != preWord && input.length !== 0) {
     $.each(fruits, function(i, fruit) {
       if (fruit.match(reg)) {
         appendList(fruit);
       }
     });
     if ($(".list").length === 0) {
       appendList("一致する果物はありませんでした");
     }
   }
   preWord = word;
 });
});

<解説>
if (word != preWord && input.length !== 0) に変化しています。
このまま読むと、入力フォームの中身が更新された時かつ、何かしらの文字列が入っている、つまり入力された文字が0になった場合のみ処理を実行します。


STEP4 スペースが入力された時にすべての要素が表示されないようにしよう

var fruits = ['apple', 'apricot', 'avocado', 'blueberry', 'cherry', 'coconut', 'cranberry', 'dragonfruit', 'durian', 'grape', 'grapefruit', 'guava', 'kiwi fruit', 'lemon', 'lime', 'lychee', 'mango', 'melon', 'watermelon', 'miracle fruit', 'orange', 'bloodorange','clementine','mandarine','tangerine','papaya','passionfruit','peach','pear','persimmon','physalis','plum/prune','pineapple','pomegranate','raspberry','rambutan','star fruit','strawberry'];
$(function() {
 var list = $("#list");
 var preWord;
 function appendList(word) {
   var item = $('<li class="list">').append(word);
   list.append(item);
 }
 function editElement(element) {
   var result = "^" + element;
   return result;
 }
 $("#keyword").on("keyup", function() {
   var input = $("#keyword").val();
   var inputs = input.split(" ").filter(function(e) { return e; });
   var newInputs = inputs.map(editElement);
   var word = newInputs.join("|");
   var reg = RegExp(word);
   if (word != preWord) {
     $(".list").remove();
     if(input.length !== 0) {
       $.each(fruits, function(i, fruit) {
         if (fruit.match(reg)) {
           appendList(fruit);
         }
       });
       if ($(".list").length === 0) {
         appendList("一致する果物はありませんでした");
       }
     }
   }
   preWord = word;
 });
});

<解説>

これまで手順どおりに進めていくと、検索窓に「a」⇒「a 」(a,スペース)⇒「a k」と入力した際、スペースを打った時だけ全てのフルーツが表示されます。(その後、kを打つと戻ります)

この現象を回避するために下記コードを追記します。

var inputs = input.split(" ").filter(function(e) { return e; });

上記のコードは[“a”,””,”k”]という文字列が送られたときに””だけ取り除き、[“a”,”k”]とする処理です。

なぜこんなことができるのかというと、filterは引数の条件式の中の値がfalseの場合は破棄して、値がtrueのもののみ取り出して新たな配列を形成することができるからです。

では、なぜ””がfalseなのかというと、JavaScriptにおいては””は問答無用でfalseだからです。(ここばかりはそういうルールなのだと捉えるしかないです)

これで解決です。

と、思いきやまた新たに問題が発生します。

今度はスペースが入力されたときに全ての要素が消えるようになりました。

なぜ全て消えるのかというと、上の処理でスペースは文字としてカウントしないようにフィルターをかけたので、「a」⇒「a 」(a+スペース)と打ったとしても、スペースはノーカウントの扱いなので、word!=preWordの条件式に当てはまらなくなりました。

(下記箇所が原因です)

   if (word != preWord) {
     $(".list").remove();

対応策として、検索する文字が増えても減っても対応できるようにもう1つ条件式を重ねます。

if(input.length !== 0)

このコードは入力した文字が0でない場合に処理を実行する
⇒つまり、なんかしらの文字が入力されれば処理を実行するという意味になります。

スペースはfalseでノーカウントだけれども、なんかしらの文字ではあるので、文字を表示しますという内容です。(多分)

スペースはノーカウントだけれども、たしかにここには存在するので、文字数としてはカウントするといった解釈がよろしいかと思います。

これで完成です。

【所感】

インクリメンタルサーチ難しすぎですね。

最後の部分は自分の都合の良いように解釈しましたが、9割方はあっていると思います。

最後の部分についてわかりやすい解説できる方募集しています。。

この記事が気に入ったらサポートをしてみませんか?