    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.Environmental;
    017import org.apache.tapestry5.annotations.HeartbeatDeferred;
    018import org.apache.tapestry5.annotations.Import;
    019import org.apache.tapestry5.annotations.Parameter;
    020import org.apache.tapestry5.annotations.SupportsInformalParameters;
    021import org.apache.tapestry5.corelib.internal.ComponentActionSink;
    022import org.apache.tapestry5.corelib.internal.FormSupportAdapter;
    023import org.apache.tapestry5.corelib.internal.HiddenFieldPositioner;
    024import org.apache.tapestry5.corelib.mixins.TriggerFragment;
    025import org.apache.tapestry5.dom.Element;
    026import org.apache.tapestry5.ioc.annotations.Inject;
    027import org.apache.tapestry5.services.ClientDataEncoder;
    028import org.apache.tapestry5.services.Environment;
    029import org.apache.tapestry5.services.FormSupport;
    030import org.apache.tapestry5.services.HiddenFieldLocationRules;
    031import org.apache.tapestry5.services.javascript.JavaScriptSupport;
    032import org.slf4j.Logger;
    033
    034/**
    035 * A FormFragment is a portion of a Form that may be selectively displayed. Form elements inside a FormFragment will
    036 * automatically bypass validation when the fragment is invisible. The trick is to also bypass server-side form
    037 * processing for such fields when the form is submitted; client-side logic "removes" the
    038 * {@link org.apache.tapestry5.corelib.components.Form#FORM_DATA form data} for the fragment if it is invisible when the
    039 * form is submitted (e.g., the hidden form field is disabled);
    040 * alternately, client-side logic can simply remove the form fragment element (including its visible and
    041 * hidden fields) to prevent server-side processing.
    042 *
    043 * The client-side element will now listen to two new events defined by client-side constants:
    044 * <dl>
    045 * <dt>core/events.formfragment.changeVisibility or Tapestry.CHANGE_VISIBILITY_EVENT</dt>
    046 * <dd>Change the visibility as per the event memo's visibility property. When the visibility changes, the correct
    047 * animation is executed.</dd>
    048 * <dt>core/events.formfragment.remove or Tapestry.HIDE_AND_REMOVE_EVENT</dt>
    049 * <dd>Hides the element, then removes it from the DOM entirely.
    050 * </dl>
    051 *
    052 * @tapestrydoc
    053 * @see TriggerFragment
    054 * @see Form
    055 */
    056@SupportsInformalParameters
    057@Import(module = "t5/core/form-fragment")
    058public class FormFragment implements ClientElement
    059{
    060    /**
    061     * Determines if the fragment is initially visible or initially invisible (the default). This is only used when
    062     * rendering; when the form is submitted, the hidden field value is used to determine whether the elements within
    063     * the fragment should be processed (or ignored if still invisible).
    064     */
    065    @Parameter
    066    private boolean visible;
    067
    068    /**
    069     * If true, then the fragment submits the values from fields it contains <em>even if</em> the fragment is not
    070     * visible.
    071     * The default is to omit values from fields when the enclosing fragment is non visible.
    072     *
    073     * @since 5.2.0
    074     */
    075    @Parameter
    076    private boolean alwaysSubmit;
    077
    078    /**
    079     * Name of a function on the client-side Tapestry.ElementEffect object that is invoked to make the fragment visible.
    080     * This is no longer used.
    081     *
    082     * @deprecated Deprecated in 5.4; clients that wish to animate should handle the <code>events.element.didShow</code> client-side event.
    083     */
    084    @Parameter(defaultPrefix = BindingConstants.LITERAL)
    085    private String show;
    086
    087    /**
    088     * Name of a function on the client-side Tapestry.ElementEffect object that is invoked when the fragment is to be
    089     * hidden.  This is no longer used.
    090     *
    091     * @deprecated Deprecated in 5.4; clients that wish to animate should handle the <code>events.element.didHide</code> client-side event.
    092     */
    093    @Parameter(defaultPrefix = BindingConstants.LITERAL)
    094    private String hide;
    095
    096    /**
    097     * The element to render for each iteration of the loop. The default comes from the template, or "div" if the
    098     * template did not specific an element.
    099     */
    100    @Parameter(defaultPrefix = BindingConstants.LITERAL)
    101    private String element;
    102
    103    /**
    104     * If bound, then the id attribute of the rendered element will be this exact value. If not bound, then a unique id
    105     * is generated for the element.
    106     */
    107    @Parameter(name = "id", defaultPrefix = BindingConstants.LITERAL)
    108    private String idParameter;
    109
    110    /**
    111     * The name of a javascript function that overrides the default visibility search bound.
    112     * Tapestry normally ensures that not only the form fragment but all parent elements up to the containing body
    113     * are visible when determining whether to submit the contents of a form fragment.  This behavior can be modified by
    114     * supplying a javascript function that receives the "current" element in the chain.  Returning true will stop the
    115     * search (and report ElementWrapper.deepVisible() as true).  Returning false will continue the search up the chain.
    116     *
    117     * @since 5.3
    118     * @deprecated Deprecated in 5.4 with no current replacement.
    119     */
    120    @Parameter(defaultPrefix = BindingConstants.LITERAL, allowNull = false)
    121    private String visibleBound;
    122
    123    @Inject
    124    private Environment environment;
    125
    126    @Environmental
    127    private JavaScriptSupport javascriptSupport;
    128
    129    @Inject
    130    private ComponentResources resources;
    131
    132    private String clientId;
    133
    134    private ComponentActionSink componentActions;
    135
    136    @Inject
    137    private Logger logger;
    138
    139    @Inject
    140    private HiddenFieldLocationRules rules;
    141
    142    private HiddenFieldPositioner hiddenFieldPositioner;
    143
    144    @Inject
    145    private ClientDataEncoder clientDataEncoder;
    146
    147    String defaultElement()
    148    {
    149        return resources.getElementName("div");
    150    }
    151
    152    /**
    153     * Renders a &lt;div&gt; tag and provides an override of the {@link org.apache.tapestry5.services.FormSupport}
    154     * environmental.
    155     */
    156    void beginRender(MarkupWriter writer)
    157    {
    158        FormSupport formSupport = environment.peekRequired(FormSupport.class);
    159
    160        String clientId = getClientId();
    161
    162        hiddenFieldPositioner = new HiddenFieldPositioner(writer, rules);
    163
    164        Element element = writer.element(this.element,
    165                "id", clientId,
    166                "data-component-type", "core/FormFragment");
    167
    168        if (alwaysSubmit) {
    169            element.attribute("data-always-submit", "true");
    170        }
    171
    172        resources.renderInformalParameters(writer);
    173
    174        if (!visible)
    175        {
    176            element.attribute("style", "display: none;");
    177
    178            if (!alwaysSubmit)
    179            {
    180                javascriptSupport.require("t5/core/form-fragment").invoke("hide").with(clientId);
    181            }
    182        }
    183
    184        componentActions = new ComponentActionSink(logger, clientDataEncoder);
    185
    186        // Here's the magic of environmentals ... we can create a wrapper around
    187        // the normal FormSupport environmental that intercepts some of the behavior.
    188        // Here we're setting aside all the actions inside the FormFragment so that we
    189        // can control whether those actions occur when the form is submitted.
    190
    191        FormSupport override = new FormSupportAdapter(formSupport)
    192        {
    193            @Override
    194            public <T> void store(T component, ComponentAction<T> action)
    195            {
    196                componentActions.store(component, action);
    197            }
    198
    199            @Override
    200            public <T> void storeCancel(T component, ComponentAction<T> action)
    201            {
    202                componentActions.storeCancel(component, action);
    203            }
    204
    205            @Override
    206            public <T> void storeAndExecute(T component, ComponentAction<T> action)
    207            {
    208                componentActions.store(component, action);
    209
    210                action.execute(component);
    211            }
    212        };
    213
    214        // Tada! Now all the enclosed components will use our override of FormSupport,
    215        // until we pop it off.
    216
    217        environment.push(FormSupport.class, override);
    218
    219    }
    220
    221    /**
    222     * Closes the &lt;div&gt; tag and pops off the {@link org.apache.tapestry5.services.FormSupport} environmental
    223     * override.
    224     *
    225     * @param writer
    226     */
    227    void afterRender(MarkupWriter writer)
    228    {
    229        Element hidden = hiddenFieldPositioner.getElement();
    230
    231        hidden.attributes("type", "hidden",
    232
    233                "name", Form.FORM_DATA,
    234
    235                "value", componentActions.getClientData());
    236
    237        writer.end(); // div
    238
    239
    240        environment.pop(FormSupport.class);
    241
    242        resetClientId();
    243    }
    244
    245    @HeartbeatDeferred
    246    void resetClientId()
    247    {
    248        clientId = null;
    249    }
    250
    251    public String getClientId()
    252    {
    253        if (clientId == null)
    254        {
    255            clientId = resources.isBound("id") ? idParameter : javascriptSupport.allocateClientId(resources);
    256        }
    257        return clientId;
    258    }
    259}