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