org.omnifaces.eventlistener.ResetInputAjaxActionListener Maven / Gradle / Ivy
/*
* Copyright 2012 OmniFaces.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
package org.omnifaces.eventlistener;
import static javax.faces.component.visit.VisitContext.createVisitContext;
import java.util.Collection;
import java.util.EnumSet;
import java.util.Set;
import javax.faces.component.EditableValueHolder;
import javax.faces.component.UIComponent;
import javax.faces.component.visit.VisitCallback;
import javax.faces.component.visit.VisitContext;
import javax.faces.component.visit.VisitHint;
import javax.faces.component.visit.VisitResult;
import javax.faces.context.FacesContext;
import javax.faces.context.PartialViewContext;
import javax.faces.event.AbortProcessingException;
import javax.faces.event.ActionEvent;
import javax.faces.event.ActionListener;
import javax.faces.event.AjaxBehaviorListener;
import javax.faces.event.PhaseEvent;
import javax.faces.event.PhaseId;
import javax.faces.event.SystemEventListener;
import org.omnifaces.util.Hacks;
/**
*
* The {@link ResetInputAjaxActionListener} will reset input fields which are not executed during ajax submit, but which
* are rendered/updated during ajax response. This will prevent those input fields to remain in an invalidated state
* because of a validation failure during a previous request. This is very useful for cases where you need to update one
* form from another form by for example a modal dialog, or when you need a cancel/clear button.
*
* How does it work? First, here are some JSF facts:
*
* - When JSF validation succeeds for a particular input component during the validations phase, then the submitted
* value is set to
null
and the validated value is set as local value of the input component.
* - When JSF validation fails for a particular input component during the validations phase, then the submitted
* value is kept in the input component.
*
- When at least one input component is invalid after the validations phase, then JSF will not update the model
* values for any of the input components. JSF will directly proceed to render response phase.
*
- When JSF renders input components, then it will first test if the submitted value is not
null
and
* then display it, else if the local value is not null
and then display it, else it will display the
* model value.
* - As long as you're interacting with the same JSF view, you're dealing with the same component state.
*
*
* So, when the validation has failed for a particular form submit and you happen to need to update the values of input
* fields by a different ajax action or even a different ajax form (e.g. populating a field depending on a dropdown
* selection or the result of some modal dialog form, etc), then you basically need to reset the target input
* components in order to get JSF to display the model value which was edited during invoke action. Otherwise JSF will
* still display its local value as it was during the validation failure and keep them in an invalidated state.
*
* The {@link ResetInputAjaxActionListener} is designed to solve exactly this problem. There are basically three ways
* to configure and use it:
*
* Register it as <phase-listener>
in faces-config.xml
. It'll be applied
* to every single ajax action throughout the webapp, on both UIInput
and
* UICommand
components.
*
* <lifecycle>
* <phase-listener>org.omnifaces.eventlistener.ResetInputAjaxActionListener</phase-listener>
* </lifecycle>
*
* Or register it as <action-listener>
in faces-config.xml
. It'll
* only be applied to ajax actions which are invoked by an UICommand
component such as
* <h:commandButton>
and <h:commandLink>
.
*
* <application>
* <action-listener>org.omnifaces.eventlistener.ResetInputAjaxActionListener</action-listener>
* </application>
*
* Or register it as <f:actionListener>
on the invidivual UICommand
* components where this action listener is absolutely necessary to solve the concrete problem. Note that it isn't
* possible to register it on the individual UIInput
components using the standard JSF tags.
*
* <h:commandButton value="Update" action="#{bean.updateOtherInputs}">
* <f:ajax execute="currentInputs" render="otherInputs" />
* <f:actionListener type="org.omnifaces.eventlistener.ResetInputAjaxActionListener" />
* </h:commandButton>
*
*
*
* This works with standard JSF, PrimeFaces and RichFaces actions. Only for RichFaces there's a reflection hack,
* because its ExtendedPartialViewContextImpl
always returns an empty collection for render IDs.
* See also RF issue 11112.
*
* Design notice: being a phase listener was mandatory in order to be able to hook on every single ajax action as
* standard JSF API does not (seem to?) offer any ways to register some kind of {@link AjaxBehaviorListener} in an
* application wide basis, let alone on a per <f:ajax>
tag basis, so that it also get applied to
* ajax actions in UIInput
components. There are ways with help of {@link SystemEventListener}, but it
* ended up to be too clumsy.
*
*
See also:
*
JSF spec issue 1060
*
* @author Bauke Scholtz
*/
public class ResetInputAjaxActionListener extends DefaultPhaseListener implements ActionListener {
// Constants ------------------------------------------------------------------------------------------------------
private static final long serialVersionUID = -5317382021715077662L;
private static final Set VISIT_HINTS = EnumSet.of(VisitHint.SKIP_TRANSIENT, VisitHint.SKIP_UNRENDERED);
private static final VisitCallback VISIT_CALLBACK = new VisitCallback() {
@Override
public VisitResult visit(VisitContext context, UIComponent target) {
FacesContext facesContext = context.getFacesContext();
Collection executeIds = facesContext.getPartialViewContext().getExecuteIds();
if (executeIds.contains(target.getClientId(facesContext))) {
return VisitResult.REJECT;
}
if (target instanceof EditableValueHolder) {
((EditableValueHolder) target).resetValue();
}
else if (context.getIdsToVisit() != VisitContext.ALL_IDS) {
// Render ID didn't specifically point an EditableValueHolder. Visit all children as well.
target.visitTree(createVisitContext(facesContext, null, context.getHints()), VISIT_CALLBACK);
}
return VisitResult.ACCEPT;
}
};
// Variables ------------------------------------------------------------------------------------------------------
private ActionListener wrapped;
// Constructors ---------------------------------------------------------------------------------------------------
/**
* Construct a new reset input ajax action listener. This constructor will be used when specifying the action
* listener by <f:actionListener>
or when registering as <phase-listener>
in
* faces-config.xml
.
*/
public ResetInputAjaxActionListener() {
this(null);
}
/**
* Construct a new reset input ajax action listener around the given wrapped action listener. This constructor
* will be used when registering as <action-listener>
in faces-config.xml
.
* @param wrapped The wrapped action listener.
*/
public ResetInputAjaxActionListener(ActionListener wrapped) {
super(PhaseId.INVOKE_APPLICATION);
this.wrapped = wrapped;
}
// Actions --------------------------------------------------------------------------------------------------------
/**
* Delegate to the {@link #processAction(ActionEvent)} method when this action listener is been registered as a
* phase listener so that it get applied on all ajax requests.
* @see #processAction(ActionEvent)
*/
@Override
public void beforePhase(PhaseEvent event) {
processAction(null);
}
/**
* Handle the reset input action as follows, only and only if the current request is an ajax request and the
* {@link PartialViewContext#getRenderIds()} does not return an empty collection nor is the same as
* {@link PartialViewContext#getExecuteIds()}: find all {@link EditableValueHolder} components based on
* {@link PartialViewContext#getRenderIds()} and if the component is not covered by
* {@link PartialViewContext#getExecuteIds()}, then invoke {@link EditableValueHolder#resetValue()} on the
* component.
* @throws IllegalArgumentException When one of the client IDs resolved to a null
component. This
* would however indicate a bug in the concrete {@link PartialViewContext} implementation which is been used.
*/
@Override
public void processAction(ActionEvent event) throws AbortProcessingException {
FacesContext context = FacesContext.getCurrentInstance();
PartialViewContext partialViewContext = context.getPartialViewContext();
if (partialViewContext.isAjaxRequest()) {
Collection renderIds = getRenderIds(partialViewContext);
if (!renderIds.isEmpty() && !partialViewContext.getExecuteIds().containsAll(renderIds)) {
context.getViewRoot().visitTree(createVisitContext(context, renderIds, VISIT_HINTS), VISIT_CALLBACK);
}
}
if (wrapped != null && event != null) {
wrapped.processAction(event);
}
}
// Helpers --------------------------------------------------------------------------------------------------------
/**
* Helper method with RichFaces4 hack to return the proper render IDs from the given partial view context.
* @param partialViewContext The partial view context to return the render IDs for.
* @return The render IDs.
*/
private static Collection getRenderIds(PartialViewContext partialViewContext) {
Collection renderIds = partialViewContext.getRenderIds();
// WARNING: START OF HACK! ------------------------------------------------------------------------------------
// HACK for RichFaces4 because its ExtendedPartialViewContextImpl class doesn't return its componentRenderIds
// property on getRenderIds() call when the action is executed using a RichFaces-specific command button/link.
// See also https://issues.jboss.org/browse/RF-11112
if (renderIds.isEmpty() && Hacks.isRichFacesInstalled()) {
renderIds = Hacks.getRichFacesRenderIds();
}
// END OF HACK ------------------------------------------------------------------------------------------------
return renderIds;
}
}