web-dev-qa-db-fra.com

h: commandButton / h: commandLink ne fonctionne pas au premier clic, ne fonctionne qu'au deuxième clic

Nous avons un menu de navigation ajax qui met à jour une inclusion dynamique. Les fichiers include ont chacun leurs propres formulaires.

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

Cela fonctionne correctement, mais aucun bouton de commande du fichier inclus ne fonctionne au premier clic. Cela ne fonctionne qu'au deuxième clic.

J'ai trouvé cette question méthode commandButton/commandLink/ajax action/listener non invoquée ou valeur d'entrée non mise à jour et mon problème est décrit au point 9. Je comprends que je dois inclure explicitement l'ID du <h:form> dans l'inclusion dans le <f:ajax render> pour le résoudre.

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

Dans mon cas, cependant, l'ID du formulaire n'est pas connu à l'avance. De plus, ce formulaire ne sera pas initialement disponible dans le contexte.

Y a-t-il une solution au scénario ci-dessus?

23
Skyler Hays

Vous pouvez utiliser le script suivant pour corriger le bogue Mojarra 2.0/2.1/2.2 (remarque: cela ne se manifeste pas dans MyFaces). Ce script créera le champ caché javax.faces.ViewState Pour les formulaires qui n'ont récupéré aucun état d'affichage après la mise à jour ajax.

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

Il suffit de l'inclure comme <h:outputScript name="some.js" target="head"> Dans le <h:body> De la page d'erreur. Si vous ne pouvez pas garantir que la page en question utilise JSF <f:ajax>, Ce qui déclencherait l'inclusion automatique de jsf.js, Alors vous voudrez peut-être ajouter une if (typeof jsf !== 'undefined') vérification supplémentaire avant l'appel de jsf.ajax.addOnEvent(), ou pour l'inclure explicitement par

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

Notez que jsf.ajax.addOnEvent Ne couvre que le JSF standard <f:ajax> Et non par ex. PrimeFaces <p:ajax> Ou <p:commandXxx> Comme ils utilisent sous les couvertures jQuery pour le travail. Pour couvrir également les demandes ajax PrimeFaces, ajoutez ce qui suit:

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

Mise à jour si vous utilisez la bibliothèque d'utilitaires JSF OmniFaces , il est bon de savoir que ce qui précède fait depuis 1.7 partie d'OmniFaces . Il s'agit simplement de déclarer le script suivant dans le <h:body>. Voir aussi vitrine .

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

Merci à BalusC car sa réponse est vraiment super (comme d'habitude :)). Mais je dois ajouter que cette approche ne fonctionne pas pour les requêtes ajax provenant de RichFaces 4. Ils ont plusieurs problèmes avec ajax et l'un d'eux est que les gestionnaires JSF-ajax ne sont pas invoqués. Ainsi, lorsque vous effectuez un rendu sur un conteneur contenant un formulaire à l'aide de composants RichFaces, la fonction fixViewState n'est pas appelée et le ViewState est alors manquant.

Dans le RichFaces Component Reference , ils indiquent comment enregistrer les rappels pour "leurs" demandes ajax (en fait, ils utilisent jQuery pour accrocher toutes les demandes ajax). Mais en utilisant cela, je n'ai pas pu obtenir la réponse ajax qui est utilisée par le script de BalusC ci-dessus pour obtenir le ViewState.

Donc, basé sur le correctif de BalusC, j'en ai élaboré un très similaire. Mon script enregistre toutes les valeurs ViewState de tous les formulaires sur la page actuelle dans une carte avant que la demande ajax ne soit traitée par le navigateur. Après la mise à jour du DOM, j'essaie de restaurer tous les ViewStates qui ont été enregistrés auparavant (pour tous les formulaires auxquels il manque le ViewState maintenant).

Passez:

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

Étant donné que je ne suis pas un expert JavaScript, je suppose que cela peut être amélioré davantage. Mais cela fonctionne certainement sur FF 17, Chromium 24, Chrome 12 et IE 11.

Deux questions supplémentaires à cette approche:

  • Est-il possible d'utiliser à nouveau la même valeur ViewState? C'est à dire. JSF attribue-t-il la même valeur ViewState à chaque formulaire pour chaque demande/réponse? Mon approche est basée sur cette hypothèse (et je n'ai trouvé aucune information connexe).

  • Est-ce que quelqu'un s'attend à des problèmes avec ce code JavaScript ou en a déjà rencontré certains à l'aide d'un navigateur?

5
MrD