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