001// Licensed under the Apache License, Version 2.0 (the "License"); 002// you may not use this file except in compliance with the License. 003// You may obtain a copy of the License at 004// 005// http://www.apache.org/licenses/LICENSE-2.0 006// 007// Unless required by applicable law or agreed to in writing, software 008// distributed under the License is distributed on an "AS IS" BASIS, 009// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 010// See the License for the specific language governing permissions and 011// limitations under the License. 012 013package org.apache.tapestry5.corelib.components; 014 015import org.apache.tapestry5.*; 016import org.apache.tapestry5.annotations.*; 017import org.apache.tapestry5.corelib.internal.ComponentActionSink; 018import org.apache.tapestry5.corelib.internal.FormSupportAdapter; 019import org.apache.tapestry5.corelib.internal.HiddenFieldPositioner; 020import org.apache.tapestry5.dom.Element; 021import org.apache.tapestry5.internal.services.RequestConstants; 022import org.apache.tapestry5.ioc.annotations.Inject; 023import org.apache.tapestry5.ioc.annotations.Symbol; 024import org.apache.tapestry5.json.JSONObject; 025import org.apache.tapestry5.services.*; 026import org.apache.tapestry5.services.compatibility.DeprecationWarning; 027import org.apache.tapestry5.services.javascript.JavaScriptSupport; 028import org.slf4j.Logger; 029 030/** 031 * A Zone is portion of the output page designed for easy dynamic updating via Ajax or other client-side effects. A 032 * Zone renders out as a <div> element (or whatever is specified in the template) and may have content initially, 033 * or may only get its content as a result of client side activity. 034 * 035 * When a user clicks an {@link org.apache.tapestry5.corelib.components.ActionLink} whose zone parameter is set triggers a 036 * series of client-side behaviors, and an Ajax request to the server. 037 * 038 * The server side event handler can return a {@link org.apache.tapestry5.Block} or a component to render as the new 039 * content on the client side. Often, re-rendering the Zone's {@linkplain #getBody() body} is useful. Multiple 040 * client-side zones may be updated via the {@link org.apache.tapestry5.services.ajax.AjaxResponseRenderer} service. 041 * 042 * You will often want to specify the id parameter of the Zone, in addition to its Tapestry component id; this "locks 043 * down" the client-side id, so the same value is used even in later partial renders of the page (essential if the Zone 044 * is nested inside another Zone). When you specify the client-side id, it is used exactly as provided (meaning that you 045 * are responsible for ensuring that there will not be an id conflict even in the face of multiple partial renders of 046 * the page). Failure to provide an explicit id results in a new, and non-predictable, id being generated for each 047 * partial render, which will often result in client-side failures to locate the element to update when the Zone is 048 * triggered. 049 * 050 * In some cases, you may want to know (on the server side) the client id of the zone that was updated; this is passed 051 * as part of the Ajax request, as the {@link QueryParameterConstants#ZONE_ID} parameter. An example use of this would 052 * be to provide new content into a Zone that updates the same Zone, when the Zone's client-side id is dynamically 053 * allocated (rather than statically defined). In most cases, however, the programmer is responsible for assigning a 054 * specific client-side id, via the id parameter. 055 * 056 * A Zone starts and stops a {@link Heartbeat} when it renders (both normally, and when re-rendering). 057 * 058 * After the client-side content is updated, a client-side event is fired on the zone's element. The constant 059 * <code>core/events:zone.didUpdate</code> can be used to listen to the event. 060 * 061 * @tapestrydoc 062 * @see AjaxFormLoop 063 * @see FormFragment 064 */ 065@SupportsInformalParameters 066@Import(module = "t5/core/zone") 067public class Zone implements ClientBodyElement 068{ 069 /** 070 * Name of a function on the client-side Tapestry.ElementEffect object that is invoked to make the Zone's 071 * <div> visible before being updated. If not specified, then the basic "show" method is used. 072 * 073 * @deprecated In 5.4, with no specific replacement, now does nothing (see notes on client-side JavaScript events, elsewhere) 074 */ 075 @Parameter(defaultPrefix = BindingConstants.LITERAL) 076 private String show; 077 078 /** 079 * Name of a function on the client-side Tapestry.ElementEffect object that is invoked after the Zone's content has 080 * been updated. If not specified, then the basic "highlight" method is used, which performs a classic "yellow fade" 081 * to indicate to the user that and update has taken place. 082 * 083 * @deprecated In 5.4, with no specific replacement, now does nothing (see notes on client-side JavaScript events, elsewhere) 084 */ 085 @Parameter(defaultPrefix = BindingConstants.LITERAL) 086 private String update; 087 088 /** 089 * The element name to render for the zone; this defaults to the element actually used in the template, or "div" if 090 * no specific element was specified. 091 */ 092 @Parameter(required = true, allowNull = false, defaultPrefix = BindingConstants.LITERAL) 093 private String elementName; 094 095 /** 096 * If bound, then the id attribute of the rendered element will be this exact value. If not bound, then a unique id 097 * is generated for the element. 098 */ 099 @Parameter(name = "id", defaultPrefix = BindingConstants.LITERAL) 100 private String idParameter; 101 102 @Environmental 103 private JavaScriptSupport javascriptSupport; 104 105 @Inject 106 private Environment environment; 107 108 /** 109 * In prior releases, this parameter could be overridden to false to force the outer element of the rendered 110 * Zone to be non-visible. This behavior is no longer supported. 111 * 112 * @deprecated Deprecated in 5.4 with no replacement. 113 */ 114 @Parameter 115 private boolean visible; 116 117 /** 118 * if set to true, then Ajax updates related to this Zone will, when rending, use simple IDs (not namespaced ids). 119 * This is useful when the Zone contains a simple Form, as it (hopefully) ensures that the same ids used when 120 * initially rendering, and when processing the submission, are also used when re-rendering the Form (to present 121 * errors to the user). The default is false, maintaining the same behavior as in Tapestry 5.3 and earlier. 122 * 123 * @since 5.4 124 */ 125 @Parameter 126 private boolean simpleIds; 127 128 @Inject 129 private ComponentResources resources; 130 131 @Inject 132 private Heartbeat heartbeat; 133 134 @Inject 135 private Logger logger; 136 137 @Inject 138 private ClientDataEncoder clientDataEncoder; 139 140 @Inject 141 private HiddenFieldLocationRules rules; 142 143 private String clientId; 144 145 private boolean insideForm; 146 147 private HiddenFieldPositioner hiddenFieldPositioner; 148 149 private ComponentActionSink actionSink; 150 151 @Environmental(false) 152 private FormSupport formSupport; 153 154 @Inject 155 private DeprecationWarning deprecationWarning; 156 157 @Inject 158 @Symbol(SymbolConstants.COMPACT_JSON) 159 private boolean compactJSON; 160 161 String defaultElementName() 162 { 163 return resources.getElementName("div"); 164 } 165 166 void pageLoaded() 167 { 168 deprecationWarning.ignoredComponentParameters(resources, "show", "update", "visible"); 169 } 170 171 void beginRender(MarkupWriter writer) 172 { 173 clientId = resources.isBound("id") ? idParameter : javascriptSupport.allocateClientId(resources); 174 175 Element e = writer.element(elementName, 176 "id", clientId, 177 "data-container-type", "zone"); 178 179 if (simpleIds) 180 { 181 e.attribute("data-simple-ids", "true"); 182 } 183 184 resources.renderInformalParameters(writer); 185 186 insideForm = formSupport != null; 187 188 if (insideForm) 189 { 190 JSONObject parameters = new JSONObject(RequestConstants.FORM_CLIENTID_PARAMETER, formSupport.getClientId(), 191 RequestConstants.FORM_COMPONENTID_PARAMETER, formSupport.getFormComponentId()); 192 193 e.attribute("data-zone-parameters", 194 parameters.toString(compactJSON)); 195 196 hiddenFieldPositioner = new HiddenFieldPositioner(writer, rules); 197 198 actionSink = new ComponentActionSink(logger, clientDataEncoder); 199 200 environment.push(FormSupport.class, new FormSupportAdapter(formSupport) 201 { 202 @Override 203 public <T> void store(T component, ComponentAction<T> action) 204 { 205 actionSink.store(component, action); 206 } 207 208 @Override 209 public <T> void storeCancel(T component, ComponentAction<T> action) 210 { 211 actionSink.storeCancel(component, action); 212 } 213 214 @Override 215 public <T> void storeAndExecute(T component, ComponentAction<T> action) 216 { 217 store(component, action); 218 219 action.execute(component); 220 } 221 222 }); 223 } 224 225 heartbeat.begin(); 226 } 227 228 void afterRender(MarkupWriter writer) 229 { 230 heartbeat.end(); 231 232 if (insideForm) 233 { 234 environment.pop(FormSupport.class); 235 236 if (actionSink.isEmpty()) 237 { 238 hiddenFieldPositioner.discard(); 239 } else 240 { 241 hiddenFieldPositioner.getElement().attributes("type", "hidden", 242 243 "name", Form.FORM_DATA, 244 245 "value", actionSink.getClientData()); 246 } 247 } 248 249 writer.end(); // div 250 } 251 252 /** 253 * The client id of the Zone; this is set when the Zone renders and will either be the value bound to the id 254 * parameter, or an allocated unique id. When the id parameter is bound, this value is always accurate. 255 * When the id parameter is not bound, the clientId is set during the {@linkplain BeginRender begin render phase} 256 * and will be null or inaccurate before then. 257 * 258 * @return client-side element id 259 */ 260 public String getClientId() 261 { 262 if (resources.isBound("id")) 263 return idParameter; 264 265 // TAP4-2342. I know this won't work with a Zone with no given clientId and that was already 266 // via AJAX inside an outer Zone, but it's still better than nothing. 267 if (clientId == null) 268 { 269 clientId = resources.getId(); 270 } 271 272 return clientId; 273 } 274 275 /** 276 * Returns the zone's body (the content enclosed by its start and end tags). This is often used as part of an Ajax 277 * partial page render to update the client with a fresh render of the content inside the zone. 278 * 279 * @return the zone's body as a Block 280 */ 281 public Block getBody() 282 { 283 return resources.getBody(); 284 } 285}