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