UIInput 和 UICommand 的自定义组件

rau*_*ach 2 jsf custom-component

在处理 JSF 自定义组件时有点卡住了。如果有人能给我一个开端,那就太好了。

这个想法是有一个组件,让我通过一个LocalDate. 想一个更简单的日期选择器。呈现的 HTML 有两个按钮,一个将日期增加一天,另一个按钮将日期减少一天。值本身存储在一个输入隐藏中。附件是组件的一个简单版本,它不起作用,但希望能描述我想要实现的目标。

这个想法是<my:datePager value="#{myBackingBean.someDate}" />在页面上说并在单击任一按钮之一后执行一些逻辑/操作。

例如<my:datePager value="#{myBackingBean.someDate} action="..." /><my:datePager value="#{myBackingBean.someDate} previous="..." next="..."/>不知道什么更好。

这是我遇到的问题:如何调用组件上的previousnext方法?

到目前为止,这是我的 JSF 2.3 自定义组件:

@FacesComponent(LocalDatePager.COMPONENT_TYPE)
public class LocalDatePager extends UIInput {

    public static final String COMPONENT_TYPE = "LocalDatePager";

    public LocalDatePager() {
        setConverter(LocalDateConverter.INSTANCE);
    }

    public void previous() {
        LocalDate localDate = (LocalDate) getValue();
        localDate = localDate.minusDays(1);
        setValue(localDate);
    }

    public void next() {
        LocalDate localDate = (LocalDate) getValue();
        localDate = localDate.plusDays(1);
        setValue(localDate);
    }

    @Override
    public void decode(FacesContext context) {
        super.decode(context);
    }

    @Override
    public void encodeEnd(FacesContext context) throws IOException {
        ResponseWriter writer = context.getResponseWriter();

        writer.startElement("div", this);
        writer.writeAttribute("class", "btn-group", null);
        writer.writeAttribute("role", "group", null);
        writer.startElement("button", this);
        writer.writeAttribute("type", "button", null);
        writer.writeAttribute("class", "btn btn-outline-primary", null);
        writer.writeText("Previous", null);
        writer.endElement("button");
        writer.startElement("button", this);
        writer.writeAttribute("type", "button", null);
        writer.writeAttribute("class", "btn btn-outline-primary", null);
        writer.writeText("Next", null);
        writer.endElement("button");
        writer.endElement("div");

        writer.startElement("input", this);
        writer.writeAttribute("name", getClientId(context), "clientId");
        writer.writeAttribute("type", "hidden", null);
        writer.writeAttribute("value", getValue(), "value");
        writer.endElement("input");
    }

}
Run Code Online (Sandbox Code Playgroud)

根据我的理解,我所拥有的组件是 mind 是 bothUIInputUICommand。我需要实施ActionEvent吗?火值变化事件?

目前,我在支持 bean 中而不是在组件中执行整个上一个和下一个方法。这会导致大量重复代码,我认为自定义组件最适合。我尝试使用复合组件,但这也没有让我走得更远。

到目前为止,另一种方法是客户端行为:执行 JavaScript,更改隐藏输入的值并提交表单。这感觉更糟。

如果有人能给我一个正确方向的指针,那就太好了。

Bal*_*usC 5

作为 UIInput

首先,您需要将按钮呈现为encodeXxx()方法中的普通提交按钮。此外,您宁愿使用encodeAll()而不是encodeEnd()为此,因为这似乎是一个“叶”组件,因此您不想与孩子打交道。这样实施起来encodeAll()会更有效率。将您现有的encodeEnd()方法重命名为encodeAll()并调整按钮的编码如下:

writer.startElement("button", this);
writer.writeAttribute("type", "submit", null); // instead of "button"
writer.writeAttribute("name", getClientId(context) + "_previous", "clientId");
writer.writeText("Previous", null);
// ...
writer.startElement("button", this);
writer.writeAttribute("type", "submit", null); // instead of "button"
writer.writeAttribute("name", getClientId(context) + "_next", "clientId");
writer.writeText("Next", null);
// ...
// The hidden input field in your existing code is fine as-is.
Run Code Online (Sandbox Code Playgroud)

