Search code examples
jsfjsf-2commandbuttoncommandlink

h:commandButton/h:commandLink does not work on first click, works only on second click


We have an ajax navigation menu which updates a dynamic include. The include files have each their own forms.

<h:form>
    <h:commandButton value="Add" action="#{navigator.setUrl('AddUser')}">
        <f:ajax render=":propertiesArea" />
    </h:commandButton>
</h:form>
<h:panelGroup id="propertiesArea" layout="block">
    <ui:include src="#{navigator.selectedLevel.url}" />
</h:panelGroup>

It works correctly, but any command button in the include file doesn't work on first click. It works only on second click and forth.

I found this question commandButton/commandLink/ajax action/listener method not invoked or input value not updated and my problem is described in point 9. I understand that I need to explicitly include the ID of the <h:form> in the include in the <f:ajax render> to solve it.

<f:ajax render=":propertiesArea :propertiesArea:someFormId" />

In my case, however, the form ID is not known beforehand. Also this form will not be available in the context initally.

Is there any solution to the above scenario?


Solution

  • You can use the following script to fix the Mojarra 2.0/2.1/2.2 bug (note: this doesn't manifest in MyFaces). This script will create the javax.faces.ViewState hidden field for forms which did not retrieve any view state after ajax update.

    jsf.ajax.addOnEvent(function(data) {
        if (data.status == "success") {
            fixViewState(data.responseXML);
        }
    });
    
    function fixViewState(responseXML) {
        var viewState = getViewState(responseXML);
    
        if (viewState) {
            for (var i = 0; i < document.forms.length; i++) {
                var form = document.forms[i];
    
                if (form.method == "post") {
                    if (!hasViewState(form)) {
                        createViewState(form, viewState);
                    }
                }
                else { // PrimeFaces also adds them to GET forms!
                    removeViewState(form);
                }
            }
        }
    }
    
    function getViewState(responseXML) {
        var updates = responseXML.getElementsByTagName("update");
    
        for (var i = 0; i < updates.length; i++) {
            var update = updates[i];
    
            if (update.getAttribute("id").match(/^([\w]+:)?javax\.faces\.ViewState(:[0-9]+)?$/)) {
                return update.textContent || update.innerText;
            }
        }
    
        return null;
    }
    
    function hasViewState(form) {
        for (var i = 0; i < form.elements.length; i++) {
            if (form.elements[i].name == "javax.faces.ViewState") {
                return true;
            }
        }
    
        return false;
    }
    
    function createViewState(form, viewState) {
        var hidden;
    
        try {
            hidden = document.createElement("<input name='javax.faces.ViewState'>"); // IE6-8.
        } catch(e) {
            hidden = document.createElement("input");
            hidden.setAttribute("name", "javax.faces.ViewState");
        }
    
        hidden.setAttribute("type", "hidden");
        hidden.setAttribute("value", viewState);
        hidden.setAttribute("autocomplete", "off");
        form.appendChild(hidden);
    }
    
    function removeViewState(form) {
        for (var i = 0; i < form.elements.length; i++) {
            var element = form.elements[i];
            if (element.name == "javax.faces.ViewState") {
                element.parentNode.removeChild(element);
            }
        }
    }
    

    Just include it as <h:outputScript name="some.js" target="head"> inside the <h:body> of the error page. If you can't guarantee that the page in question uses JSF <f:ajax>, which would trigger auto-inclusion of jsf.js, then you might want to add an additional if (typeof jsf !== 'undefined') check before jsf.ajax.addOnEvent() call, or to explicitly include it by

    <h:outputScript library="javax.faces" name="jsf.js" target="head" />
    

    Note that jsf.ajax.addOnEvent only covers standard JSF <f:ajax> and not e.g. PrimeFaces <p:ajax> or <p:commandXxx> as they use under the covers jQuery for the job. To cover PrimeFaces ajax requests as well, add the following:

    $(document).ajaxComplete(function(event, xhr, options) {
        if (typeof xhr.responseXML != 'undefined') { // It's undefined when plain $.ajax(), $.get(), etc is used instead of PrimeFaces ajax.
            fixViewState(xhr.responseXML);
        }
    }
    

    Update if you're using JSF utility library OmniFaces, it's good to know that the above has since 1.7 become part of OmniFaces. It's just a matter of declaring the following script in the <h:body>. See also the showcase.

    <h:body>
        <h:outputScript library="omnifaces" name="fixviewstate.js" target="head" />
        ...
    </h:body>