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