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