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。
【编辑推荐】