h:commandButton/h:commandLink在第一次点击时无效,只有在第二次点击时才有效。

26

我们有一个使用Ajax实现的导航菜单,它更新一个动态包含(include)。每个包含文件都有自己的表单(form)。

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

它能够正确地工作,但是包含文件中的任何命令按钮在第一次点击时都不起作用。它仅在第二次和第四次点击时才有效。

我在这个问题commandButton/commandLink/ajax action/listener method not invoked or input value not updated找到了答案,我的问题在第9点中描述。 我明白我需要在<f:ajax render>中的include中显式包含<h:form>的ID来解决这个问题。

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

然而在我的情况下,表单ID事先是未知的。此外,该表单最初在上下文中也不可用。

针对上述场景是否有任何解决方案?

2个回答

46
您可以使用以下脚本来修复Mojarra 2.0/2.1/2.2的bug(注意:这在MyFaces中不会表现出来)。此脚本将为未在ajax更新后检索任何视图状态的表单创建javax.faces.ViewState隐藏字段。
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);
        }
    }
}

只需在错误页面的<h:body>内包含它,如下所示:<h:outputScript name="some.js" target="head">。如果不能保证所涉及的页面使用JSF <f:ajax>,以触发自动包含jsf.js,那么您可能需要在jsf.ajax.addOnEvent()调用之前添加一个额外的if(typeof jsf !== 'undefined')检查,或者通过以下方式显式地包含它:

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

请注意,jsf.ajax.addOnEvent仅适用于标准的JSF <f:ajax>,而不适用于例如PrimeFaces <p:ajax><p:commandXxx>,因为它们在底层使用jQuery进行操作。要同时处理PrimeFaces ajax请求,请添加以下内容:

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

更新 如果你正在使用JSF实用库OmniFaces,那么很好知道,自1.7版本以来,上面的内容已经成为OmniFaces的一部分。只需要在<h:body>中声明以下脚本即可。还可以参见演示文稿.

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

1
@Thang:是的,但是在IE6-8中,setAttribute("name", value)对于输入元素不起作用,这正是为什么首先需要这个hack的原因。另请参见例如http://webbugtrack.blogspot.com/2007/10/bug-235-createelement-is-broken-in-ie.html。 - BalusC
我在PrimeFaces 5.1中仍然遇到这个问题。 - adranale
1
我将其用作 <ui:define name="left">