【问题标题】:Script is not rendered after postback in a composite component added programmatically在以编程方式添加的复合组件中回发后不呈现脚本
【发布时间】:2017-01-29 02:57:55
【问题描述】:

我有这个复合组件:

inputMask.xhtml

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:composite="http://xmlns.jcp.org/jsf/composite"
      xmlns:f="http://xmlns.jcp.org/jsf/core"
      xmlns:h="http://xmlns.jcp.org/jsf/html"
      xmlns:c="http://xmlns.jcp.org/jsp/jstl/core">

      <composite:interface>
        <composite:attribute name="value" />
        <composite:attribute name="mask" type="java.lang.String" required="true" />
        <composite:attribute name="converterId" type="java.lang.String" default="br.edu.ufca.eventos.visao.inputmask.inputMask" />
      </composite:interface>

      <composite:implementation>
        <h:outputScript library="script" name="inputmask.js" target="head" />

        <h:inputText id="mascara">
            <c:if test="#{cc.getValueExpression('value') != null}">
                <f:attribute name="value" value="#{cc.attrs.value}" />
            </c:if>
            <f:converter converterId="#{cc.attrs.converterId}" />
            <f:attribute name="mask" value="#{cc.attrs.mask}" />
        </h:inputText>

        <h:outputScript target="body">
            defineMask("#{cc.clientId}", "#{cc.attrs.mask}");
        </h:outputScript>
      </composite:implementation>
</html>

在我的最后一个问题中:

Error trying to add composite component programmatically ("no tag was defined for name")

我收到了这个错误:

javax.faces.view.facelets.TagException: //C:/wildfly-10/standalone/tmp/eventos.ear.visao.war/mojarra7308315477323852505.tmp @2,127 <j:inputMask.xhtml> Tag Library supports namespace: http://xmlns.jcp.org/jsf/composite/componente, but no tag was defined for name: inputMask.xhtml

当尝试使用此代码以编程方式添加上述复合组件时:

Map<String, String> attributes = new HashMap<>();
attributes.put("mask", "999.999");
Components.includeCompositeComponent(Components.getCurrentForm(), "componente", "inputMask.xhtml", "a123", attributes);

但我设法通过这种方式解决了这个问题:

OmniFaces 2.4(我使用的版本)方法Components#includeCompositeComponent的实现是这样的:

public static UIComponent includeCompositeComponent(UIComponent parent, String libraryName, String tagName, String id, Map<String, String> attributes) {
    String taglibURI = "http://xmlns.jcp.org/jsf/composite/" + libraryName;
    Map<String, Object> attrs = (attributes == null) ? null : new HashMap<String, Object>(attributes);

    FacesContext context = FacesContext.getCurrentInstance();
    UIComponent composite = context.getApplication().getViewHandler()
        .getViewDeclarationLanguage(context, context.getViewRoot().getViewId())
        .createComponent(context, taglibURI, tagName, attrs);
    composite.setId(id);
    parent.getChildren().add(composite);
return composite;
}

所以我决定尝试一下这个方法的早期版本的 OmniFaces 的代码(添加了一些我的 attributes 参数):

public static UIComponent includeCompositeComponent(UIComponent parent, String libraryName, String resourceName, String id, Map<String, String> attributes) {
    // Prepare.
    FacesContext context = FacesContext.getCurrentInstance();
    Application application = context.getApplication();
    FaceletContext faceletContext = (FaceletContext) context.getAttributes().get(FaceletContext.FACELET_CONTEXT_KEY);

    // This basically creates <ui:component> based on <composite:interface>.
    Resource resource = application.getResourceHandler().createResource(resourceName, libraryName);
    UIComponent composite = application.createComponent(context, resource);
    composite.setId(id); // Mandatory for the case composite is part of UIForm! Otherwise JSF can't find inputs.

    // This basically creates <composite:implementation>.
    UIComponent implementation = application.createComponent(UIPanel.COMPONENT_TYPE);
    implementation.setRendererType("javax.faces.Group");
    composite.getFacets().put(UIComponent.COMPOSITE_FACET_NAME, implementation);

    if (!attributes.isEmpty()) {
        ExpressionFactory factory = application.getExpressionFactory();
        ELContext ctx = context.getELContext();
        for (Map.Entry<String, String> entry : attributes.entrySet()) {
            ValueExpression expr = factory.createValueExpression(ctx, entry.getValue(), Object.class);
            composite.setValueExpression(entry.getKey(), expr);
        }
    } 

    // Now include the composite component file in the given parent.
    parent.getChildren().add(composite);
    parent.pushComponentToEL(context, composite); // This makes #{cc} available.
    try {
        faceletContext.includeFacelet(implementation, resource.getURL());
    } catch (IOException e) {
        throw new FacesException(e);
    } finally {
        parent.popComponentFromEL(context);
    }

    return composite;
}

