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