    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.http.services.Request;
    021import org.apache.tapestry5.internal.util.Holder;
    022import org.apache.tapestry5.ioc.annotations.Inject;
    023import org.apache.tapestry5.ioc.internal.util.InternalUtils;
    024import org.apache.tapestry5.json.JSONArray;
    025import org.apache.tapestry5.services.FormSupport;
    026import org.apache.tapestry5.services.Heartbeat;
    027import org.apache.tapestry5.services.javascript.JavaScriptSupport;
    028
    029/**
    030 * Corresponds to &lt;input type="submit"&gt; or &lt;input type="image"&gt;, 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}