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