JSF 是一个基于组件的框架,这意味着它可以提供您所需要的基础架构来实现您自己的组件。JSF 2 提供一种简单的方法来实现自定义的复合组件。
在前面的文章中,我已经向您展示了几个复合组件的实现(见“模板及复合组件”,“Ajax 组件” 和 “后来添加的 Ajax 复合组件”)。在本文中,我将通过展示 5 个使用 JSF 2 实现复合组件的最佳实践来结束该主题 — 以及 JSF 2 fu 系列:
为了更好地理解这些最佳实践,我将讨论如何应用一个简单复合组件的实现。
文本的示例组件是一个可编辑的输入复合组件。 所示的应用程序使用两个可编辑输入复合组件,一个用于输入名字,另一个输入姓氏:
图 1. 可编辑文本组件
从上到下, 中的这 3 个屏幕截图显示了名字的编辑序列:
- 最上面的截图显示应用程序的初始状态,edit... 按钮位于 First name: 和 Last name: 标签的右边。
- 中间截图显示了当用户单击了 First name: 后边的 edit... 按钮,并在文本输入区域输入
Roger之后应用程序的状态。一个 done 按钮出现在文本输入区右边。 - 最下面的屏幕截图显示了当用户单击了 done 按钮之后应用程序的状态。现在出现了 First name: Roger,在其右边有一个 edit... 按钮。
接下来,我们将讨论如何使用这个可编辑输入组件,并向您显示如何实现。之后,我将就组件的实现逐个讨论这 5 个最佳实践。
使用可编辑输入组件和使用其他任何 JFS 组件一样:声明适当的名称空间,然后对复合组件使用 JFS 生成的标记。 说明了 中所示的页面的两个步骤以及标签:
清单 1. 使用
<util:inputEditable>
<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>Implementing custom components</title>
</h:head>
<h:body>
<h:form>
<h:panelGrid columns="2">
First name:
<util:inputEditable id="firstName"
value="#{user.firstName}"/>
Last name:
<util:inputEditable id="lastName"
value="#{user.lastName}"/>
</h:panelGrid>
</h:form>
</h:body>
</html> |
为了完整起见, 显示了 引用的 user bean 的实现:
清单 2.
User bean
package com.corejsf;
import java.io.Serializable;
import javax.inject.Named;
import javax.enterprise.context.SessionScoped;
@Named("user")
@SessionScoped
public class UserBean implements Serializable {
private String firstName;
private String lastName;
public String getFirstName() { return firstName; }
public void setFirstName(String newValue) { firstName = newValue; }
public String getLastName() { return lastName; }
public void setLastName(String newValue) { lastName = newValue; }
}
|
现在您已经看到了如何使用可编辑输入组件,我将向您演示它的实现。
可编辑输入组件是在 resources/util 目录下的 inputEditable.js、inputEditable.properties 和 inputEditable.xhtml 文件中实现的,如 中的文件系统层次结构所示:
图 2. 组件的文件
显示的是 inputEditable.xhtml:
清单 3.
inputEditable 组件的标记(inputEditable.xhtml)
<!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:f="http://java.sun.com/jsf/core"
xmlns:composite="http://java.sun.com/jsf/composite">
<composite:interface>
<composite:attribute name="text"/>
<composite:editableValueHolder name="text" targets="editableText" />
<composite:actionSource name="editButton" targets="editButton" />
<composite:actionSource name="doneButton" targets="doneButton" />
<composite:clientBehavior name="edit" event="action" targets="editButton"/>
<composite:clientBehavior name="done" event="action" targets="doneButton"/>
<composite:facet name="textMessage"/>
</composite:interface>
<composite:implementation>
<h:outputScript library="javascript" name="prototype.js" target="head"/>
<h:outputScript library="javascript" name="scriptaculous.js" target="head"/>
<h:outputScript library="javascript" name="effects.js" target="head"/>
<h:outputScript library="util" name="inputEditable.js" target="head"/>
<div id="#{cc.clientId}">
<h:outputText id="text" value="#{cc.attrs.text}"/>
<h:commandButton id="editButton" type="button"
value="#{cc.resourceBundleMap.editButtonText}"
onclick="this.startEditing()"/>
<h:inputText id="editableText" value="#{cc.attrs.text}" style="display: none"/>
<h:commandButton id="doneButton"
value="#{cc.resourceBundleMap.doneButtonText}" style="display: none">
<f:ajax render="text textMessage" execute="editableText"
onevent="ajaxExecuting"/>
</h:commandButton>
<h:panelGroup id="textMessage">
<composite:renderFacet name="textMessage"/>
</h:panelGroup>
</div>
<script> com.clarity.init('#{cc.clientId}'); </script>
</composite:implementation>
</html> |
该标记创建了 4 个组件,最初只有两个 — 文本和编辑按钮 — 是可见的。当用户单击 edit 按钮时,应用程序调用 com.clarity.startEditing() JavaScript 函数,其实现如 所示:
清单 4.
inputEditable 组件的 JavaScript 函数(inputEditable.js)
package com.corejsf;
var com = {};
if (!com.clarity) {
com.clarity = {
init: function (ccid) {
var mydiv = document.getElementById(ccid);
mydiv.editButton = $(mydiv.id + ':editButton');
mydiv.text = $(mydiv.id + ':text');
mydiv.editableText = $(mydiv.id + ':editableText');
mydiv.doneButton = $(mydiv.id + ':doneButton');
mydiv.doneButton.offsetLeft = mydiv.editButton.offsetLeft;
mydiv.editButton.startEditing = function() {
mydiv.text.fade( { duration: 0.25 } );
mydiv.editButton.fade( { duration: 0.25 } );
window.setTimeout( function() {
mydiv.editableText.appear( { duration: 0.25 } );
mydiv.doneButton.appear( { duration: 0.25 } );
window.setTimeout( function() {
mydiv.editableText.focus();
}, 300);
}, 300);
};
},
toggleDisplay: function(element) {
element.style.display = element.style.display == "none" ? "" : "none";
},
ajaxExecuting: function(data) {
var mydiv = $(data.source.parentNode);
if (data.status == 'complete') {
toggleDisplay(mydiv.editableText);
toggleDisplay(mydiv.doneButton);
toggleDisplay(mydiv.text);
toggleDisplay(mydiv.editButton);
}
}
}
}
|
startEditing() 函数使用源自 Scriptaculous 框架的 fade() 和 appear() 方法(见 参考资料)。它也使用了一对计时器来确保以正确的次序淡入淡出。注意 startEditing() 函数最终也给了文本输入焦点。
显示了 inputEditable.properties:
清单 5
inputEditable 组件的属性文件(inputEditable.properties)editButtonText=edit... doneButtonText=done |
接下来,我将从 5 个最佳实践的角度来讨论可编辑输入的实现。
JFS 在创建一个复合组件时,它创建了一个框架称之为命名容器 的组件,这包括复合组件中的所有组件。这个命名容器不能生成标记;相反,JFS 为复合组件中的每一个组件生成标记。结果是页面标记不能通过其组件 ID 将组件作为一个整体引用,因为默认情况下没有带有此 ID 的组件。
您可以为页面创建者提供引用复合组件的能力,在您执行该组件时将其封装在一个 DIV 中。比如,假设您想要页面标记来引用一个可编辑的输入组件作为一个 Ajax 调用的一部分。在下列标记中,我将添加一个 Ajax 按钮来处理名字的输入。单击按钮使 Ajax 对服务器进行调用,其中名字输入已被处理;Ajax 调用返回时,JFS 呈现输入内容:
<h:form>
<h:panelGrid columns="2">
First name:
<util:inputEditable id="firstName"
value="#{user.firstName}"/>
Last name:
<util:inputEditable id="lastName"
value="#{user.lastName}"/>
<h:commandButton value="Update first name">
<f:ajax execute="firstName" render="firstName">
</h:commandButton>
</h:panelGrid>
</h:form>
|
前面标记中的 Ajax 按钮正常工作,因为在 中我将组件封装在一个 DIV 中:
<div id="#{cc.clientId}">
...
</div>
|
用于 DIV 的标识符是复合组件自身的客户端标识符。因此,页面创建者可以将复合组件作为一个整体引用,和我之前讨论的 Ajax 按钮一样。
在本文的静态截图中,您看不到当用户单击 edit... 按钮时可编辑输入组件执行的淡出动画。淡出是由 Scriptaculous 框架最后完成的。在 中,我使用 <h:outputScript> 标记输出 Scriptaculous 所需的 JavaScript,在
中,我使用框架的 fade() 和 appear() 方法来完成想要的动画效果:
mydiv.editButton.startEditing = function() {
mydiv.text.fade( { duration: 0.25 } );
mydiv.editButton.fade( { duration: 0.25 } );
window.setTimeout( function() {
mydiv.editableText.appear( { duration: 0.25 } );
mydiv.doneButton.appear( { duration: 0.25 } );
window.setTimeout( function() {
mydiv.editableText.focus();
}, 300);
}, 300);
};
|
在前面的 JavaScript 中嵌套的计时器确保动画中的一切按部就班地出现。例如,我会推迟将输入文本设为焦点直至我确定输入出现在屏幕上;否则,我将在输入显示之前调用 focus(),调用将不起作用。
通过 JFS 使用第三方 JavaScript 框架(比如 Scriptaculous 或 jQuery)很容易。您可以在页面输出适当的 JavaScript,然后在您的 JavaScript 中使用该框架。
用户单击 done 按钮时,可编辑输入组件也可使用 Ajax 对服务器进行调用:
<h:commandButton id="doneButton"
value="#{cc.resourceBundleMap.doneButtonText}" style="display: none">
<f:ajax render="text textMessage" execute="editableText"
onevent="ajaxExecuting"/>
</h:commandButton>
|
在上述标记中,当用户单击 done 按钮时,我使用 JSF 的 <f:ajax> 标记来进行一次 Ajax 调用。此 Ajax 调用在服务端执行文本输入,当 Ajax 调用返回时更新文本和文本消息。
当您实现复合组件时,您必须在一个页面上提供多个组件。当一个组件的所有实例共享同一个 JavaScript 时,您必须小心操作用户目前正在与之交互的那个组件。
您可以用几种方法在一个页面上支持多个组件。一个方法(Oracle 工程师 Jim Driscoll 在其博客上关于一个类似可编辑输入组件所讨论的)是维护组件 ID 的名称空间(见 参考资料),另一个方法是使用 JavaScript 闭包:
com.clarity = {
init: function (ccid) {
var mydiv = document.getElementById(ccid);
mydiv.editButton = $(mydiv.id + ':editButton');
mydiv.text = $(mydiv.id + ':text');
mydiv.editableText = $(mydiv.id + ':editableText');
mydiv.doneButton = $(mydiv.id + ':doneButton');
mydiv.doneButton.offsetLeft = mydiv.editButton.offsetLeft;
mydiv.editButton.startEditing = function() {
mydiv.text.fade( { duration: 0.25 } );
mydiv.editButton.fade( { duration: 0.25 } );
window.setTimeout( function() {
mydiv.editableText.appear( { duration: 0.25 } );
mydiv.doneButton.appear( { duration: 0.25 } );
window.setTimeout( function() {
mydiv.editableText.focus();
}, 300);
}, 300);
};
},
|
这是用于可编辑输入组件的 JavaScript, 中显示一个整体。init() 函数被每个可编辑输入组件所调用,正如您在 的底部所看到的。给定组件封装的 DIV 客户端标识符,我得到该特定 DIV 的一个引用。因为 JavaScript 是一个动态语言,允许您在运行时向对象添加属性和方法,我向 DIV 中添加了对回调函数中所需的所有元素的引用。
我也向组件的 edit 按钮添加了一个 startEditing() 方法。当用户单击 edit... 时,调用此方法:
<h:commandButton id="editButton" type="button"
value="#{cc.resourceBundleMap.editButtonText}"
onclick="this.startEditing()"/>
|
当 startEditing() 函数被调用时,mydiv 变量保持在 init() 方法调用时的原始值。这是关于 JavaScript 闭包最酷的(在某种程度上是 Java 内部类)。init() 和 startEditing() 之间花多少时间没有关系,init() 是否在它们之间被多次调用也无关紧要 — 当 startEditing() 被调用时,mydiv 的值正是当 init() 方法被具体组件调用时其自身的复制。
因为 JavaScript 闭包会保留周围函数变量的值,您要确保每个 startEditing() 都访问其组件的适当 DIV。
在您的组件定义中,通常是纯粹的一两行 XML,您可以让页面创建者定制您的组件。定制复合组件的 3 个主要方法是:
- 添加验证器、转换器和监听器
- Facets
- Ajax
在您公布这些内部组件的时候,可以允许页面创建者将验证器、转换器和监听器添加到您的复合组件中的组件中。例如, 显示了添加到一个可编辑输入组件的验证器:
图 3. 验证名字字段
显示了一个错误消息,表明该字段至少需要 10 个字符。页面创建者向名字可编辑输入组件的文本添加了一个验证器,像这样:
<util:inputEditable id="firstname" text="#{user.firstName}">
<f:validateLength minimum="10" for="text"/>
</util:inputEditable>
|
注意 <f:validateLength> 标记的 for 属性。它通知 JSF 验证器是针对可编辑输入组件内的文本 的。JSF 知道该文本,因为我将其暴露在组件的实现中:
<composite:interface>
...
<composite:editableValueHolder name="text" targets="editableText" />
...
</composite:interface>
<composite:implementation>
...
<h:inputText id="editableText" value="#{cc.attrs.text}" style="display: none"/>
...
</composite:implementation>
|
在 中,错误消息显示在页面的底部,这是因为我正在使用 Development 项目阶段,JSF 自动在底部添加验证错误。您不能告知这个错误是和哪个可编辑输入有关系的,因此您最好将错误消息放在违法组件旁边,如 所示:
图 4 . 使用 facets
页面创建者可以通过向组件添加一个 facet 来实现:
<util:inputEditable id="firstname" text="#{user.firstName}">
<f:validateLength minimum="10" for="text"/>
<f:facet name="textMessage">
<h:message for="editableText" style="color: red"/>
</f:facet>
</util:inputEditable>
|
此 facet 是组件所支持的:
<composite:interface>
<composite:facet name="textMessage"/>
</composite:interface>
<div id="#{cc.clientId}">
<h:panelGroup id="textMessage">
<composite:renderFacet name="textMessage"/>
</h:panelGroup>
...
</div>
|
正如我在 “后来添加的 Ajax 复合组件” 中所介绍的,您可以使用 <composite:clientBehavior> 标记来让页面创建者添加 Ajax 功能到您的复合组件。 显示了一个对话框,在用户单击 edit... 按钮时监控所发生的 Ajax 调用:
图 5. 监控 Ajax 请求
页面作者仅向组件添加了一个 <f:ajax>,然后指定一个 Ajax 函数,它的 onevent 属性是一个 JavaScript 函数(该函数显示对话框),在 Ajax 调用处理过程中调用:
<util:inputEditable id="firstname" text="#{user.firstName}">
<f:validateLength minimum="10" for="text"/>
<f:facet name="textMessage">
<h:message for="editableText" style="color: red"/>
</f:facet>
<f:ajax event="edit" onevent="monitorAjax"/>
</util:inputEditable>
|
页面创建者可以添加一个 <f:ajax> 标记到组件,因为我已经在组件的实现中公布了这个客户端行为:
<composite:interface> <composite:clientBehavior name="edit" event="action" targets="editButton"/> </composite:interface> |
可编辑输入组件在它的两个按钮上显示文本(edit... 和 done) 。因为组件可能被多个地方的用户所用,因此必须被国际化和本地化。
为了本地化一个组件文本,只需在同一个目录下添加一个属性文件作为组件
,然通过这个表达式来访问属性文件中的密钥:
#{cc.resourceBundleMap.KEY},其中 KEY 是
属性文件中的密钥。正如您在 和 中所看到的,至此我已经为可编辑输入组件按钮的文本进行了本地化。
JSF 1 使实现组件很困难,因此多数 JFS 开发人员放弃了使用它。有了 JFS 2 之后,自定义组件不再是自定义组件架构开发人员的专利。在本文中,我向您介绍了一些实现复合组件的最佳实践。只需要一点点改动,您就可以使您的组件对页面创建者来说是易于扩展的。
学习
- JSF 网站:找到更多关于 JSF 开发的参考资料。
-
Scriptaculous:您可以使用 JFS 将它和其他第三方 JavaScript 框架集成。
-
“Another JSF 2.0 Ajax Component: Editable Text”(Jim Driscoll,java.net,2008 年 11 月):Driscoll 采用另一个方法来实现一个可编辑输入复合组件。
-
developerWorks Java 技术专区:这里有数百篇关于 Java 编程各个方面的文章。
获得产品和技术
- JSF:下载 JSF 2.0。
讨论
- 加入 developerWorks 中文社区。查看开发人员推动的博客、论坛、组和维基,并与其他 developerWorks 用户交流。
David Geary 是一名作家、演讲家和顾问,也是 Clarity Training, Inc. 的总裁,他指导开发人员使用 JSF 和 Google Web Toolkit (GWT) 实现 Web 应用程序。他是 JSTL 1.0 和 JSF 1.0/2.0 专家组的成员,与人合作编写了 Sun 的 Web Developer 认证考试的内容,并且为多个开源项目作出贡献,包括 Apache Struts 和 Apache Shale。David 的 Graphic Java Swing 一直是关于 Java 的畅销书籍,而 Core JSF(与 Cay Horstman 合著)是关于 JSF 的畅销书。他还是 GWT Solutions 一书的作者。David 经常在各大会议和用户组发表演讲。他从 2003 年开始就一直是 NFJS tour 的定期演讲人,并且在 Java University 教授课程,三次当选为 JavaOne 之星。