内容


JSF 2 简介

复合组件最佳实践

实现可扩展的自定义组件

Comments

系列内容:

此内容是该系列 # 部分中的第 # 部分: JSF 2 简介

敬请期待该系列的后续内容。

此内容是该系列的一部分:JSF 2 简介

敬请期待该系列的后续内容。

JSF 是一个基于组件的框架,这意味着它可以提供您所需要的基础架构来实现您自己的组件。JSF 2 提供一种简单的方法来实现自定义的复合组件。

在前面的文章中,我已经向您展示了几个复合组件的实现(见“模板及复合组件”,“Ajax 组件” 和 “后来添加的 Ajax 复合组件”)。在本文中,我将通过展示 5 个使用 JSF 2 实现复合组件的最佳实践来结束该主题 — 以及 JSF 2 fu 系列:

  1. 将您的组件封装在一个 DIV 中。
  2. 合并 JavaScript 和 Ajax。
  3. 使用 JavaScript 闭包在一个页面中支持多个组件。
  4. 允许页面创建者定制您的组件。
  5. 国际化您的组件。

为了更好地理解这些最佳实践,我将讨论如何应用一个简单复合组件的实现。

可编辑输入复合组件

文本的示例组件是一个可编辑的输入复合组件。 所示的应用程序使用两个可编辑输入复合组件,一个用于输入名字,另一个输入姓氏:

图 1. 可编辑文本组件
从上到下显示名字编辑序列的 3 个屏幕截图
从上到下显示名字编辑序列的 3 个屏幕截图

从上到下, 中的这 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 组件的文件系统层次结构和文件夹的屏幕截图

显示的是 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 个最佳实践的角度来讨论可编辑输入的实现。

将组件封装在一个 DIV 中

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 按钮一样。

合并 JavaScript 和 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 闭包

当您实现复合组件时,您必须在一个页面上提供多个组件。当一个组件的所有实例共享同一个 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>

Facets

在 中,错误消息显示在页面的底部,这是因为我正在使用 Development 项目阶段,JSF 自动在底部添加验证错误。您不能告知这个错误是和哪个可编辑输入有关系的,因此您最好将错误消息放在违法组件旁边,如 所示:

图 4 . 使用 facets
图 4 . 使用 facets
图 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

正如我在 “后来添加的 Ajax 复合组件” 中所介绍的,您可以使用 <composite:clientBehavior> 标记来让页面创建者添加 Ajax 功能到您的复合组件。 显示了一个对话框,在用户单击 edit... 按钮时监控所发生的 Ajax 调用:

图 5. 监控 Ajax 请求
监控 Ajax 请求
监控 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 之后,自定义组件不再是自定义组件架构开发人员的专利。在本文中,我向您介绍了一些实现复合组件的最佳实践。只需要一点点改动,您就可以使您的组件对页面创建者来说是易于扩展的。


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology, Web development
ArticleID=659416
ArticleTitle=JSF 2 简介: 复合组件最佳实践
publish-date=05172011