最后错误消失了。复合组件被动态添加到页面中。

但是又出现了一个问题。

按钮中添加组件的动作或多或少是这样的:

if (Components.findComponent("form:a123") == null)
{
    Map<String, String> attributes = new HashMap<>();
    attributes.put("value", "#{bean.cpf}");
    attributes.put("mask", "999.999.999-99");
    includeCompositeComponent(Components.getCurrentForm(), "componente", "inputMask.xhtml", "a123", attributes);
}

如您所见,复合组件只添加一次。

第一次添加组件时,组件中的脚本代码:

<h:outputScript target="body">
    defineMask("#{cc.clientId}", "#{cc.attrs.mask}");
</h:outputScript>

被添加到页面。当我在浏览器中可视化 html 源代码时,我可以看到它。但是在回发时,不再呈现此脚本代码。它不在生成的 html 页面中。 &lt;h:outputScript&gt;target="head" 每次都按预期呈现,但不是这个。

从我的角度来看,即使在页面回发时,在上述方法中组装复合组件代码以修复脚本代码时,可能仍然缺少一些东西。我真的不知道。这只是一个猜测。

您知道发生了什么或缺少什么吗?

---- 更新 1 ----

我认为我真的找到了问题的根源。这似乎是 JSF 中的一个错误,与以编程方式包含的复合组件中的脚本有关。

这是我发现的:

我注意到 OmniFaces 中包含我的复合组件的正确代码是这样的:

Components.includeCompositeComponent(Components.getCurrentForm(), "componente", "inputMask", "a123", attributes);

正确的是"inputMask",而不是"inputMask.xhtml"。但正如我之前告诉你的,当我使用这段代码时,我得到了这个错误:

Caused by: javax.faces.FacesException: Cannot remove the same component twice: form:a123:j_idt2

所以我怀疑 ID 为 form:a123:j_idt2 的组件是复合组件中存在的 h:outputScript 之一。于是我把复合组件代码改成这样:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:composite="http://xmlns.jcp.org/jsf/composite"
      xmlns:f="http://xmlns.jcp.org/jsf/core"
      xmlns:h="http://xmlns.jcp.org/jsf/html"
      xmlns:c="http://xmlns.jcp.org/jsp/jstl/core">

      <composite:interface componentType="inputMask">
        <composite:attribute name="value" />
        <composite:attribute name="mask" type="java.lang.String" required="true" />
        <composite:attribute name="converterId" type="java.lang.String" default="br.edu.ufca.eventos.visao.inputmask.inputMask" />
      </composite:interface>

      <composite:implementation>
        <h:inputText id="mascara">
            <c:if test="#{cc.getValueExpression('value') != null}">
                <f:attribute name="value" value="#{cc.attrs.value}" />
            </c:if>
            <f:converter converterId="#{cc.attrs.converterId}" />
            <f:attribute name="mask" value="#{cc.attrs.mask}" />
        </h:inputText>

        <script type="text/javascript">
            defineMask("#{cc.clientId}", "#{cc.attrs.mask}");
        </script>
      </composite:implementation>
</html>

删除对 h:outputScript 标记的所有引用。 (当然,我把inputmask.js脚本放在了复合组件外面,让组件继续工作)。

