答案以复合/自定义组件的形式出现,以及“AutoCompleteUtils”类,它有点像组件的支持 bean。组件的复合部分将用于生成组件的 html,并根据需要将属性值传递给 AutoCompleteUtils。 AutoCompleteUtils 将主要用于调用您将传递给组件的 bean 中的方法。组件的自定义部分实际上只需要通过 validate(FacesContext context) 方法验证值。
我将首先解释组件的基础知识,然后在最后包含一个完整的示例,其中包含所有的花里胡哨。
基础知识
第一步是声明组件及其资源。因此,在组件项目的 taglib.xml 文件中,您需要:
<tag>
<tag-name>autoCompleteInput</tag-name>
<component>
<resource-id>
components/autoCompleteInput.xhtml
</resource-id>
</component>
</tag>
在你的 faces-config.xml 中:
<component>
<component-type>AutoCompleteInput</component-type>
<component-class>base.ui.jsf.components.AutoCompleteInput</component-class>
</component>
<managed-bean>
<managed-bean-name>autoCompleteUtils</managed-bean-name>
<managed-bean-class>base.ui.jsf.components.utils.AutoCompleteUtils</managed-bean-class>
<managed-bean-scope>view</managed-bean-scope>
</managed-bean>
这里声明 AutoCompleteUtils 是因为发现在这种情况下使用通常的@ManagedBean @ViewScoped 注解不起作用(虽然老实说我不确定为什么不这样做)。
我认为了解组件如何工作的最佳方式是立即跳到页面上查看它将如何实现。如果我们以用户列表为例,它看起来像这样:
<namespace:autoCompleteInput id="selectUser"
value="#{beanName.userId}">
<f:attribute name="searchListener" value="#{beanName.searchUsers}" />
<f:attribute name="existingItemLoader" value="#{beanName.loadExistingUser}" />
</namespace:autoCompleteInput>
如您所见,方法通过 f:attribute 标签传递给组件。 searchListener 是在每次按键时调用以执行搜索的方法。它接受单个字符串参数(要搜索的术语),并返回一个包含找到的匹配项的列表(其中 label 属性是要在页面上显示的值,value 属性是要提交给 bean 的值)。
如果该组件将用作必须从现有记录加载数据的可编辑表单的一部分,则还需要一个 existingItemLoader。
existingItemLoader 是您的支持 bean 中的一个方法,它接受单个 String 参数(现有项目的值),
并返回项目标签的字符串。由设计者编写为指定值检索适当标签的逻辑。
所以在你的 bean 中,你必须有:
public List<SelectItem> searchUsers(String searchTerm)
{
List<SelectItem> selectItems = new ArrayList<SelectItem>();
// Some action which retrieves a list of matches:
// Sort the list of matches here if desired, as the component does no sorting
for (UserObject user: matchingUsers)
{
selectItems.add(new SelectItem(user.getUserId(), user.getFullName()));
}
return selectItems;
}
public String loadExistingUser(String userId)
{
int id = -1;
String label = "";
// Some action which retrieves the label for the item:
return label;
}
现在要真正从组件中调用这些方法,我们需要在 AutoCompleteUtils 中有一个“中间人”方法。那是因为我们想在输入上使用 f:ajax 来执行匹配的搜索,但它不能直接调用监听器。所以 ajax 监听器将是 AutoCompleteUtils 中的一个方法,然后它会从 bean 调用 searchListener。所以在复合组件中,会有这样的东西(现在是简化版):
<h:inputText id="labelInput"
value="#{autoCompleteUtils.itemLabel}"
label="#{cc.attrs.label}"
autocomplete="off">
<f:ajax event="keyup" listener="#{autoCompleteUtils.itemLabelChange}" render="matches">
</h:inputText>
<h:selectOneListbox id="matches">
<f:selectItems value="#{autoCompleteUtils.selectItems}"/>
</h:selectOneListbox>
<h:inputHidden id="valueInput"
binding="#{cc.valueInput}"
value="#{cc.attrs.value}" />
输入的值被传递给 AutoCompleteUtils 以便它可以被传递给 searchListener 以便它知道要搜索表单的术语。它将返回匹配列表,该列表存储在 AutoCompleteUtils 中,并提供给 selectOneListbox。当用户从列表框中选择一个项目时,该项目的值将被放入隐藏输入,其值是提交给您的 bean 的值(之前指定为 value="#{beanName.userId}")。
现在,要实际调用我们的 searchListener,我们在 AutoCompleteUtils 中有以下内容:
public void itemLabelChange(AjaxBehaviorEvent event)
{
selectItems = new ArrayList<SelectItem>();
ValueExpression searchListener = null;
AutoCompleteInput input = (AutoCompleteInput) up(event.getComponent(), UIInput.class);
searchListener = input.getValueExpression("searchListener");
if (searchListener != null)
{
Class<?>[] paramTypes = {String.class};
Object[] paramValues = {itemLabel};
selectItems = (List<SelectItem>) invokeMethod(FacesContext.getCurrentInstance(), searchListener.getExpressionString(), paramValues, paramTypes);
}
}
private Object invokeMethod(FacesContext context, String expression, Object[] params, Class<?>[] paramTypes)
{
ExpressionFactory eFactory = context.getApplication().getExpressionFactory();
ELContext elContext = context.getELContext();
MethodExpression method = eFactory.createMethodExpression(elContext, expression, Object.class, paramTypes);
return method.invoke(elContext, params);
}
private UIComponent up(UIComponent base, Class type)
{
UIComponent finder = base.getParent();
while (!(type.isInstance(finder)) && finder.getParent() != null) {
finder = finder.getParent();
}
if (!type.isInstance(finder)) {
finder = null;
}
return finder;
}
existingItemLoader 的工作方式大致相同。
这里值得讨论的另一个大问题是如何延迟 ajax 请求,使其不会在每次按键时发生。为此,我们使用 javascript。在复合组件中,我们需要:
<script type="text/javascript">
var #{cc.id}JS = new AutoCompleteInput('#{cc.clientId}', #{cc.attrs.delay});
</script>
其中引用了一个 autoCompleteInput.js 文件,其中包含以下内容:
function AutoCompleteInput(clientID, ajaxDelay)
{
var labelInput = $("#" + clientID + "-labelInput");
var valueInput = $("#" + clientID + "-valueInput");
var matchesList = $("#" + clientID + "-matches");
var matchesCount = 0;
labelInput.each(function(index, input) {
var onkeyup = input.onkeyup;
input.onkeyup = null;
$(input).on("keydown", function(e){
var key;
if (typeof e.which != "undefined"){
key = e.which;
}
else{
key = e.keyCode;
}
if (key == 40){ // If down, set focus on listbox
e.preventDefault();
matchesList.focus()
}
else if(key != 37 && key != 38 && key != 39){ // Reset the value on any non-directional key to prevent the user from having an invalid label but still submitting a valid value.
valueInput.val('')
}
});
$(input).on("keyup", function(e) {
var key;
if (typeof e.which != "undefined"){
key = e.which;
}
else{
key = e.keyCode;
}
if (key == 37 || key == 38 || key == 39){ // If left, right, or up, do not perform the search
return false;
}
else{ // Otherwise, delay the ajax request by the specified time
delay(function() { onkeyup.call(input, e); }, ajaxDelay);
}
});
});
var delay = (function() {
var timer = 0;
return function(callback, timeout) {
clearTimeout(timer);
timer = setTimeout(callback, timeout);
};
})();
}
我们还需要确定隐藏/显示匹配列表(它应该默认为隐藏)。我们只想在找到匹配项时显示它,但在 AJAX 请求完成之前我们不知道是否找到匹配项。为此,我们将输入的 f:ajax 更改为以下内容:
<f:ajax event="keyup" listener="#{autoCompleteUtils.itemLabelChange}" render="matches" onevent="function(data) { if (data.status === 'success') { #{cc.id}JS.checkMatches() }}"/>
调用函数:
this.checkMatches = function()
{
labelInput = $("#" + clientID + "-labelInput");
matchesList = $("#" + clientID + "-matches");
matchesCount = matchesList.children("option").length;
if (matchesCount > 1 && labelInput.val().length > 0){
matchesList.show();
}
else if (matchesCount == 1){ // If the search returned only one possible match, automatically select it
var item = matchesList.children("option").get(0);
selectItem(item.text, item.value);
setTimeout(function(){matchesList.fadeOut()}, 500)
}
else{
matchesList.hide();
}
}
完整示例
这应该涵盖了组件的大部分基础知识,所以我将切入正题,只展示完整的示例。它包括键盘可访问性和列表框的自动调整大小。它还包括一个“严格”属性:如果为真,则要求用户从建议列表中手动选择一项。如果为 false,组件将提交用户输入的任何值(以允许部分/自定义值)。
复合组件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>
<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:ui="http://java.sun.com/jsf/facelets"
xmlns:jsf="http://xmlns.jcp.org/jsf"
xmlns:composite="http://java.sun.com/jsf/composite"
xmlns:c="http://java.sun.com/jsp/jstl/core"
xmlns:pt="http://xmlns.jcp.org/jsf/passthrough"
>
<composite:interface componentType="AutoCompleteInput">
<composite:attribute name="value"/>
<composite:attribute name="delay" default="400" type="java.lang.Integer"/>
<composite:attribute name="strict" default="true" type="java.lang.Boolean"/>
<composite:attribute name="required" default="false" type="java.lang.Boolean"/>
<composite:attribute name="requiredMessage"/>
<composite:attribute name="placeholder" default="" />
<composite:attribute name="label"/>
<composite:attribute name="inputSize" default="25" type="java.lang.Integer"/>
<composite:attribute name="listboxSize" default="20" type="java.lang.Integer"/>
<composite:attribute name="maxlength" default="10000" type="java.lang.Integer"/>
</composite:interface>
<composite:implementation>
<h:outputScript name="components/js/autoCompleteInput.js" />
<h:outputStylesheet name="components/css/autoCompleteInput.css"/>
<div id="#{cc.clientId}-container"
class="#{cc.attrs.styleClass} has-success"
style="#{cc.attrs.style}">
<h:selectBooleanCheckbox id="strictCheckbox"
binding="#{cc.strictCheckbox}"
value="#{cc.attrs.strict}"
style="display:none"/>
<h:inputHidden id="clientIdInput"
value="#{cc.clientId}"
binding="#{autoCompleteUtils.clientIdInput}"
pt:disabled="disabled"/>
<h:inputHidden id="utilsValueInput"
value="#{cc.attrs.value}"
binding="#{autoCompleteUtils.itemValueInput}" />
<h:inputHidden id="listboxSizeInput"
binding="#{autoCompleteUtils.listboxSizeInput}"
value="#{cc.attrs.listboxSize}" />
<h:inputHidden id="valueInput"
binding="#{cc.valueInput}"
value="#{cc.attrs.value}" />
<h:inputText id="labelInput"
value="#{autoCompleteUtils.itemLabel}"
binding="#{cc.labelInput}"
pt:placeholder="#{cc.attrs.placeholder}"
label="#{cc.attrs.label}"
required="#{cc.attrs.required}"
requiredMessage="#{cc.attrs.requiredMessage}"
autocomplete="off"
size="#{cc.attrs.inputSize}"
styleClass="form-control focusable"
maxlength="#{cc.attrs.maxlength}"
onblur="#{cc.id}JS.checkFocus(#{cc.attrs.strict})"
onfocus="#{cc.id}JS.showMatches()"
rendered="#{autoCompleteUtils.loadExistingItem}">
<f:ajax event="keyup" listener="#{autoCompleteUtils.itemLabelChange}" render="matches" onevent="function(data) { if (data.status === 'success') { #{cc.id}JS.inputKeyup(#{cc.attrs.strict}) }}"/>
</h:inputText>
<h:selectOneListbox id="matches"
onkeyup="#{cc.id}JS.listboxKeypress(this)"
onclick="#{cc.id}JS.listboxClick(this)"
tabindex="0"
onblur="#{cc.id}JS.checkFocus(#{cc.attrs.strict})"
styleClass="matchesContainer focusable"
size="#{autoCompleteUtils.listboxSize}">
<f:selectItems value="#{autoCompleteUtils.selectItems}"/>
</h:selectOneListbox>
</div>
<script type="text/javascript">
var #{cc.id}JS = new AutoCompleteInput('#{cc.clientId}', #{cc.attrs.delay});
</script>
</composite:implementation>
</html>
自定义组件
public class AutoCompleteInput extends UIInput implements NamingContainer{
private UIInput valueInput;
private UIInput labelInput;
private UISelectBoolean strictCheckbox;
@Override
public String getFamily(){
return "javax.faces.NamingContainer";
}
@Override
public Object getSubmittedValue() {
return valueInput.getSubmittedValue();
}
@Override
public void validate(FacesContext context) {
String itemLabel = (String) labelInput.getSubmittedValue();
String itemValue = (String) getSubmittedValue();
Boolean strict = (Boolean) strictCheckbox.getValue();
if ((StringUtils.isEmpty(itemValue) || itemLabel.equals("-1")) && !StringUtils.isEmpty(itemLabel) && strict == true)
{
String message = (String)JsfUtils.resolveValueExpression("#{template.validationErrorAutoCompleteInputValue}");
JsfUtils.addErrorMessageForComponent(getId(), message, message);
setValid(false);
}
else if (isRequired() && (StringUtils.isEmpty(itemLabel) || itemLabel.equals("-1") || StringUtils.isEmpty(itemValue)))
{
String message = (String)JsfUtils.resolveValueExpression("#{template.validationErrorAutoCompleteInputLabel}");
String requiredMessage = (String)getAttributes().get("requiredMessage");
if (StringUtils.isNotBlank(requiredMessage)){
message = requiredMessage;
}
JsfUtils.addErrorMessageForComponent(getId(), message, message);
setValid(false);
}
super.validate(context);
}
public UIInput getvalueInput() {
return valueInput;
}
public void setvalueInput(UIInput valueInput) {
this.valueInput = valueInput;
}
public UIInput getLabelInput() {
return labelInput;
}
public void setLabelInput(UIInput labelInput) {
this.labelInput = labelInput;
}
public UISelectBoolean getStrictCheckbox() {
return strictCheckbox;
}
public void setStrictCheckbox(UISelectBoolean strictCheckbox) {
this.strictCheckbox = strictCheckbox;
}
}
组件实用程序
public class AutoCompleteUtils implements Serializable {
private static final long serialVersionUID = 1636810191718728665L;
private String itemLabel;
private List<SelectItem> selectItems = new ArrayList<SelectItem>();
private UIInput itemValueInput;
private UIInput clientIdInput;
private UIInput listboxSizeInput;
public boolean isLoadExistingItem()
{
String clientId = (String) clientIdInput.getValue();
ValueExpression itemLoader = null;
AutoCompleteInput input = (AutoCompleteInput) FacesContext.getCurrentInstance().getViewRoot().findComponent(clientId);
itemLoader = input.getValueExpression("existingItemLoader");
if (itemLoader != null)
{
String itemValue = itemValueInput.getValue().toString();
if (!StringUtils.isEmpty(itemValue) && !itemValue.equals("-1"))
{
Class<?>[] paramTypes = {String.class};
Object[] paramValues = {itemValue};
itemLabel = (String) invokeMethod(FacesContext.getCurrentInstance(), itemLoader.getExpressionString(), paramValues, paramTypes);
}
}
return true;
}
public void itemLabelChange(AjaxBehaviorEvent event)
{
selectItems = new ArrayList<SelectItem>();
ValueExpression searchListener = null;
AutoCompleteInput input = (AutoCompleteInput) up(event.getComponent(), UIInput.class);
searchListener = input.getValueExpression("searchListener");
if (searchListener != null)
{
Class<?>[] paramTypes = {String.class};
Object[] paramValues = {itemLabel};
selectItems = (List<SelectItem>) invokeMethod(FacesContext.getCurrentInstance(), searchListener.getExpressionString(), paramValues, paramTypes);
}
}
public int getListboxSize()
{
int size = (Integer) listboxSizeInput.getValue();
if (selectItems.size() > 0 && selectItems.size() < size)
{
size = selectItems.size();
}
return size;
}
private Object invokeMethod(FacesContext context, String expression, Object[] params, Class<?>[] paramTypes)
{
ExpressionFactory eFactory = context.getApplication().getExpressionFactory();
ELContext elContext = context.getELContext();
MethodExpression method = eFactory.createMethodExpression(elContext, expression, Object.class, paramTypes);
return method.invoke(elContext, params);
}
private UIComponent up(UIComponent base, Class type)
{
UIComponent finder = base.getParent();
while (!(type.isInstance(finder)) && finder.getParent() != null) {
finder = finder.getParent();
}
if (!type.isInstance(finder)) {
finder = null;
}
return finder;
}
public String getitemLabel() {
return itemLabel;
}
public void setitemLabel(String itemLabel) {
this.itemLabel = itemLabel;
}
public UIInput getClientIdInput() {
return clientIdInput;
}
public void setClientIdInput(UIInput clientIdInput) {
this.clientIdInput = clientIdInput;
}
public UIInput getItemValueInput() {
return itemValueInput;
}
public void setItemValueInput(UIInput itemValueInput) {
this.itemValueInput = itemValueInput;
}
public UIInput getListboxSizeInput() {
return listboxSizeInput;
}
public void setListboxSizeInput(UIInput listboxSizeInput) {
this.listboxSizeInput = listboxSizeInput;
}
public List<SelectItem> getselectItems() {
return selectItems;
}
public void setselectItems(List<SelectItem> selectItems) {
this.selectItems = selectItems;
}
}
Javascript
function AutoCompleteInput(clientID, ajaxDelay)
{
var labelInput = $("#" + clientID + "-labelInput");
var valueInput = $("#" + clientID + "-valueInput");
var matchesList = $("#" + clientID + "-matches");
var matchesCount = 0;
labelInput.each(function(index, input) {
var onkeyup = input.onkeyup;
input.onkeyup = null;
$(input).on("keydown", function(e){
var key;
if (typeof e.which != "undefined"){
key = e.which;
}
else{
key = e.keyCode;
}
if (key == 40){ // If down, set focus on listbox
e.preventDefault();
matchesList.focus()
}
else if(key != 37 && key != 38 && key != 39){ // Reset the value on any non-directional key to prevent the user from having an invalid label but still submitting a valid value.
valueInput.val('')
}
});
$(input).on("keyup", function(e) {
var key;
if (typeof e.which != "undefined"){
key = e.which;
}
else{
key = e.keyCode;
}
if (key == 37 || key == 38 || key == 39){ // If left, right, or up, do not perform the search
return false;
}
else{ // Otherwise, delay the ajax request by the specified time
delay(function() { onkeyup.call(input, e); }, ajaxDelay);
}
});
});
var delay = (function() {
var timer = 0;
return function(callback, timeout) {
clearTimeout(timer);
timer = setTimeout(callback, timeout);
};
})();
this.inputKeyup = function(strict)
{
labelInput = $("#" + clientID + "-labelInput");
matchesList = $("#" + clientID + "-matches");
matchesCount = matchesList.children("option").length;
if (matchesCount > 1 && labelInput.val().length > 0){
matchesList.show();
}
else if (matchesCount == 1 && strict){
var item = matchesList.children("option").get(0);
selectItem(item.text, item.value);
setTimeout(function(){matchesList.fadeOut()}, 500)
}
else{
matchesList.hide();
}
}
this.listboxKeypress = function(input)
{
$(input).on("keydown", function(e){
var key;
if (typeof e.which != "undefined"){
key = e.which;
}
else{
key = e.keyCode;
}
if (key == 13){ // If enter, then select the item
e.preventDefault();
var label = $(input).find('option:selected').text();
var value = $(input).val();
selectItem(label, value);
matchesList.hide();
}
});
}
this.listboxClick = function(input)
{
var label = $(input).find('option:selected').text();
var value = $(input).val();
selectItem(label, value);
matchesList.hide();
}
this.showMatches = function()
{
if (typeof matchesList != "undefined" && matchesCount !=0 && labelInput.val().length > 0){
matchesList.show();
}
}
this.checkFocus = function(strict)
{
setTimeout(function () {
if (document.activeElement.className.indexOf("focusable") == -1 &&
typeof matchesList != "undefined"){
matchesList.hide();
}
}, 100);
if (strict == false){
valueInput.val(labelInput.val());
}
}
function selectItem(itemLab, itemVal)
{
labelInput.val(itemLab);
valueInput.val(itemVal);
}
}
结束评论
我不得不承认这个组件有一些我不太喜欢的地方,尤其是在 facelet 中使用隐藏输入来传递属性值。但这是我发现的工作。但是,我对 JSF 还是比较陌生(对 javascript 也很陌生),所以我会感谢任何有关如何改进它的 cmets 和建议!