然后你可以在请求参数映射中检查它们。如果你想让你的组件成为一个UIInput,那么我建议在getConvertedValue()方法中执行这项工作:

@Override
protected Object getConvertedValue(FacesContext context, Object submittedValue) throws ConverterException {
    LocalDate localDate = (LocalDate) super.getConvertedValue(context, submittedValue);

    if (localDate != null) {
        Map<String, String> params = context.getExternalContext().getRequestParameterMap();

        if (params.get(getClientId(context) + "_previous") != null) {
            localDate = localDate.minusDays(1);
        }
        else if (params.get(getClientId(context) + "_next") != null) {
            localDate = localDate.plusDays(1);
        }
    }

    return localDate;
}
Run Code Online (Sandbox Code Playgroud)

然后,您可以简单地在值更改侦听器中完成这项工作:

<my:datePager value="#{bean.localDate}" valueChangeListener="#{bean.onpage}" />
Run Code Online (Sandbox Code Playgroud)
public void onpage(ValueChangeEvent event) {
    // ...
}
Run Code Online (Sandbox Code Playgroud)

这基本上就是全部。无需手动触发值更改事件。其余的逻辑已经由 JSF 处理。

然而,这种方法的缺点是它可能在 JSF 生命周期中的错误时刻被调用。在验证阶段调用值更改侦听器,而您确实希望在调用应用程序阶段调用它。想象一下,这个按钮被放置在一个其状态取决于与 关联的数据的表单中#{bean.localDate},那么它们上的更新模型值阶段可能会出错,因为localDate已经改变得太快了。

作为 UICommand

如果上述的缺点是一个真正的搅局者,虽然有变通,那么你最好转换UIInputUICommand。不能两者兼而有之。你选择一个或另一个。我个人的建议是选择UICommand在 JSF 生命周期中的行为“更自然”,同时牢记组件的唯一目的(“分页到下一个/上一个日期”)。以下是步骤:

  1. extends UIInput换出extends UICommand.

  2. 删除setConverter()构造函数中的调用。

  3. 保持encodeAll()方法。完全没问题。

  4. 删除getConvertedValue()方法并实现decode()如下:

     @Override
     public void decode(FacesContext context) {
         Map<String, String> params = context.getExternalContext().getRequestParameterMap();
    
         if (params.get(getClientId(context) + "_previous") != null) {
             queueEvent(new ActionEvent(context, this));
         }
         else if (params.get(getClientId(context) + "_next") != null) {
             queueEvent(new ActionEvent(context, this));
         }
     }
    
    Run Code Online (Sandbox Code Playgroud)

    这些queueEvent()调用将导致调用broadcast()相同组件的 。所以覆盖它如下:

     @Override
     public void broadcast(FacesEvent event) throws AbortProcessingException {
         FacesContext context = event.getFacesContext();
         Map<String, String> params = context.getExternalContext().getRequestParameterMap();
         LocalDate localDate = LocalDate.parse(params.get(getClientId())); // TODO: gracefully handle any conversion error.
    
         if (params.get(getClientId(context) + "_previous") != null) {
             localDate = localDate.minusDays(1);
         }
         else if (params.get(getClientId(context) + "_next") != null) {
             localDate = localDate.plusDays(1);
         }
    
         getValueExpression("value").setValue(context.getELContext(), localDate);
         super.broadcast(event); // Invokes bean method.
     }
    
    Run Code Online (Sandbox Code Playgroud)

    注意value属性是如何在模型中手动解码、转换和更新的。尽管这是作为隐藏输入值传递的,但您可能希望添加一些逻辑来优雅地处理任何转换错误,如TODO.

现在您可以按如下方式使用它:

<my:datePager value="#{bean.localDate}" action="#{bean.onpage}" />
Run Code Online (Sandbox Code Playgroud)
public void onpage() {
    // ...
}
Run Code Online (Sandbox Code Playgroud)