001// Copyright 2007-2013 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.*;
019import org.apache.tapestry5.corelib.SubmitMode;
020import org.apache.tapestry5.internal.util.Holder;
021import org.apache.tapestry5.ioc.annotations.Inject;
022import org.apache.tapestry5.ioc.internal.util.InternalUtils;
023import org.apache.tapestry5.json.JSONArray;
024import org.apache.tapestry5.services.FormSupport;
025import org.apache.tapestry5.services.Heartbeat;
026import org.apache.tapestry5.services.Request;
027import org.apache.tapestry5.services.javascript.JavaScriptSupport;
028
029/**
030 * Corresponds to <input type="submit"> or <input type="image">, a client-side element that can force the
031 * enclosing form to submit. The submit responsible for the form submission will post a notification that allows the
032 * application to know that it was the responsible entity. The notification is named
033 * {@linkplain EventConstants#SELECTED selected}, by default, and has no context.
034 *
035 * @tapestrydoc
036 */
037@SupportsInformalParameters
038@Events(EventConstants.SELECTED + " by default, may be overridden")
039@Import(module="t5/core/forms")
040public class Submit implements ClientElement
041{
042    /**
043     * If true (the default), then any notification sent by the component will be deferred until the end of the form
044     * submission (this is usually desirable). In general, this can be left as the default except when the Submit
045     * component is rendering inside a {@link Loop}, in which case defer should be bound to false (otherwise, the
046     * event context will always be the final value of the Loop).
047     */
048    @Parameter
049    private boolean defer = true;
050
051    /**
052     * The name of the event that will be triggered if this component is the cause of the form submission. The default
053     * is {@link EventConstants#SELECTED}.
054     */
055    @Parameter(allowNull = false, defaultPrefix = BindingConstants.LITERAL)
056    private String event = EventConstants.SELECTED;
057
058    /**
059     * If true, then the field will render out with a disabled attribute
060     * (to turn off client-side behavior). When the form is submitted, the
061     * bound value is evaluated again and, if true, the field's value is
062     * ignored (not even validated) and the component's events are not fired.
063     */
064    @Parameter("false")
065    private boolean disabled;
066
067    /**
068     * The list of values that will be made available to event handler method of this component when the form is
069     * submitted.
070     *
071     * @since 5.1.0.0
072     */
073    @Parameter
074    private Object[] context;
075
076    /**
077     * If provided, the component renders an input tag with type "image". Otherwise "submit".
078     *
079     * @since 5.1.0.0
080     */
081    @Parameter(defaultPrefix = BindingConstants.ASSET)
082    private Asset image;
083
084    /**
085     * Defines the mode, or client-side behavior, for the submit. The default is {@link SubmitMode#NORMAL}; clicking the
086     * button submits the form with validation. {@link SubmitMode#CANCEL} indicates the form should be submitted as a cancel,
087     * with no client-side validation. {@link SubmitMode#UNCONDITIONAL} bypasses client-side validation, but does not indicate
088     * that the form was cancelled.
089     *
090     * @see EventConstants#CANCELED
091     * @since 5.2.0
092     */
093    @Parameter(allowNull = false, defaultPrefix = BindingConstants.LITERAL)
094    private SubmitMode mode = SubmitMode.NORMAL;
095
096    /**
097     * CSS class for the element.
098     *
099     * @since 5.4
100     */
101    @Parameter(name = "class", defaultPrefix = BindingConstants.LITERAL,
102            value = "message:private-core-components.submit.class")
103    private String cssClass;
104
105    @Environmental
106    private FormSupport formSupport;
107
108    @Environmental
109    private Heartbeat heartbeat;
110
111    @Inject
112    private ComponentResources resources;
113
114    @Inject
115    private Request request;
116
117    @Inject
118    private JavaScriptSupport javascriptSupport;
119
120    @SuppressWarnings("unchecked")
121    @Environmental
122    private TrackableComponentEventCallback eventCallback;
123
124    private String clientId;
125
126    private static class ProcessSubmission implements ComponentAction<Submit>
127    {
128        private final String clientId, elementName;
129
130        public ProcessSubmission(String clientId, String elementName)
131        {
132            this.clientId = clientId;
133            this.elementName = elementName;
134        }
135
136        public void execute(Submit component)
137        {
138            component.processSubmission(clientId, elementName);
139        }
140    }
141
142    public Submit()
143    {
144    }
145
146    Submit(Request request)
147    {
148        this.request = request;
149    }
150
151    void beginRender(MarkupWriter writer)
152    {
153        clientId = javascriptSupport.allocateClientId(resources);
154
155        String name = formSupport.allocateControlName(resources.getId());
156
157        // Save the element, to see if an id is later requested.
158
159        String type = image == null ? "submit" : "image";
160
161        writer.element("input",
162
163                "type", type,
164
165                "name", name,
166
167                "data-submit-mode", mode.name().toLowerCase(),
168
169                "class", cssClass,
170
171                "id", clientId);
172
173        if (disabled)
174        {
175            writer.attributes("disabled", "disabled");
176        }
177
178        if (image != null)
179        {
180            writer.attributes("src", image.toClientURL());
181        }
182
183        formSupport.store(this, new ProcessSubmission(clientId, name));
184
185        resources.renderInformalParameters(writer);
186    }
187
188    void afterRender(MarkupWriter writer)
189    {
190        writer.end();
191    }
192
193    void processSubmission(String clientId, String elementName)
194    {
195        if (disabled || !selected(clientId, elementName))
196            return;
197
198        // TAP5-1658: copy the context of the current Submit instance so we trigger the event with
199        // the correct context later
200        final Holder<Object[]> currentContextHolder = Holder.create();
201        if (context != null)
202        {
203            Object[] currentContext = new Object[context.length];
204            System.arraycopy(context, 0, currentContext, 0, context.length);
205            currentContextHolder.put(currentContext);
206        }
207
208        Runnable sendNotification = new Runnable()
209        {
210            public void run()
211            {
212                // TAP5-1024: allow for navigation result from the event callback
213                resources.triggerEvent(event, currentContextHolder.get(), eventCallback);
214            }
215        };
216
217        // When not deferred, don't wait, fire the event now (actually, at the end of the current
218        // heartbeat). This is most likely because the Submit is inside a Loop and some contextual
219        // information will change if we defer.
220
221        if (defer)
222            formSupport.defer(sendNotification);
223        else
224            heartbeat.defer(sendNotification);
225    }
226
227    private boolean selected(String clientId, String elementName)
228    {
229        // Case #1: via JavaScript, the client id is passed up.
230
231        String raw = request.getParameter(Form.SUBMITTING_ELEMENT_ID);
232
233        if (InternalUtils.isNonBlank(raw) &&
234                new JSONArray(raw).getString(0).equals(clientId))
235        {
236            return true;
237        }
238
239        // Case #2: No JavaScript, look for normal semantic (non-null value for the element's name).
240        // If configured as an image submit, look for a value for the x position. Ah, the ugliness
241        // of HTML.
242
243        String name = image == null ? elementName : elementName + ".x";
244
245        String value = request.getParameter(name);
246
247        return value != null;
248    }
249
250    /**
251     * Returns the component's client id. This must be called after the component has rendered.
252     *
253     * @return client id for the component
254     */
255    public String getClientId()
256    {
257        return clientId;
258    }
259}