/
ResetInputAjaxActionListener.java
200 lines (178 loc) · 10 KB
/
ResetInputAjaxActionListener.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
/*
* Copyright 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
*
* https://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 jakarta.faces.component.visit.VisitContext.ALL_IDS;
import static jakarta.faces.component.visit.VisitContext.createVisitContext;
import static jakarta.faces.component.visit.VisitHint.SKIP_TRANSIENT;
import static jakarta.faces.component.visit.VisitHint.SKIP_UNRENDERED;
import static jakarta.faces.component.visit.VisitResult.ACCEPT;
import static jakarta.faces.component.visit.VisitResult.REJECT;
import static jakarta.faces.event.PhaseId.INVOKE_APPLICATION;
import java.util.Collection;
import java.util.EnumSet;
import java.util.Set;
import jakarta.faces.component.EditableValueHolder;
import jakarta.faces.component.visit.VisitCallback;
import jakarta.faces.component.visit.VisitHint;
import jakarta.faces.context.FacesContext;
import jakarta.faces.context.PartialViewContext;
import jakarta.faces.event.ActionEvent;
import jakarta.faces.event.ActionListener;
import jakarta.faces.event.AjaxBehaviorListener;
import jakarta.faces.event.PhaseEvent;
import jakarta.faces.event.SystemEventListener;
/**
* <p>
* 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.
* <p>
* How does it work? First, here are some Faces facts:
* <ul>
* <li>When Faces validation succeeds for a particular input component during the validations phase, then the submitted
* value is set to <code>null</code> and the validated value is set as local value of the input component.
* <li>When Faces validation fails for a particular input component during the validations phase, then the submitted
* value is kept in the input component.
* <li>When at least one input component is invalid after the validations phase, then Faces will not update the model
* values for any of the input components. Faces will directly proceed to render response phase.
* <li>When Faces renders input components, then it will first test if the submitted value is not <code>null</code> and
* then display it, else if the local value is not <code>null</code> and then display it, else it will display the
* model value.
* <li>As long as you're interacting with the same Faces view, you're dealing with the same component state.
* </ul>
* <p>
* 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 Faces to display the model value which was edited during invoke action. Otherwise Faces will
* still display its local value as it was during the validation failure and keep them in an invalidated state.
* <p>
* The {@link ResetInputAjaxActionListener} is designed to solve exactly this problem. There are basically three ways
* to configure and use it:
* <ul>
* <li><p>Register it as <code><phase-listener></code> in <code>faces-config.xml</code>. It'll be applied
* to <strong>every single</strong> ajax action throughout the webapp, on both <code>UIInput</code> and
* <code>UICommand</code> components.
* <pre>
* <lifecycle>
* <phase-listener>org.omnifaces.eventlistener.ResetInputAjaxActionListener</phase-listener>
* </lifecycle>
* </pre>
* <li><p><i>Or</i> register it as <code><action-listener></code> in <code>faces-config.xml</code>. It'll
* <strong>only</strong> be applied to ajax actions which are invoked by an <code>UICommand</code> component such as
* <code><h:commandButton></code> and <code><h:commandLink></code>.
* <pre>
* <application>
* <action-listener>org.omnifaces.eventlistener.ResetInputAjaxActionListener</action-listener>
* </application>
* </pre>
* <li><p><i>Or</i> register it as <code><f:actionListener></code> on the invidivual <code>UICommand</code>
* 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 <code>UIInput</code> components using the standard Faces tags.
* <pre>
* <h:commandButton value="Update" action="#{bean.updateOtherInputs}">
* <f:ajax execute="currentInputs" render="otherInputs" />
* <f:actionListener type="org.omnifaces.eventlistener.ResetInputAjaxActionListener" />
* </h:commandButton>
* </pre>
* </ul>
* <p>
* This works with standard Faces, PrimeFaces and RichFaces actions. Only for RichFaces there's a reflection hack,
* because its <code>ExtendedPartialViewContextImpl</code> <i>always</i> returns an empty collection for render IDs.
* See also <a href="https://issues.jboss.org/browse/RF-11112">RF issue 11112</a>.
* <p>
* Design notice: being a phase listener was mandatory in order to be able to hook on every single ajax action as
* standard Faces 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 <code><f:ajax></code> tag basis, so that it also get applied to
* ajax actions in <code>UIInput</code> components. There are ways with help of {@link SystemEventListener}, but it
* ended up to be too clumsy.
*
* <p><strong>See also</strong>:
* <br><a href="https://github.com/jakartaee/faces/issues/1060">Faces spec issue 1060</a>
*
* @author Bauke Scholtz
*/
public class ResetInputAjaxActionListener extends DefaultPhaseListener implements ActionListener {
// Constants ------------------------------------------------------------------------------------------------------
private static final long serialVersionUID = 1L;
private static final Set<VisitHint> VISIT_HINTS = EnumSet.of(SKIP_TRANSIENT, SKIP_UNRENDERED);
private static final VisitCallback VISIT_CALLBACK = (context, target) -> {
FacesContext facesContext = context.getFacesContext();
if (facesContext.getPartialViewContext().getExecuteIds().contains(target.getClientId(facesContext))) {
return REJECT;
}
if (target instanceof EditableValueHolder) {
((EditableValueHolder) target).resetValue();
}
else if (!ALL_IDS.equals(context.getIdsToVisit())) {
// Render ID didn't specifically point an EditableValueHolder. Visit all children as well.
target.visitTree(createVisitContext(facesContext, null, context.getHints()), ResetInputAjaxActionListener.VISIT_CALLBACK);
}
return ACCEPT;
};
// Variables ------------------------------------------------------------------------------------------------------
private transient ActionListener wrapped;
// Constructors ---------------------------------------------------------------------------------------------------
/**
* Construct a new reset input ajax action listener. This constructor will be used when specifying the action
* listener by <code><f:actionListener></code> or when registering as <code><phase-listener></code> in
* <code>faces-config.xml</code>.
*/
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 <code><action-listener></code> in <code>faces-config.xml</code>.
* @param wrapped The wrapped action listener.
*/
public ResetInputAjaxActionListener(ActionListener wrapped) {
super(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 <strong>all</strong> 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 <code>null</code> component. This
* would however indicate a bug in the concrete {@link PartialViewContext} implementation which is been used.
*/
@Override
public void processAction(ActionEvent event) {
FacesContext context = FacesContext.getCurrentInstance();
PartialViewContext partialViewContext = context.getPartialViewContext();
if (partialViewContext.isAjaxRequest()) {
Collection<String> renderIds = partialViewContext.getRenderIds();
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);
}
}
}