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