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}