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