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