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 }