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