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}