    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
    015    package org.apache.tapestry5.corelib.components;
    016
    017    import org.apache.tapestry5.*;
    018    import org.apache.tapestry5.annotations.Environmental;
    019    import org.apache.tapestry5.annotations.Events;
    020    import org.apache.tapestry5.annotations.Parameter;
    021    import org.apache.tapestry5.annotations.SupportsInformalParameters;
    022    import org.apache.tapestry5.corelib.SubmitMode;
    023    import org.apache.tapestry5.internal.InternalConstants;
    024    import org.apache.tapestry5.ioc.annotations.Inject;
    025    import org.apache.tapestry5.ioc.internal.util.InternalUtils;
    026    import org.apache.tapestry5.json.JSONArray;
    027    import org.apache.tapestry5.services.FormSupport;
    028    import org.apache.tapestry5.services.Heartbeat;
    029    import org.apache.tapestry5.services.Request;
    030    import org.apache.tapestry5.services.javascript.JavaScriptSupport;
    031
    032    /**
    033     * Corresponds to &lt;input type="submit"&gt; or &lt;input type="image"&gt;, 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")
    041    public 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    }