本週對 jQuery UI 的自動完成器進行了功能測試, 所謂的自動完成器 autocomplete 就是一個文字輸入框, 當你輸入第一個字元時 (預設), 它就會去比對資料來源, 並且把資料來源中含有該字元的資料顯示在輸入框下方出現的一個下拉式選單中, 這樣使用者就可以從中挑選要填入的資料, 這是對使用者相當友善的設計. 使用者在文字輸入框中輸入的字串有一個術語叫做 term (字眼).
自動完成器的承載容器是一個 input 元素 :
<input id="auto1" type="text" />
但接下來與其他 widget 不同的是, 自動完成器在呼叫 autocomplete() 函式時, 必須傳入 source 選項提供資料來源才行, 否則將因無資料可比對而無作用.
選項 source 有三種形式: 陣列, 字串, 或回呼函式. 首先介紹陣列形式, 這是資料來源為本地 (即程式本身所提供) 時用的形式. 這個陣列有兩種, 第一種
文字陣列, 如範例 1 所示 :
測試範例 1 : http://tony1966.xyz/test/jquerytest/autocomplete_1.htm [看原始碼]
範例 1 中我們放了三個 input 元素, 第一個用來輸入股票名稱, 後面兩個用來輸入股票號碼 (但第三個選項是錯誤的示範) :
var stock_name=["味王", "車王電", "葡萄王", "王品", "花王", "緯創", "創意"]; //正確的
var stock_id=["1203", "1533", "1707", "2727", "8906", "3231", "3443"]; //正確的
var stock_id_number=[1203, 1533, 1707, 2727, 8906, 3231, 3443]; //錯誤的
$("#auto1").autocomplete({source: stock_name});
$("#auto2").autocomplete({source: stock_id});
$("#auto3").autocomplete({source: stock_id_number});
當我們在 auto1 輸入 "王" 時, 自動完成器就會把含有 "王" 的股票名稱找出來作為下拉式選單的選項 (共五項), 輸入 "創" 則會找到兩個選項. 特別注意,
陣列的元素必須是文字, 不可以用數值, 否則選項將不會顯示, 這就是 auto3 測試所呈現的結果.
接著在範例 2 要介紹第二種陣列-物件陣列 (其實就是 JSON 格式之陣列), 陣列的每一個元素必須要有 label 與 value 兩個 key (即屬性), 其中
label 用來顯示於下拉式選單中的選項, 必須是字串; 而 value 則是使用者選取後填入 input 欄位中的值,
可以用數值, 但建議一律使用字串. 其次, 要介紹 delay, minlength 這兩個選項的用法. 選項 delay 是向 source 選項指定之資料來源發出請求或擷取動作之前的等待毫秒數. 預設為 300 毫秒 (0.3 秒). 因為透過網路取得取得非本地端資料來源時速度較慢, 利用 delay 延遲可以讓使用者多輸入幾個字元再送出請求可以縮小資料量. 對於本地資料 (陣列), 可以將此選項設為 0 以加快反應時間. 而 minLength 是設定要輸入多少字元才會觸發向 source 選項指定之資料來源擷取資料的動作. 預設值為 1. 當符合的資料量很多時, 可以利用此選項來限縮資料量至合理範圍. 設為 0 時, 只要隨意輸入一個字元再刪除, 就會觸發資料擷取動作,
顯示全部選項.
範例 2 中我們放置了兩個 input 元素 : auto1 與 auto2, 兩個自動完成器的物件陣列之 label 與 value 剛好顛倒, auto1 的 delay 與 minlength 都設為 0, 所以反應時間很快; 而 auto2 的 delay 設為 1000 (1 秒), 因此雖然是本地資料, 感覺反應較慢, 其 minLength 設為 2, 須輸入兩個字元才會觸發自動完成動作. 請注意,
如果是本地端提供的資料, 兩個屬性名稱有沒有加引號都可以, 亦即 label 與 value 或 "label" 與 "value" 均可. 但是,
如果資料是由遠端伺服器提供, 則屬性名稱務必加上引號. 請參考下面的範例 5.
測試範例 2 : http://tony1966.xyz/test/jquerytest/autocomplete_2.htm [看原始碼]
$(document).ready(function(){
var stock_name=[{"label":"味王", "value":1203},
{label:"車王電", value:"1533"},
{label:"葡萄王", value:"1707"},
{label:"王品", value:"2727"},
{label:"花王", value:"8906"},
{label:"緯創", value:"3231"},
{label:"創意", value:"3443"}];
var stock_id=[{label:"1101", value:"台泥"},
{label:"1102", value:"亞泥"},
{label:"1103", value:"嘉泥"},
{label:"2204", value:"中華"},
{label:"2207", value:"和泰車"},
{label:"2227", value:"裕日車"},
{label:"2206", value:"裕隆"}];
$("#auto1").autocomplete({source: stock_name,
delay: 0, minLength: 0});
$("#auto2").autocomplete({source: stock_id,
delay: 1000, minLength: 2});
從上面兩個範例可知, 字串陣列事實上是物件陣列的一種特例, 也就是說字串陣列是 label=value 的物件陣列, 亦即下列兩個是等效的 :
var stock_name=["味王", "車王電"];
var stock_name=[{label:"味王", value:"味王"}, {label:"車王電", value:"車王電"}];
其實物件陣列不一定同時需要 lable 與 value 這兩個屬性, 單獨一個屬性也是可以的, 只有 label 時, 表示 value 等於 label, 反之亦然, 總之,
其中一個屬性沒提供的話, 就會用另一個代替, 下列範例 3 我們就來測試看看 :
測試範例 3 : http://tony1966.xyz/test/jquerytest/autocomplete_3.htm [看原始碼]
在範例 3 中, 我們放了兩個 input 元素, 第一個 auto1 的物件陣列中, 只有一個屬性 label, 選取後之值就是 label; 而第二個 auto2 則只有一個屬性 value, 顯示的 label 就是 value.
var stock_name=[{label:"味王"},
{label:"車王電"},
{label:"葡萄王"},
{label:"王品"},
{label:"花王"},
{label:"緯創"},
{label:"創意"}];
var stock_id=[{value:"1101"},
{value:"1102"},
{value:"1103"},
{value:"2204"},
{value:"2207"},
{value:"2227"},
{value:"2206"}];
$("#auto1").autocomplete({source: stock_name});
$("#auto2").autocomplete({source: stock_id});
所以跟範例 1 效果一樣, 也就是說, 如果顯示用的 label 與設值的 value 一樣時, 其實用單純的文字陣列就可以了, 沒必要用物件陣列, 只有 "表裡不一" 時才需要用物件陣列, 例如下拉式選單顯示 "2330 (台積電)", 但選取後設定在輸入框的是 2330 (後端資料庫搜尋時的 key).
接著來介紹 source 選項為字串的用法. 這個字串是一個指向遠端伺服器的 URL 字串, 也就是說, 自動完成器內部對於 source 為 URL 的情況內建了一個 Ajax 機制, 向遠端伺服器要求提供資料作為下拉式選單選項的資料來源. 自動完成器會將使用者輸入的字串以名稱為 term 的網頁參數傳給伺服器, 後端的 PHP 程式可以用 $_GET["term"] 或 $_REQUEST["term"] 取得使用者輸入的字眼. 程式傳回的資料格式必須是 JSON, 以 PHP 而言, 可以將回應資料先存入字串陣列中, 最後再呼叫 json_encode() 函式即可轉成 JSON 格式.
在下面的範例 4 我們要測試從遠端資料庫中擷取股票名稱或代號, 顯示於自動完成器的下拉式選單中. 所以要先在 MySQL 中建立一個資料表 stock_list, 它只有兩個文字欄位 : stock_id 與 stock_name, 分別用來儲存股票代號與名稱. 建立此資料表的 SQL 指令如下 :
CREATE TABLE IF NOT EXISTS `stocks_list` (
`stock_id` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
`stock_name` varchar(255) COLLATE utf8_unicode_ci NOT NULL
PRIMARY KEY (`stock_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
然後把資料 INSERT 進去, 由於資料很多, 指令僅擷取片段說明 :
INSERT INTO `stocks_list` (`stock_id`, `stock_name`) VALUES
('0015', '富邦'),
('0050', '台灣50'),
('0051', '中100'),
.....
('9955', '佳龍'),
('9958', '世紀鋼');
完整的 SQL 指令檔可從下列 URL 下載 :
stocks_list.sql http://tony1966.xyz/test/jquerytest/stocks_list.sql
只要將此 SQL 指令檔灌進 (載入/輸入) MySQL 資料庫即可.
後端資料庫安排妥當後, 接下來看前端網頁與程式. 在網頁中我們放置了兩個 input 元素 : auto1 用來輸入股名; auto2 用來輸入股號 :
股名 : <input gt="" id="auto1" p="" type="text" />
股號 : <input gt="" id="auto2" p="" type="text" />
Javascript 程式部分, 我們設定兩個自動完成器的 source 屬性為遠端的 PHP 程式, 這兩個程式必須傳回 JSON 格式的回應 :
var url_1="http://tony1966.xyz/test/jquerytest/
get_stocks_name.php";
var url_2="http://tony1966.xyz/test/jquerytest/
get_stocks_id.php";
$("#auto1").autocomplete({source: url_1});
$("#auto2").autocomplete({source: url_2});
現在剩下兩個後端 PHP 程式了, 先來看一下 get_stocks_name.php, 先連線資料庫, 並取得自動完成器傳出之 term 參數 (即使用者輸入的字串) 以製作 SQL 查詢指令. 注意, 這裡用 LIKE '%大%' 條件來查詢 stock_name 欄位值含有 '大' 者. 然後呼叫 PHP 的 mysql_query 取得記錄集, 再呼叫 mysql_fetch_array() 函式來取得每一列, 再把其中的 stock_name 欄位值儲存在陣列中, 最後呼叫 json_encode() 函式把股名陣列轉成 JSON 格式字串傳回.
<?php
header('Content-Type: text/html;charset=UTF-8');
$host="mysql.1freehosting.com"; //MySQL 主機位址
$username="tony1966_test"; //MySQL 使用者名稱
$password="123456"; //MySQL 使用者密碼
$database="testdb"; //資料庫名稱
$conn=mysql_connect($host, $username, $password); //建立連線
mysql_query("SET NAMES 'utf8'"); //設定查詢所用之字元集為 utf-8
mysql_select_db($database, $conn); //開啟資料庫
$term=$_GET['term']; //擷取 jQueryUI 傳出參數 'term'
$SQL="
SELECT * FROM `stocks_list` WHERE `stock_name` LIKE '%".$term."%'";
$result=mysql_query($SQL, $conn); //執行 SQL 指令
$arr=Array(); //用來儲存 stock_name 欄位值之陣列
for ($i=0; $i
< $row=mysql_fetch_array($result); //取得列陣列
$arr[]=$row["stock_name"]; //放入陣列
} //end of for
echo json_encode($arr); //將陣列轉成 JSON 資料格式傳回
?>
另一個 PHP 程式 get_stocks_id.php 只要將 stock_name 改為 stock_id 即可.
測試範例 4 : http://tony1966.xyz/test/jquerytest/autocomplete_4.htm [看原始碼]
上述範例 4 是股票名稱與代號分開, 現在想要合而為一, 只需要一個文字框, 使用者可以輸入股名或股號, 都會觸發自動完成器的 Ajax 查詢動作, 而且選單會顯示例如 "2330 (台積電)", 選取後文字框會自動填入代號 2330, 這要怎麼做? 其實只要修改一下網頁與後端 PHP 查詢程式即可. 在下列範例 5 中, 網頁中只放置一個 input 元素 :
股名或股號 : <input gt="" id="auto1" p="" type="text" />
而後端 PHP 查詢程式則改為 get_stocks_list.php 如下 :
<?php
header('Content-Type: text/html;charset=UTF-8');
$host="mysql.1freehosting.com"; //MySQL 主機位址
$username="tony1966_test"; //MySQL 使用者名稱
$password="123456"; //MySQL 使用者密碼
$database="testdb"; //資料庫名稱
$conn=mysql_connect($host, $username, $password); //建立連線
mysql_query("SET NAMES 'utf8'"); //設定查詢所用之字元集為 utf-8
mysql_select_db($database, $conn); //開啟資料庫
$term=$_GET['term']; //擷取 jQueryUI 傳出參數 'term'
$SQL="
SELECT * FROM `stocks_list` WHERE `stock_id` LIKE '".$term.
"%' OR `stock_name` LIKE '".$term."%'";
$result=mysql_query($SQL, $conn); //執行 SQL 指令
$arr=Array(); //用來儲存 stock_id 欄位值之陣列
for ($i=0; $i < mysql_numrows($result); $i++) { //走訪紀錄集 (列)
$row=mysql_fetch_array($result); //取得列陣列
$label=$row["stock_id"]." (".$row["stock_name"].")";
$value=$row["stock_id"];
$stock=Array("label" => $label, "value" => $value);
$arr[]=$stock; //放入陣列
} //end of for
echo json_encode($arr); //將陣列轉成 JSON 資料格式傳回
?>
與前面範例 4 不同的是, 此處的 $SQL 指令在搜尋 stock_id 與 stock_name 時都是比對前面字元符合者, 例如 LIKE '2%' 會搜尋股號以 2 開頭者, 而 LIKE '%大%' 會搜尋股名中以 "大" 開頭者.
其次, 因為 label 與 value 要求 "表裡不一", 顯示 "2330 (台積電)", 選取後只填入 2330 為值, 因此 source 必須為物件陣列. 這樣在後端程式就必須以二維陣列來儲存 label 與 value, 第一維是索引式陣列, 第二維是關聯式陣列, 以 "label" 與 "value" 當作索引. 這樣在呼叫 json_encode() 函式之後就會轉換成物件陣列了. 上面曾提過, 如果資料來源為遠端伺服器, 則物件陣列的屬性必須加上雙引號 (亦即要用 "label" 與 "value", 而非 label 與 value 或 'label' 與 'value'), 否則會出現 Parse Error 而沒效果, 其實若是呼叫 json_encode(), 這部分不用擔心, 因為我們用關聯式陣列以字串做索引, 呼叫 json_encode() 時就會轉成字串屬性名稱, 而且是用雙引號. 例如搜尋股名中含有 "創" 者, 後端程式 get_stocks_list.php 會傳回如下結果 (共 2 筆) :
http://tony1966.xyz/test/jquerytest/get_stocks_list.php?term=%E5%89%B5
[{"label":"2451 (\u5275\u898b)","value":"2451"},{"label":"3443 (\u5275\u610f)","value":"3443"}]
可見 json_encode() 會把中文轉成字元實體. 關於 json_encode() 的使用, Tsung's Blog 有一篇文章可參考 : "PHP 讓 json_encode() 指定回傳格式".
測試範例 5 : http://tony1966.xyz/test/jquerytest/autocomplete_5.htm [看原始碼]
但一定要用 json_encode() 嗎? 那倒不一定, 其實土法煉鋼, 自己輸出符合要求的格式字串也行. 我們把上述範例 5 的後端程式 get_stocks_list.php 的後半部修改為下列, 存成 get_stocks_list_2.php 試試看 :
$arr=Array(); //用來儲存 stock_id 欄位值之陣列
for ($i=0;
$i < mysql_numrows($result); $i++) { //走訪紀錄集 (列)
$row=mysql_fetch_array($result); //取得列陣列
$label=$row["stock_id"]." (".$row["stock_name"].")";
$value=$row["stock_id"];
$arr[]=
'{"label":"'.$label.'","value":"'.$value.'"}'; //放入陣列
} //end of for
echo
"[".join(",", $arr)."]"; //輸出 JSON 格式字串
同樣向 get_stocks_list_2.php 查詢股名以 "創" 字開頭者, 它的回應如下 :
http://tony1966.xyz/test/jquerytest/get_stocks_list_2.php?term=%E5%89%B5
[{"label":"2451 (創見)","value":"2451"},{"label":"3443 (創意)","value":"3443"}]
這樣也是 OK 的, 請參考範例 6.
測試範例 6 : http://tony1966.xyz/test/jquerytest/autocomplete_6.htm [看原始碼]
當然如果想要像 json_encode() 那樣把中文改成字元實體也是可以的. 特別注意, 輸出 JSON 格式時, key 的名稱務必用雙引號, 如果把它改為單引號, 那就不行了, 我們只要改範例 6 的後端程式 get_stocks_list_2.php 的倒數第三行為如下, 存成 get_stocks_list_3.php :
$arr[]="{'label':'".$label."','value':'".$value."'}"; //放入陣列
這時向 get_stocks_list_3.php 查詢股名以 "創" 字開頭者, 可以發現回應的 key 都改為單引號了 :
http://tony1966.xyz/test/jquerytest/get_stocks_list_3.php?term=%E5%89%B5
[{'label':'2451 (創見)','value':'2451'},{'label':'3443 (創意)','value':'3443'}]
以這個回應做成範例 7 可知, 這樣是不行的 :
測試範例 7 : http://ㄒ/jquerytest/autocomplete_7.htm [看原始碼] (無效)
哇, 光是測試 source=url 就花了這麼多口水, 不過通過這些測試也了解了自動完成器的一些脾氣, 這些在官網卻沒提到. 總結一下 : source 是遠端伺服器時, 回應的 JSON 物件陣列中, 屬性名稱 label 與 value 務必要用雙引號括起來.
下面介紹 source 的第三種類型 : 回呼函式. 據書上說這是自動完成器最強的部份, 因為它的彈性最大, 可以透過回呼函式操控輸出結果. 這個回呼函式有兩個傳入參數 request 與 response, 使用者輸入的字眼 (term) 是透過 request 物件唯一的 term 屬性取得的 :
var term=request.term; //取得使用者輸入的查詢字串
然後函式要產生一個結果陣列 result, 此陣列可以是上述所說的文字陣列或 JSON 物件陣列. 然後呼叫 response() 把結果陣列傳回去就搞定了 :
response(result);
下面範例 8 我們就用一個超簡單功能來測試一下. 從以上測試結果可知, 若資料來源為本地陣列, 比對的方式是 "contains", 亦即陣列元素的 label 是否包含使用者輸入的字眼, 這相當於 SQL 的 LIKE '%字眼%' 條件. 如果要比對起始字串是否符合該怎麼辦呢呢? 哪是有這款的症頭, 不是打 "控八控控" 喔, 而是要把陣列交給回呼函式處理一下.
測試範例 8 : http://tony1966.xyz/test/jquerytest/autocomplete_8.htm [看原始碼]
範例 8 我們對股名與股號分別放了 input 元素 :
股名 (輸入 "中","台","創") : <input id="auto1" type="text" />
股號 (輸入 "1","2","3") : <input id="auto2" type="text" />
程式部分, 我們只列出 auto1 部分, auto2 的只要把 stock_name 改成 stock_id 即可. 選項 source 設為回呼函式, 並傳入兩個參數 request 與 response. 但這裡的重點是利用正規表達式物件 RegExt() 來過濾原始資料來源, 其中 "^" 是表示以後面的字串起頭之意. 注意, 因為正規式範本含有變數 term, 所以一定要用 RegExp() 產生範本物件, 沒辦法使用 var reg=/^ ... /i 方式.
var stock_name=["中華電","中碳","台新金","台積電","台塑","創見","創意"];
$("#auto1").autocomplete({source:
function(request, response) {
var term=request.term; //取得使用者輸入字串
var result=new Array();
var reg=new RegExp("^" + term,"i"); //以 term 開頭者
for (var i=0; i<stock_name.length; i++) {
var match=reg.test(stock_name[i]);
if (match) {result.push(stock_name[i]);}
}
response(result);
}
});
接下來我們把範例 8 合而為一改成範例 9, 只用一個 input 元素, 搜尋股名或股號均可 :
股名或股號 : <input gt="" id="auto1" p="" type="text" />
程式部分, 原始資料我們用物件陣列的方式呈現, 重點是比對符合後, 我們把新的 label (股號+股名) 以及 value 分別設定為物件屬性值, 再推入陣列中傳回 :
var data=[{label:"中華電", value:"2412"},
{label:"中碳", value:"1723"},
{label:"台新金", value:"3045"},
{label:"台積電", value:"2330"},
{label:"台塑", value:"1301"},
{label:"創見", value:"2451"},
{label:"創意", value:"3443"}];
$("#auto1").autocomplete({source:
function(request, response) {
var term=request.term;
var result=new Array();
var reg=new RegExp("^" + term,"i"); //以 term 開頭者
for (var i=0; i<data.length; i++) {
var match=reg.test(data[i].label) || reg.test(data[i].value);
if (match) {
var obj=new Object();
obj.label=data[i].label + ' (' + data[i].value + ')';
obj.value=data[i].value;
result.push(obj);
} //end of if
} //end of for
response(result);
} //end of function
});
測試範例 9 : http://tony1966.xyz/test/jquerytest/autocomplete_9.htm [看原始碼]
以上是以回呼函式處理 (過濾) 本地資料來源的方法. 當然也可以在回呼函式中用 Ajax 從遠端伺服器擷取資料回來後再處理. 不過這樣似乎有點多此一舉, 既然要從遠端伺服器取得資料, 何不將過濾處理的工作全部由伺服器一手包呢 (例如範例 4 我們就將 stock_id 以 term 起頭的過濾作業利用 SQL 完成)? 話是沒錯啦, 如果伺服器程式是自己寫的當然沒問題, 如果不是的話, 例如資料來自於公共資料庫, 你沒辦法要求它回應你要的格式時, 想要呈現客制化格式就要用回呼函式處理了.
我們用範例 10 來測試在回呼函式中用 Ajax 從遠端伺服器擷取資料回來後再處理的作法. 在範例 6 中, 後端 PHP 程式 get_stocks_list_2.php 會回應如下 JSON 資料 :
[{"label":"2451 (創見)","value":"2451"},{"label":"3443 (創意)","value":"3443"}]
現在假定後端程式只能回應如下資料 :
[{"label":"創見","value":"2451"},{"label":"創意","value":"3443"}]
亦即 label 中沒有附帶股號, 而且這是公用資料庫, 回應格式就是固定這樣沒法改, 我們該如何把取得之資料喬成跟上面有附股號的一樣呢? 先把 get_stocks_list_2.php 修改一下變成 get_stocks_list_4.php, 讓它只回應無股號之 JSON 資料 :
$term=$_GET['term']; //擷取 jQueryUI 傳出參數 'term'
$SQL="SELECT * FROM `stocks_list` WHERE `stock_id` LIKE '".$term.
"%' OR `stock_name` LIKE '".$term."%'";
$result=mysql_query($SQL, $conn); //執行 SQL 指令
$arr=Array(); //用來儲存 stock_id 欄位值之陣列
for ($i=0; $i < mysql_numrows($result); $i++) { //走訪紀錄集 (列)
$row=mysql_fetch_array($result); //取得列陣列
$label=$row["stock_name"];
$value=$row["stock_id"];
$arr[]='{"label":"'.$label.'","value":"'.$value.'"}'; //放入陣列
} //end of for
echo "[".join(",", $arr)."]";
而前端網頁程式部份, 我們就在選項 source 的回呼函式中, 以 Ajax 擷取 get_stocks_list_4.php 的回應資料加以處理, 如範例 10 所示 :
測試範例 10 : http://tony1966.xyz/test/jquerytest/autocomplete_10.htm [看原始碼]
未完待續 ...