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 <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")
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    }