24

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?

Community
  • 1
  • 1
Skyler Hays
  • 259
  • 1
  • 4
  • 14

2 Answers2

45

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>
BalusC
  • 992,635
  • 352
  • 3,478
  • 3,452
  • Thanks. The solution worked just right.But i had another point to make. We are also using primefaces(3.3) for some UI components.And i observed that if i replace the h:commandbutton with p:commandbutton, this problem does not occur. Not sure why but the only difference i could find was that h:commandbutton is rendered as as tag and p:commandbutton is rendered as – Skyler Hays Jul 11 '12 at 05:23
  • Indeed, PrimeFaces has solved it internally in its own JSF ajax engine (check the `core.js` file). So if you use PrimeFaces ajax components, you will not face this JSF API specific problem. The difference in HTML is not relevant. It's all about ajax response handling. – BalusC Jul 11 '12 at 12:26
  • When I add this script to my page, chrome work great but in IE9, I got this javascript message `SCRIPT438: Object doesn't support property or method 'getElementById` on line `var viewState = data.responseXML.getElementById("javax.faces.ViewState").firstChild.nodeValue`. I look, but can seems to find a workaround on the web. Do you know how to handle this in IE9, BalusC? – Thang Pham Jan 30 '13 at 10:13
  • @BalusC: Thank you so much, I now have error `DOM Exception: INVALID_CHARACTER_ERR (5)` on line `var hidden = document.createElement(ie ? "" : "input");`, can you please help me a bit more, BalusC? – Thang Pham Jan 30 '13 at 11:25
  • @Thang: oh :( IE9 didn't swallow it anymore. I'll fix it. – BalusC Jan 30 '13 at 11:43
  • Ok I fixed the issue base on the answer from this link, http://stackoverflow.com/questions/5344029/invalid-character-dom-exception-in-ie9 Thank you BalusC – Thang Pham Jan 30 '13 at 11:43
  • 1
    @Thang: yes, but `setAttribute("name", value)` doesn't work for input elements in IE6-8, that's exactly why this hack was there in first place. See also e.g. http://webbugtrack.blogspot.com/2007/10/bug-235-createelement-is-broken-in-ie.html – BalusC Jan 30 '13 at 11:45
  • Awesome, Thank you BalusC :) – Thang Pham Jan 30 '13 at 13:20
  • Any solution for this problem in jsf 1.2? – Pedro Vítor Jun 13 '14 at 13:15
  • I still have this problem in primefaces 5.1 – adranale Feb 03 '16 at 15:08
  • I used this as ..... – osmingo Feb 11 '16 at 14:03
  • I can confirm that this bug is not present in apache MyFaces. Can anybody confirm if this bug is still present in mojarra 2.3+? – Mateus Viccari Jun 29 '17 at 13:58
  • 1
    @MateusViccari: I fixed it myself for Mojarra 2.3 https://github.com/javaserverfaces/mojarra/commit/ff26c4dccafd50d22757ce8562230e31f9768033 and made it a required part of JSF spec https://github.com/javaee/javaserverfaces-spec/issues/790 – BalusC Jun 29 '17 at 14:53
  • Legendary answer! thanx – fareed Sep 19 '17 at 22:37
5

Thanks to BalusC since his answer is really great (as usual :) ). But I have to add that this approach does not work for ajax requests coming from RichFaces 4. They have several issues with ajax and one of them is that the JSF-ajax-handlers are not being invoked. Thus, when doing a rerender on some container holding a form using RichFaces-components, the fixViewState-function is not called and the ViewState is missing then.

In the RichFaces Component Reference, they state how to register callbacks for "their" ajax-requests (in fact they're utilizing jQuery to hook on all ajax-requests). But using this, I was not able to get the ajax-response which is used by BalusC's script above to get the ViewState.

So based on BalusC's fix, i worked out a very similar one. My script saves all ViewState-values of all forms on the current page in a map before the ajax-request is being processed by the browser. After the update of the DOM, I try to restore all ViewStates which have been saved before (for all forms which are missing the ViewState now).

Move on:

jQuery(document).ready(function() {
    jQuery(document).on("ajaxbeforedomupdate", function(args) {
        // the callback will be triggered for each received JSF AJAX for the current page
        // store the current view-states of all forms in a map
        storeViewStates(args.currentTarget.forms);
    });
    jQuery(document).on("ajaxcomplete", function(args) {
        // the callback will be triggered for each completed JSF AJAX for the current page
        // restore all view-states of all forms which do not have one
        restoreViewStates(args.currentTarget.forms);
    });
});

var storedFormViewStates = {};

function storeViewStates(forms) {
    storedFormViewStates = {};
    for (var formIndex = 0; formIndex < forms.length; formIndex++) {
        var form = forms[formIndex];
        var formId = form.getAttribute("id");
        for (var formChildIndex = 0; formChildIndex < form.children.length; formChildIndex++) {
            var formChild = form.children[formChildIndex];
            if ((formChild.hasAttribute("name")) && (formChild.getAttribute("name").match(/^([\w]+:)?javax\.faces\.ViewState(:[0-9]+)?$/))) {
                storedFormViewStates[formId] = formChild.value;
                break;
            }
        }
    }
}

function restoreViewStates(forms) {
    for (var formIndexd = 0; formIndexd < forms.length; formIndexd++) {
        var form = forms[formIndexd];
        var formId = form.getAttribute("id");
        var viewStateFound = false;
        for (var formChildIndex = 0; formChildIndex < form.children.length; formChildIndex++) {
            var formChild = form.children[formChildIndex];
            if ((formChild.hasAttribute("name")) && (formChild.getAttribute("name").match(/^([\w]+:)?javax\.faces\.ViewState(:[0-9]+)?$/))) {
                viewStateFound = true;
                break;
            }
        }
        if ((!viewStateFound) && (storedFormViewStates.hasOwnProperty(formId))) {
            createViewState(form, storedFormViewStates[formId]);
        }
    }
}

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

Since I am not an JavaScript-expert, I guess that this may be improved further. But it definitely works on FF 17, Chromium 24, Chrome 12 and IE 11.

Two additional questions to this approach:

  • Is it feasible to use the same ViewState-value again? I.e. is JSF assigning the same ViewState-value to each form for every request/response? My approach is based on this assumption (and I have not found any related information).

  • Does someone expect any problems with this JavaScript-code or already ran into some using any browser?

MrD
  • 1,196
  • 1
  • 8
  • 22
  • could you be a little bit more specific about using your js code? I tried this in richfaces 4.5.13 with a simple script tag and it's the same result(I have to click twice) on a commandbutton (either "h:" or "a4j:" ); you also didn't mention your jquery/richfaces version – osmingo Feb 09 '16 at 17:36