001 // Copyright 2007, 2008, 2009, 2010, 2011, 2012 The Apache Software Foundation
002 //
003 // Licensed under the Apache License, Version 2.0 (the "License");
004 // you may not use this file except in compliance with the License.
005 // You may obtain a copy of the License at
006 //
007 // http://www.apache.org/licenses/LICENSE-2.0
008 //
009 // Unless required by applicable law or agreed to in writing, software
010 // distributed under the License is distributed on an "AS IS" BASIS,
011 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012 // See the License for the specific language governing permissions and
013 // limitations under the License.
014
015 package org.apache.tapestry5.corelib.components;
016
017 import org.apache.tapestry5.*;
018 import org.apache.tapestry5.annotations.Environmental;
019 import org.apache.tapestry5.annotations.Events;
020 import org.apache.tapestry5.annotations.Parameter;
021 import org.apache.tapestry5.annotations.SupportsInformalParameters;
022 import org.apache.tapestry5.corelib.SubmitMode;
023 import org.apache.tapestry5.internal.InternalConstants;
024 import org.apache.tapestry5.ioc.annotations.Inject;
025 import org.apache.tapestry5.ioc.internal.util.InternalUtils;
026 import org.apache.tapestry5.json.JSONArray;
027 import org.apache.tapestry5.services.FormSupport;
028 import org.apache.tapestry5.services.Heartbeat;
029 import org.apache.tapestry5.services.Request;
030 import org.apache.tapestry5.services.javascript.JavaScriptSupport;
031
032 /**
033 * Corresponds to <input type="submit"> or <input type="image">, a client-side element that can force the
034 * enclosing form to submit. The submit responsible for the form submission will post a notification that allows the
035 * application to know that it was the responsible entity. The notification is named "selected" and has no context.
036 *
037 * @tapestrydoc
038 */
039 @SupportsInformalParameters
040 @Events(EventConstants.SELECTED + " by default, may be overridden")
041 public class Submit implements ClientElement
042 {
043 /**
044 * If true (the default), then any notification sent by the component will be deferred until the end of the form
045 * submission (this is usually desirable). In general, this can be left as the default except when the Submit
046 * component is rendering inside a {@link Loop}, in which case defer should be bound to false (otherwise, the
047 * event context will always be the final value of the Loop).
048 */
049 @Parameter
050 private boolean defer = true;
051
052 /**
053 * The name of the event that will be triggered if this component is the cause of the form submission. The default
054 * is {@link EventConstants#SELECTED}.
055 */
056 @Parameter(allowNull = false, defaultPrefix = BindingConstants.LITERAL)
057 private String event = EventConstants.SELECTED;
058
059 /**
060 * If true, then the field will render out with a disabled attribute
061 * (to turn off client-side behavior). When the form is submitted, the
062 * bound value is evaluated again and, if true, the field's value is
063 * ignored (not even validated) and the component's events are not fired.
064 */
065 @Parameter("false")
066 private boolean disabled;
067
068 /**
069 * The list of values that will be made available to event handler method of this component when the form is
070 * submitted.
071 *
072 * @since 5.1.0.0
073 */
074 @Parameter
075 private Object[] context;
076
077 /**
078 * If provided, the component renders an input tag with type "image". Otherwise "submit".
079 *
080 * @since 5.1.0.0
081 */
082 @Parameter(defaultPrefix = BindingConstants.ASSET)
083 private Asset image;
084
085 /**
086 * Defines the mode, or client-side behavior, for the submit. The default is {@link SubmitMode#NORMAL}; clicking the
087 * button submits the form with validation. {@link SubmitMode#CANCEL} indicates the form should be submitted as a cancel,
088 * with no client-side validation. {@link SubmitMode#UNCONDITIONAL} bypasses client-side validation, but does not indicate
089 * that the form was cancelled.
090 *
091 * @see EventConstants#CANCELED
092 * @since 5.2.0
093 */
094 @Parameter(allowNull = false, defaultPrefix = BindingConstants.LITERAL)
095 private SubmitMode mode = SubmitMode.NORMAL;
096
097 @Environmental
098 private FormSupport formSupport;
099
100 @Environmental
101 private Heartbeat heartbeat;
102
103 @Inject
104 private ComponentResources resources;
105
106 @Inject
107 private Request request;
108
109 @Inject
110 private JavaScriptSupport javascriptSupport;
111
112 @SuppressWarnings("unchecked")
113 @Environmental
114 private TrackableComponentEventCallback eventCallback;
115
116 private String clientId;
117
118 private static class ProcessSubmission implements ComponentAction<Submit>
119 {
120 private final String clientId, elementName;
121
122 public ProcessSubmission(String clientId, String elementName)
123 {
124 this.clientId = clientId;
125 this.elementName = elementName;
126 }
127
128 public void execute(Submit component)
129 {
130 component.processSubmission(clientId, elementName);
131 }
132 }
133
134 public Submit()
135 {
136 }
137
138 Submit(Request request)
139 {
140 this.request = request;
141 }
142
143 void beginRender(MarkupWriter writer)
144 {
145 clientId = javascriptSupport.allocateClientId(resources);
146
147 boolean isCancel = mode == SubmitMode.CANCEL;
148
149 String name =
150 isCancel ? InternalConstants.CANCEL_NAME :
151 formSupport.allocateControlName(resources.getId());
152
153 // Save the element, to see if an id is later requested.
154
155 String type = image == null ? "submit" : "image";
156
157 writer.element("input",
158
159 "type", type,
160
161 "name", name,
162
163 "id", clientId);
164
165 if (disabled)
166 {
167 writer.attributes("disabled", "disabled");
168 }
169
170 if (image != null)
171 {
172 writer.attributes("src", image.toClientURL());
173 }
174
175 formSupport.store(this, new ProcessSubmission(clientId, name));
176
177 resources.renderInformalParameters(writer);
178
179 if (mode != SubmitMode.NORMAL)
180 {
181 javascriptSupport.addInitializerCall("enableBypassValidation", getClientId());
182 }
183 }
184
185 void afterRender(MarkupWriter writer)
186 {
187 writer.end();
188 }
189
190 void processSubmission(String clientId, String elementName)
191 {
192 if (disabled || !selected(clientId, elementName))
193 return;
194
195 Runnable sendNotification = new Runnable()
196 {
197 public void run()
198 {
199 // TAP5-1024: allow for navigation result from the event callback
200 resources.triggerEvent(event, context, eventCallback);
201 }
202 };
203
204 // When not deferred, don't wait, fire the event now (actually, at the end of the current
205 // heartbeat). This is most likely because the Submit is inside a Loop and some contextual
206 // information will change if we defer.
207
208 if (defer)
209 formSupport.defer(sendNotification);
210 else
211 heartbeat.defer(sendNotification);
212 }
213
214 private boolean selected(String clientId, String elementName)
215 {
216 // Case #1: via JavaScript, the client id is passed up.
217
218 String raw = request.getParameter(Form.SUBMITTING_ELEMENT_ID);
219
220 if (InternalUtils.isNonBlank(raw) &&
221 new JSONArray(raw).getString(0).equals(clientId))
222 {
223 return true;
224 }
225
226 // Case #2: No JavaScript, look for normal semantic (non-null value for the element's name).
227 // If configured as an image submit, look for a value for the x position. Ah, the ugliness
228 // of HTML.
229
230 String name = image == null ? elementName : elementName + ".x";
231
232 String value = request.getParameter(name);
233
234 return value != null;
235 }
236
237 /**
238 * Returns the component's client id. This must be called after the component has rendered.
239 *
240 * @return client id for the component
241 */
242 public String getClientId()
243 {
244 return clientId;
245 }
246 }