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