Script is not rendered after postback in a composite component added programmatically

萝らか妹 提交于 2019-12-08 02:06:16

问题


I have this composite component:

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>

In my last question:

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

I was getting this error:

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

when trying to add the above composite component programmatically with this code:

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

but I managed to solve this problem this way:

The implementation of the method Components#includeCompositeComponent from OmniFaces 2.4 (the version I was using) is this:

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;
}

So I decided to give a try to the code from an earlier version of OmniFaces (with some change adding the attributes parameter from me) of this method:

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;
}

And finally the error was gone. The composite component was dynamically added to the page.

But another problem appeared.

The action in a button to add the component is more or less like this:

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);
}

As you can see, the composite component is only added once.

When the component is first added, the script code that is in the component:

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

is added to the page. I can see it when I visualize the html source code in the browser. But on postbacks, this script code is not rendered anymore. It's not in the genereted html page. The <h:outputScript> with target="head" is rendered everytime, as expected, but not this one.

From my point of view, maybe there's still someting missing in the assembling of the composite component code in the method above to fix the script code even on postbacks on the page. I really don't know. It's just a guess.

Do you know what's going on or what's missing?

---- UPDATE 1 ----

I think that I really found the source of the problem. It seems that it's a bug in JSF related with scripts in composite components included programatically.

Here's what I found:

I noticed that the correct code from OmniFaces to include my composite component is this:

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

The correct is "inputMask", not "inputMask.xhtml". But as I told you before, when I use this code I get this error instead:

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

So I suspected that the component with the id form:a123:j_idt2 was one of the h:outputScript present in the composite component. So I changed the composite component code to this:

<!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>

Removing all references to the h:outputScript tag. (Of course, I placed the inputmask.js script outside the composite component for the component to continue to work).

And now when I run the code, the component is finally added to the page without errors. But, as I said before with the code from an earlier version of OmniFaces, the script is still not rendered in postbacks. JSF only renders it when the component is added, loosing it on postbacks. I know this is not an expected behaviour.

So, I ask you: do you know how I can solve this script problem? Or at least any workaround I can use in this case?

Thank you in advance.

---- UPDATE 2 ----

I found a workaround for it. I did this in a backing component for the composite component and it worked, the script is always rendered:

@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");
}

but it's kind of ugly and seems unnecessary. Again, if the component is not included programmatically, I don't need the backing component. It seems like a bug in JSF. Could some of you test and confirm this? I mean, test if a composite component with script in it added programmatically loses its script on postback.

P.S.: I'm using OmniFaces 2.4 and Mojarra 2.2.13.


回答1:


The solution (workaround) is to remove all script from the composite component and create a backing component for it to do precisely what JSF was supposed to do:

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;
    }
}

Again, you don't need this if your composite component won't be included programmatically. In this case, JSF works as expected and you don't need the backing component.

If someone have the time, I think it would be nice to file a bug report to the Mojarra team.



来源:https://stackoverflow.com/questions/39606719/script-is-not-rendered-after-postback-in-a-composite-component-added-programmati

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!