你一定有看過註冊會員或線上購物時用來輸入郵遞區號和地址的選單吧!例如你在第一個選單選擇了 "台北市",另一個選單的內容就會變成中正區、大安區、信義區... 等等。當你在第一個選單選擇了 "高雄市",另一個選單的內容又會變的不一樣。這樣的選單就稱為「兩層的連動選單」。
老方法是...
以往要製作這樣的選單,有兩種方法:一是先把全部的資料全部載入到頁面中,包括所有縣市和所有區鄉鎮,再用 Javascript 去動態更改選單的內容。這樣做的好處是當你選擇了第一個選單,第二個選單會馬上跟著變,速度很快。但如果全部的資料太多,在一開始頁面載入時就得等很久的時間了,網路流量也比較大。而且當瀏覽者只會選擇台北市的話,幾乎 90% 傳送過來的其他縣市資料都使用不到,似乎有點浪費頻寬。
另一種方式是:第一個選單選擇後,將選單資料送出到後端去查詢資料庫,再將第二個選單的內容送到瀏覽者的電腦中。這樣的好處是,網路傳送的資料比較少,而且幾乎只會傳送有用的資料。但是將資料送到後端,再等待接受資料,都是需要時間的,很有可能第一個選單選擇後,等了幾分鐘後第二個選單的內容才出現,而且頁面還得整個重新 reload 一次,感覺很 OOXX。
所以不是犧牲頻寬,就是犧牲時間。遇到這樣的問題,我們會喜歡折衷的方法,先載入第一個選單的內容就好,選擇後再跟後端要求資料,而且在要求資料的時候使用 Ajax 的技術,雖然還是需要時間等待,但至少不用整個換頁,整個流程感覺是順暢多了。
從 HTML 開始
首先在網頁中建立表單以及兩個下拉選單,HTML 碼如下:

第一個選單叫做 cid 是遊戲的分類,第二個選單叫做 gid 是各分類中遊戲的名稱。頁面一載入時會將所有遊戲分類都先載入進來,放在第一個選單中讓瀏覽者可以選擇分類。選擇了某一個分類後,第二個選單才會顯示該分類中的遊戲。
一開始時因為尚未選擇分類,所以第二個選單不需要有任何預先載入的內容,只留一個空的 "請選擇"。
先來看看全部完成後兩個選單的畫面應該是這樣的:
Ajax 通用函數
IE 和 Firefox 不僅在顯示頁面內容時有所差異,連 Ajax 所使用的物件也完全不一樣,這一直都是讓網頁設計師頭髮愈來愈白 (愈來愈少?) 的原因之一啊!在 Javascript 一開始的地方就先寫了一個函數 XMLHttpRequest() ,讓 IE 也能和 Firefox 一樣使用相同的方式來建立 Ajax 的物件,如此一來就不需要有兩種寫法了,十分方便。有興趣知道這個通用函數怎麼寫的,麻煩自行看原始碼,在這裡就不多做解釋了,基本上只要知道如何使用就可以了,anyway... 這個函數後面才會用到。
另外還有一個通用函數 addURLParam(),你只要提供一個 url、一個變數名稱和一個值,這個函數就會幫你把變數名稱和值附加到 url 的尾端,變成 GET 方式的 url 參數。如果你需要加入多個參數,可以多次使用 addURLParam(),每次會加入一個參數。
通常在建立 Ajax 物件跟後端伺服器要求資料時,都會帶不同的參數以便取得相關的資料,這時便能使用 addURLParam() ,將所需的參數(例如是從表單中取得的數值)附加到要求的 url 後面。稍後會再講解使用方法。
要開始囉...
處理選單的 Javascript 終於要上場了,首先是做一些初始化動作的 code :

這段程式碼做了幾件事:
- 使用 DOM 方法 document.getElementById() 取得這兩個選單,分別存放在 oCateList 和 oGameList 變數之中。
- 將 Ajax 跟後端要求資料的 url 存放在 sURLInit 變數中。
- 宣告一個全域變數 json 來存放 Ajax 呼叫後,伺服器所傳回的資料。稍後會使用到這個變數。
- 在還沒開始任何動作之前先將第二個選單 oGameList 停用,讓它無法選擇。因為一開始時第二個選單尚未有任何內容可選,就乾脆把它 disable 掉吧!
選單 onChange ... 送出 Ajax!
搞了這麼久,到底何時才要使用 Ajax 啊?相信你一定也期待很久了吧!當瀏覽者選擇了第一個選單的其中一個選項時,就必須送出 Ajax 呼叫去和後端要求第二個選單的內容資料。所以... 主要的 Ajax 動作是寫在選單的 onchange 事件中:

