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     * &lt;div&gt; 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}