通過JSF 2實現可重用的Ajax化組件
51CTO向讀者們具體介紹過Ajax支持包,以及利用“Ajax聽取JSF 2客戶端處理代碼中的事件和錯誤”。今天51CTO編輯向大家推薦這篇文章結合復合組件和Ajax輕而易舉地實現支持Ajax自定義組件。
關于本系列JSFfu系列建立在DavidGeary的同名簡介文章的概念的基礎之上。本系列將深入探究JSF2及其生態系統,同時還將介紹如何將一些JavaEE技術,如Contexts和DependencyInjection,與JSF相集成。
在本文中,我將向您介紹如何實現自動完成組件,它將使用Ajax管理其完成項列表。在此過程中,您將了解如何將Ajax集成到您自己的復合組件中。
本系列的代碼基于在企業容器,如GlassFish或Resin,中運行的JSF2。本文的最后一部分將詳細討論如何使用GlassFish來安裝和運行本文的代碼。
JSF自動完成自定義組件
因谷歌搜索字段而聞名的自動完成字段(也稱作建議框)是許多Web應用程序的組合。它們也是Ajax的典型應用。自動完成字段隨帶了許多Ajax框架,比如Scriptaculous和JQuery,如圖1—AjaxDaddy的自動完成組件集成(參閱參考資料)—所示:
圖1.AjaxDaddy自動完成組件
本文將討論如何使用JSF來實現支持Ajax的自動完成字段。您將了解如何實現如圖2所示的自動完成字段,其中將顯示一個簡短的虛擬國家列表(選自Wikipedia的“虛擬國家列表”一文;請參閱參考資料):
圖2.自動完成字段
#p#
圖3和圖4顯示了運行中的自動完成字段。在圖3中,在字段中輸入Al之后,國家列表將縮減至使用這兩個字母開頭的名稱:
圖3.使用Al開頭的完成項目
同樣,圖4顯示了在字段中輸入Bar之后顯示的結果。列表僅顯示以Bar開頭的國家名。
圖4.以Bar開頭的完成項目
使用自動完成組件
復合組件:基礎如果您不熟悉如何實現JSF2復合組件,那么您可以從“JSF2簡介,第2部分:模板及復合組件”這篇文章中了解基本知識。
.Locations自動完成字段是一個JSF復合組件,并應用于facelet,如清單1所示:
清單1.facelet
- <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
- "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
- <html xmlns="http://www.w3.org/1999/xhtml"
- xmlns:h="http://java.sun.com/jsf/html"
- xmlns:util="http://java.sun.com/jsf/composite/util">
- <h:head>
- <title>#{msgs.autoCompleteWindowTitle}</title>
- </h:head>
- <h:body>
- <div style="padding: 20px;">
- <h:form>
- <h:panelGrid columns="2">
- #{msgs.locationsPrompt}
- <util:autoComplete value="#{user.country}"
- completionItems="#{autoComplete.countries}" />
- </h:panelGrid>
- </h:form>
- </div>
- </h:body>
- </html>
清單1中的facelet通過聲明適當的名稱空間—util—以及借助組件的相關標記(<util:autoComplete>)來使用autoComplete復合組件。
注意清單1中<util:autoComplete>標記的兩個屬性:
•value是名稱為user的托管bean的國家屬性。
•completionItems是字段的完成項目的初始集。
User類是一個簡單的托管bean,專為本例而設計。其代碼如清單2所示:
#p#
清單2.User類
- package com.corejsf;
- import java.io.Serializable;
- import javax.inject.Named;
- import javax.enterprise.context.SessionScoped;
- @Named()
- @SessionScoped
- public class User implements Serializable {
- private String country;
- public String getCountry() { return country; }
- public void setCountry(String country) { this.country = country; }
- }
請注意@Named注釋,它與@SessionScoped一起實例化了一個名稱為user的托管bean,并在JSF第一次在facelet中遇到#{user.country}時將它置于session作用域中。此應用程序中唯一的#{user.country}引用發生在清單1中,其中,我將user托管bean的country屬性指定為<util:autoComplete>組件的值。
清單3顯示了AutoComplete類,該類定義了countries屬性,即自動完成組件的完成項目列表:
清單3.完成項目
- package com.corejsf;
- import java.io.Serializable;
- import javax.enterprise.context.ApplicationScoped;
- import javax.inject.Named;
- @Named
- @ApplicationScoped
- public class AutoComplete implements Serializable {
- public String[] getLocations() {
- return new String[] {
- "Abari", "Absurdsvanj", "Adjikistan", "Afromacoland",
- "Agrabah", "Agaria", "Aijina", "Ajir", "Al-Alemand",
- "Al Amarja", "Alaine", "Albenistan", "Aldestan",
- "Al Hari", "Alpine Emirates", "Altruria",
- "Allied States of America", "BabaKiueria", "Babalstan",
- "Babar's Kingdom","Backhairistan", "Bacteria",
- "Bahar", "Bahavia", "Bahkan", "Bakaslavia",
- "Balamkadar", "Baki", "Balinderry", "Balochistan",
- "Baltish", "Baltonia", "Bataniland, Republic of",
- "Bayview", "Banania, Republica de", "Bandrika",
- "Bangalia", "Bangstoff", "Bapetikosweti", "Baracq",
- "Baraza", "Barataria", "Barclay Islands",
- "Barringtonia", "Bay View", "Basenji",
- };
- }
- }
自動完成組件的使用方法已經介紹完畢。現在,您將了解它的工作原理。
#p#
自動完成組件的工作原理
自動完成組件是一個JSF2復合組件,因此,與大多數復合組件相同,它是在XHTML文件中實現的。組件包括一個文本輸入和一個列表框,以及一些JavaScript代碼。最開始,列表框style是display:none,其作用是讓列表框不可見。
自動完成組件響應三個事件:
◆文本輸入中的keyup事件
◆文本輸入中的blur(失焦)事件
◆列表框中的change(選擇)事件
當用戶在文本輸入中鍵入內容時,自動完成組件會調用每個keyup事件的JavaScript函數。該函數結合鍵盤輸入事件,以不大于350ms的間隔定期調用Ajax。因此,在響應文本輸入中的keyup事件時,自動完成組件會以不大于350ms的間隔定期向服務器發起Ajax調用。(其作用是防止快速輸入時的大量Ajax調用將服務器淹沒。在實踐中,結合事件的頻率可能會稍高,但這足以演示如何在JavaScript中結合事件,同時這是一個非常實用的工具。)
當用戶從列表框中選擇項目時,自動完成組件會向服務器發起另一個Ajax調用。
文本輸入和列表框都附加了監聽程序,它們在Ajax調用期間完成與服務器相關的大部分有意義的工作。在響應keyup事件時,文本輸入的監聽程序會更新列表框的完成項目。在響應列表框的選擇事件時,列表框的監聽程序會將列表框的選中項目復制到文本輸入中,并隱藏列表框。
現在,您已經了解了自動完成組件的工作原理。接下來,我們來看看它的具體實現。
實現自動完成組件
自動完成組件實現包括以下工件:
◆一個復合組件
◆一系列JavaScript函數
◆一個用于更新完成項目的值變更監聽程序
我將從清單4開始復合組件:
清單4.autoComplete組件
- <ui:composition xmlns="http://www.w3.org/1999/xhtml"
- xmlns:ui="http://java.sun.com/jsf/facelets"
- xmlns:f="http://java.sun.com/jsf/core"
- xmlns:h="http://java.sun.com/jsf/html"
- xmlns:composite="http://java.sun.com/jsf/composite">
- <!-- INTERFACE -->
- <composite:interface>
- <composite:attribute name="value" required="true"/>
- <composite:attribute name="completionItems" required="true"/>
- </composite:interface>
- <!-- IMPLEMENATION -->
- <composite:implementation>
- <div id="#{cc.clientId}">
- <h:outputScript library="javascript"
- name="prototype-1.6.0.2.js" target="head"/>
- <h:outputScript library="javascript"
- name="autoComplete.js" target="head"/>
- <h:inputText id="input" value="#{cc.attrs.value}"
- onkeyup="com.corejsf.updateCompletionItems(this, event)"
- onblur="com.corejsf.inputLostFocus(this)"
- valueChangeListener="#{autocompleteListener.valueChanged}"/>
- <h:selectOneListbox id="listbox" style="display: none"
- valueChangeListener="#{autocompleteListener.completionItemSelected}">
- <f:selectItems value="#{cc.attrs.completionItems}"/>
- <f:ajax render="input"/>
- </h:selectOneListbox>
- <div>
- </composite:implementation>
- </ui:composition>
清單4的實現部分完成了三項任務。首先,該組件發起Ajax調用以響應文本輸入中的keyup事件,并通過分配給文本輸入中的keyup和blur事件的JavaScript函數在文本輸入失焦時隱藏列表框。
其實,該組件通過JSF2的<f:ajax>標記來發起Ajax調用來響應列表框中的change事件。當用戶從列表框中進行選擇時,JSF會向服務器發起一個Ajax調用,并在Ajax調用返回時更新文本輸入的值。
在<div>中封裝復合組件清單4中的復合組件通過復合組件的客戶機標識符將其實現封裝在<div>中。這樣,其他組件便可通過其客戶機ID來引用自動完成組件。舉例來說,另一個組件可能會希望在Ajax調用期間執行或呈現一個或多個自動完成組件。
.第三,文本輸入和列表框都附加了相應的值變更監聽程序,因此當JSF發起Ajax調用來響應用戶在文本輸入中的鍵入操作時,JSF會調用服務器上的文本輸入的值變更監聽程序。當用戶從列表框中選擇項目時,JSF會向服務器發起一個Ajax調用并調用列表框的值變更監聽程序。
#p#
清單5顯示了自動完成組件所使用的JavaScript:
清單5.JavaScript
- if (!com)
- var com = {}
- if (!com.corejsf) {
- var focusLostTimeout
- com.corejsf = {
- errorHandler : function(data) {
- alert("Error occurred during Ajax call: " + data.description)
- },
- updateCompletionItems : function(input, event) {
- var keystrokeTimeout
- jsf.ajax.addOnError(com.corejsf.errorHandler)
- var ajaxRequest = function() {
- jsf.ajax.request(input, event, {
- render: com.corejsf.getListboxId(input),
- x: Element.cumulativeOffset(input)[0],
- y: Element.cumulativeOffset(input)[1]
- + Element.getHeight(input)
- })
- }
- window.clearTimeout(keystrokeTimeout)
- keystrokeTimeout = window.setTimeout(ajaxRequest, 350)
- },
- inputLostFocus : function(input) {
- var hideListbox = function() {
- Element.hide(com.corejsf.getListboxId(input))
- }
- focusLostTimeout = window.setTimeout(hideListbox, 200)
- },
- getListboxId : function(input) {
- var clientId = new String(input.name)
- var lastIndex = clientId.lastIndexOf(':')
- return clientId.substring(0, lastIndex) + ':listbox'
- }
- }
- }
清單5中的JavaScript包括三個函數,我把它們放置在com.corejsf名稱空間的內部。我實現了名稱空間(從技術上說是一個JavaScript字面對象),以防止其他人有意或無意修改我的三個函數。
如果這些函數未包含在com.corejsf中,則其他人可以實現自己的updateCompletionItems函數,從而將我的實現替換成它們。一些JavaScript庫可以實現一個updateCompletionItems函數,但最理想的情況是任何人都不用設計com.corejsf.updateCompletionItems。(相反,拋棄com,并使用corejsf.updateCompletionItems可能已經足夠,但有時會難以控制。)
因此,這些函數做了些什么?updateCompletionItems()函數向服務器發起Ajax請求—通過調用JSF的jsf.ajax.request()函數—要求JSF僅在Ajax調用返回時呈現列表框組件。updateCompletionItems()函數還傳遞了兩個額外的參數到jsf.ajax.request()中:列表框左上角的x和y坐標。jsf.ajax.request()函數會將這些函數參數轉換為通過Ajax調用發送的請求參數。
JSF會在文本輸入失焦時調用inputLostFocus()函數。該函數的作用是使用Prototype的Element對象來隱藏列表框。
updateCompletionItems()和inputLostFocus()將它們的功能存儲在一個函數中。然后,它們安排自己的函數分別在350ms和200ms時執行。換句話說,每個函數都有各自的任務,但它會讓任務延時350ms或200ms。文本輸入會在keyup事件后延時,因此,updateCompletionItems()方法會最多每隔350ms發送一個Ajax請求。其思想是,如果用戶輸入速度極快,則不會讓Ajax調用淹沒服務器。
inputLostFocus函數會在文本輸入失焦時調用,并延時其任務200ms。這種延時是必要的,因為該值會在Ajax調用返回時復制到列表框之外,并且列表框必須為可見才能確保它正常運行。
最后,請注意getListBoxId()函數。這個幫助器函數會從文本輸入的客戶機標識符中獲取列表框的客戶機標識符。該函數可以完成此任務,因為它將與清單4中的autoComplete組件相結合。autoComplete組件將input和listbox分別指定為文本框和列表框的組件標識符,因此getListBoxId()函數會刪除input并附加listbox,以便獲取文本輸入的客戶機標識符。
#p#
清單6顯示了監聽程序的最終實現:
清單6.監聽程序
- package com.corejsf;
- import java.io.Serializable;
- import java.util.ArrayList;
- import java.util.List;
- import java.util.Map;
- import javax.enterprise.context.SessionScoped;
- import javax.faces.component.UIInput;
- import javax.faces.component.UISelectItems;
- import javax.faces.component.UISelectOne;
- import javax.faces.context.FacesContext;
- import javax.faces.event.ValueChangeEvent;
- import javax.inject.Named;
- @Named
- @SessionScoped
- public class AutocompleteListener implements Serializable {
- private static String COMPLETION_ITEMS_ATTR = "corejsf.completionItems";
- public void valueChanged(ValueChangeEvent e) {
- UIInput input = (UIInput)e.getSource();
- UISelectOne listbox = (UISelectOne)input.findComponent("listbox");
- if (listbox != null) {
- UISelectItems items = (UISelectItems)listbox.getChildren().get(0);
- Map<String, Object> attrs = listbox.getAttributes();
- List<String> newItems = getNewItems((String)input.getValue(),
- getCompletionItems(listbox, items, attrs));
- items.setValue(newItems.toArray());
- setListboxStyle(newItems.size(), attrs);
- }
- }
- public void completionItemSelected(ValueChangeEvent e) {
- UISelectOne listbox = (UISelectOne)e.getSource();
- UIInput input = (UIInput)listbox.findComponent("input");
- if(input != null) {
- input.setValue(listbox.getValue());
- }
- Map<String, Object> attrs = listbox.getAttributes();
- attrs.put("style", "display: none");
- }
- private List<String> getNewItems(String inputValue, String[] completionItems) {
- List<String> newnewItems = new ArrayList<String>();
- for (String item : completionItems) {
- String s = item.substring(0, inputValue.length());
- if (s.equalsIgnoreCase(inputValue))
- newItems.add(item);
- }
- return newItems;
- }
- private void setListboxStyle(int rows, Map<String, Object> attrs) {
- if (rows > 0) {
- Map<String, String> reqParams = FacesContext.getCurrentInstance()
- .getExternalContext().getRequestParameterMap();
- attrs.put("style", "display: inline; position: absolute; left: "
- + reqParams.get("x") + "px;" + " top: " + reqParams.get("y") + "px");
- attrs.put("size", rows == 1 ? 2 : rows);
- }
- else
- attrs.put("style", "display: none;");
- }
- private String[] getCompletionItems(UISelectOne listbox,
- UISelectItems items, Map<String, Object> attrs) {
- Strings] completionItems = (String[])attrs.get(COMPLETION_ITEMS_ATTR);
- if (completionItems == null) {
- completionItems = (String[])items.getValue();
- attrs.put(COMPLETION_ITEMS_ATTR, completionItems);
- }
- return completionItems;
- }
- }
JSF在Ajax調用期間調用監聽程序的valueChanged()方法來響應文本輸入中的keyup事件。該方法會創建一組新的完成項目,然后將列表框的項目設置為這個新的項目集。該方法還會設置列表框的樣式屬性,以確定Ajax調用返回時是否顯示列表框。
清單6中的setListboxStyle()方法將使用x和y請求我在發起清單5中的Ajax調用時指定的參數值。
JSF會在Ajax調用期間調用監聽程序的其他公共方法completionItemSelected(),以響應列表框中的選擇事件。該方法會將列表框的值復制到文本輸入中,并隱藏列表框。
請注意,valueChanged()方法還會將原始完成項目存儲在列表框的某個屬性中。由于每個autoComplete組件都維護自己的完成項目列表,因此多個autoComplete組件可以在相同頁面中和諧共存,而不會影響彼此的完成項目。
#p#
使用GlassFish和Eclipse運行示例
本系列中的代碼適合在JEE6容器中運行,比如GlassFish或Resin。您可以通過調整讓它們適應servlet容器,但這并非理想方案。因此,我的目標是側重于充分發揮JSF2和JEE6的潛力,而不是配置問題。我仍然堅持使用GlassFishv3。
在本文的其余部分,我將向您展示如何使用GlassFishv3和Eclipse來運行本文的示例代碼。此處的說明還適用于本系列其他文章的代碼。(我將使用Eclipse3.4.1,因此最好是使用與之相近的版本。)
圖5展示了本文代碼的目錄結構。其中的autoComplete目錄包含應用程序和一個空的Eclipse工作空間目錄。
圖5.本文下載部分的源代碼
現在,您已經下載了代碼,接下來就可以運行它了。首先,您需要GlassFishEclipse插件,可從https://glassfishplugins.dev.java.net下載它,如圖6所示:
圖6.GlassFishEclipse插件
請依照插件的安裝說明操作。
要安裝本文的代碼,請在Eclipse中創建一個DynamicWeb項目。為此,可以通過File>New菜單來實現:如果未看到DynamicWebProject,那么請選擇Other,并在接下來的對話框中打開Web文件夾并選擇DynamicWebProject,如圖7所示:
圖7.創建一個DynamicWeb項目
#p#
下一步是配置項目。在NewDynamicWebProject向導的第一個頁面中做出以下選擇,如圖8所示:
1.在Projectcontents下,保留Usedefault框為未選中狀態。在Directory字段中,輸入(或瀏覽到)示例代碼的autoComplete目錄。
2.對于TargetRuntime,請選擇GlassFishv3JavaEE6。
3.對于DynamicWebModuleversion,請輸入2.5。
4.對于Configuration,請選擇DefaultConfigurationforGlassFishv3JavaEE6。
5.在EARMembership下,保留AddprojecttoanEAR框為未選中狀態,并在EARProjectName:字段中輸入autoCompleteEAR。
圖8.配置應用程序,步驟1
單擊Next,然后輸入如圖9所示的值:
1.對于ContextRoot:,輸入autoComplete。
2.對于ContentDirectory:,輸入web。
3.對于JavaSourceDirectory:,輸入src/java。保留Generatedeploymentdescriptor框為未選中狀態。
圖9.配置應用程序,步驟2
現在,您應該已經建立了一個autoComplete項目,它將顯示在Eclipse的ProjectExplorer視圖中,如圖10所示:
圖10.autoComplete項目
#p#
現在,選擇項目,右鍵單擊它并選擇RunonServer,如圖11所示:
圖11.使用Eclipse在服務器上運行
從RunOnServer對話框的服務器列表中選擇GlassFishv3JavaEE6,如圖12所示:
圖12.選擇GlassFish
#p#
單擊Finish。Eclipse應該會相繼啟動GlassFish和autoComplete應用程序,如圖13所示:
13.在Eclipse中運行
結束語
借助JSF2,開發人員可以輕松地創建功能強大、支持Ajax的自定義組件。您不需要在XML中實現基于Java的組件或呈現器,或者集成第三方JavaScript來發起Ajax調用。借助JSF2,您只需要使用幾乎與任何JSF2facelet視圖相同的標記來創建一個復合組件,并根據需求添加一些JavaScript或Java代碼,以及voilà—您將實現一個奇妙自定義組件,為應用程序用戶提供極為方便的數據輸入功能。
在JSFfu的下一期中,我將討論實現Ajax化JSF自定義組件的更多方面,比如將<f:ajax>標記集成到您的自定義組件中,以參與其他人發起的Ajax。
【編輯推薦】