现在,当我运行代码时,组件终于添加到页面中而没有错误。但是,正如我之前使用 OmniFaces 早期版本的代码所说的,脚本仍然没有在回发中呈现。 JSF 仅在添加组件时呈现它,在回发时丢失它。我知道这不是预期的行为。

所以,我问你:你知道我该如何解决这个脚本问题吗?或者至少在这种情况下我可以使用任何解决方法?

提前谢谢你。

---- 更新 2 ----

我找到了解决方法。我在复合组件的支持组件中执行了此操作,并且它有效,脚本始终被渲染:

@Override
public void encodeEnd(FacesContext context) throws IOException
{
    super.encodeEnd(context);

    ResponseWriter writer = context.getResponseWriter();
    writer.startElement("script", this);
    writer.writeText(String.format("defineMask('%s', '%s');",
        getClientId(), getAttributes().get("mask")), null);
    writer.endElement("script");
}

但这有点难看,似乎没有必要。同样,如果组件未以编程方式包含,我不需要支持组件。这似乎是 JSF 中的一个错误。你们中的一些人可以测试并确认这一点吗?我的意思是,测试一个包含脚本的复合组件是否以编程方式在回发时丢失了它的脚本。

附注:我正在使用 OmniFaces 2.4Mojarra 2.2.13

【问题讨论】:

    标签: jsf composite-component omnifaces


    【解决方案1】:

    解决方案(变通方法)是从复合组件中删除所有脚本,并为其创建一个支持组件以准确执行 JSF 应该执行的操作:

    package br.edu.company.project.view.inputmask;
    
    import java.io.IOException;
    import java.util.Map;
    
    import javax.faces.component.FacesComponent;
    import javax.faces.component.NamingContainer;
    import javax.faces.component.UIInput;
    import javax.faces.component.UINamingContainer;
    import javax.faces.context.FacesContext;
    import javax.faces.context.ResponseWriter;
    
    import org.omnifaces.util.FacesLocal;
    
    @FacesComponent("inputMask")
    public class InputMask extends UIInput implements NamingContainer
    {
        private static final String SCRIPT_FILE_WRITTEN =
            "br.edu.company.project.SCRIPT_FILE_WRITTEN";
    
        @Override
        public String getFamily()
        {
            return UINamingContainer.COMPONENT_FAMILY;
        }
    
        @Override
        public void encodeBegin(FacesContext context) throws IOException
        {
            writeScriptFileIfNotWrittenYet(context);
    
            super.encodeBegin(context);
        }
    
        @Override
        public void encodeEnd(FacesContext context) throws IOException
        {
            super.encodeEnd(context);
    
            writeMaskDefinition(context);
        }
    
        private void writeScriptFileIfNotWrittenYet(FacesContext context) throws IOException
        {
            if (FacesLocal.getRequestMap(context).putIfAbsent(
                SCRIPT_FILE_WRITTEN, true) == null)
            {
                writeScript(context, w -> w.writeAttribute(
                    "src", "resources/script/inputmask.js", null));
            }
        }
    
        private void writeMaskDefinition(FacesContext context) throws IOException
        {
            writeScript(context, w -> w.writeText(String.format(
                "defineMask('%s', '%s');", getClientId(),
                getAttributes().get("mask")), null));
        }
    
        private void writeScript(FacesContext context, WriteAction writeAction)
            throws IOException
        {
            ResponseWriter writer = context.getResponseWriter();
            writer.startElement("script", this);
            writer.writeAttribute("type", "text/javascript", null);
            writeAction.execute(writer);
            writer.endElement("script");
        }
    
        @FunctionalInterface
        private static interface WriteAction
        {
            void execute(ResponseWriter writer) throws IOException;
        }
    }
    

    同样,如果您的复合组件不会以编程方式包含在内,您就不需要这个。在这种情况下,JSF 按预期工作,您不需要支持组件。

    如果有人有时间,我认为向 Mojarra 团队提交错误报告会很好。

    【讨论】:

      猜你喜欢
      • 2014-08-16
      • 2015-03-11
      • 1970-01-01
      • 2016-01-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-04-09
      • 2013-03-27
      相关资源
      最近更新 更多