當選單被選擇之後,先檢查選擇的項目是不是第一項 "請選擇",利用 this.selectedIndex 是否等於 0 來檢查。如果是的話就將第二個選單的項目清空,變成只有 "請選擇",而且 disable 無法選取。你可能會想說,第二個選單一開始不就是只有 "請選擇" 而且被停用的狀態嗎?為何又要多此一舉呢?因為要考慮到一種情況:瀏覽者在第一個選單先選取了有效的項目,這時第二個選單便被啟用並且有內容。然後第一個選單又選回到第一個選項 "請選擇",這時是不是就該把第二個選單的內容清空並且停用了呢!第一個選單沒有選擇正確時,第二個選單當然不能動囉。
當第一個選單選擇了正確的項目,馬上利用自訂函數 addURLParam() 將所選取項目的值 this.options[this.selectedIndex].value 附加成 url 參數(參數名稱叫做 id),產生出完整的 Ajax 要求 url,存放在 sURL 變數中。addURLParam() 的用法為:
結果 url = addURLParam(原始 url,參數名稱,參數值)
接著是建立 Ajax 物件、送出要求的標準寫法。因為先前已經由自訂函數處理掉了 IE 和 Firefox 差異的問題,所以統一使用 XMLHttpRequest() 即可:
var oRequest = new XMLHttpRequest();
oRequest.open("get"、sURL、true);
oRequest.send(null);
在這裡還要特別注意一點,送出 Ajax 要求之後、接收到資料之前會有一段等待的 "空窗期",為了避免在這段期間內瀏覽者又重新選擇了第一個選單、又送出另一個要求,把事情搞的很複雜,所以這時最好把第一個選單暫時停用,等到 Ajax 資料接收回來後再重新啟用。這是非常重要的小技巧哦!!!
處理接收的資料
送出 Ajax 要求後,最重要的便是等待後端伺服器傳回來的資料了。資料傳送需要時間,所以不可能在送出要求(oRequest.send)的下一行程式碼就立刻接收資料。到底要等待多久也無法預估,畢竟網路速度快慢不一,所以只有一個方法,就是等資料送回來的時候自動通知我們。當 Ajax 資料送回來時會自動執行 onreadystatechange() 這個物件方法,因此我們可以將處理接收的資料的程式碼寫在這個方法裡面。
在上一段程式碼中 ...... 空出來的地方,就是要放置這個 onreadystatechange() 方法。這個方法必須寫在 send() 之前,在送出要求之前就必須先定義好該如何處理接收的資料:

其實不是只有接收到資料,只要 Ajax 物件的狀態有變更時,就會呼叫 onreadystatechange()。所以在 oRequest.onreadystatechange() 裡面,先檢查變更後的狀態 oRequest.readyState 是否為 4,4 的話才是接收到伺服器端的回應。而接收到回應後的第一件事則是把第一個選單給啟用,讓瀏覽者有機會可以選擇其他項目。
接收到的回應不一定就是我們想要的資料,也有可能是錯誤訊息,表示伺服器沒有回應、找不到 url 或是網路傳輸有問題。因此再檢查回應的狀態 oRequest.status, 如果值是 200 的話表示是正確的回應,可以從 oRequest.responseText 取得傳回的資料內容。除了 200 之外都是錯誤的回應,這時可以從 oRequest.statusText 取得錯誤訊息文字。
readyState 和 status 所代表狀態,意義是完全不同的哦!請不要搞混囉!
JSON
如果成功的接收到資料後,將資料轉換為 Javascript 認識的格式,存放在變數 json 中,以便稍後可以使用:
json = "json=" + oRequest.responseText;
json = eval(json);
由伺服器所傳回的這種資料格式稱為 JSON,它只是純文字,但長得像是下面這個樣子:

如果你對 Javascript 很熟悉的話,應該不難看出,其實 JSON 就是 Javascript 的物件陣列(由很多個物件所組成的陣列)。轉換後便能使用一般存取陣列的方式 json[1].text 或 json[2].value 來存取陣列中的值。
對於 Ajax 傳回的資料來說,最方便的就是 JSON 的格式,比起 XML 可是好多了。JSON 可以直接轉換成 Javascript 的陣列來使用,而 XML 還得另外寫一大堆程式來分析內容,因此伺服器端的程式(例如 ASP、PHP 或 ColdFusion )最好是將資料庫查詢出來的結果輸出成 JSON 的格式再傳回來。如果你想知道 PHP 如何產生出像範例這樣的 JSON 字串,可以用記事本開啟 json_list.php 這個檔案來看。
動態產生選單內容
接收到資料也轉換為可以使用的格式後,最終的目的便是要產生出第二個選單的內容。由於 JSON 已經被轉換為 Javascript 的陣列,因此跑一個迴圈輕輕鬆鬆地變成將每一筆資料變成選單中的項目:
for(var i=0,j=json.length;i < j;i++) {
oGameList.options[i+1] = new Option(json[i].text,json[i].value);
}
動態產生選單的項目有幾種方式:你或許知道 DOM 的方法 document.createElement() 和 appendChild(),但這種方式比較麻煩而且耗資源。也許你會想到用較不耗資源的 innerHTML,將select 標籤的 innerHTML 替換掉,便能更新選單項目了。但很抱歉的是,在 IE 中的 select 是不吃這一套的,會發生錯誤。另一種更簡潔有力而且通用的方式是:
新增項目:選單.options[i] = new Option(文字,值)
刪除項目:選單.options[i] = null
刪除所有項目:選單.options.length = 0
不賴吧!搭配個迴圈便能輕鬆的將第二個選單內容產生出來了。有了內容之後,別忘記將第二個選單啟用,讓選單能夠被選取。
※ 補充:在開始產生選單內容的 for 迴圈前面,必須多加上 oGameList.options.length = 0; 將選單內容先清空,不然可能會有先前的 "餘毒" 哦~
表單檢驗
選單的部分都完成了,但別忘記,反是表單送出之前都應該做表單檢驗的動作,萬一瀏覽者什麼都沒選、什麼都沒填寫就按下送出,一定要狠狠的給他警告一番。表單檢驗的程式碼加在 form 的 onsubmit 事件上:

這裡做了一個很簡單的檢查,只要第二個選單所選擇的項目不是第一項 "請選擇"(selectedIndex 為 0)即可。
在這裡還要注意一個小技巧,oGameList 是選單,屬於表單中的一個物件,只要寫 表單物件.form 便能抓到該物件所在的表單,不需要再大費周章的用 getElementById() 或 getElementsByTagName() 來取得表單了。
That's all !!
(這句話是跟某個老女人學的...
)