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