001    // Copyright 2007, 2008, 2009, 2010, 2011 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.json.JSONArray;
026    import org.apache.tapestry5.services.FormSupport;
027    import org.apache.tapestry5.services.Heartbeat;
028    import org.apache.tapestry5.services.Request;
029    import org.apache.tapestry5.services.javascript.JavaScriptSupport;
030    
031    /**
032     * Corresponds to <input type="submit"> or <input type="image">, a client-side element that can force the
033     * enclosing form to submit. The submit responsible for the form submission will post a notification that allows the
034     * application to know that it was the responsible entity. The notification is named "selected" and has no context.
035     *
036     * @tapestrydoc
037     */
038    @SupportsInformalParameters
039    @Events(EventConstants.SELECTED + " by default, may be overridden")
040    public 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 client-side validation
087         * should be omitted (though server-side validation still occurs).
088         *
089         * @since 5.2.0
090         */
091        @Parameter(allowNull = false, defaultPrefix = BindingConstants.LITERAL)
092        private SubmitMode mode = SubmitMode.NORMAL;
093    
094        @Environmental
095        private FormSupport formSupport;
096    
097        @Environmental
098        private Heartbeat heartbeat;
099    
100        @Inject
101        private ComponentResources resources;
102    
103        @Inject
104        private Request request;
105    
106        @Inject
107        private JavaScriptSupport javascriptSupport;
108    
109        @SuppressWarnings("unchecked")
110        @Environmental
111        private TrackableComponentEventCallback eventCallback;
112    
113        private String clientId;
114    
115        private static class ProcessSubmission implements ComponentAction<Submit>
116        {
117            private final String clientId, elementName;
118    
119            public ProcessSubmission(String clientId, String elementName)
120            {
121                this.clientId = clientId;
122                this.elementName = elementName;
123            }
124    
125            public void execute(Submit component)
126            {
127                component.processSubmission(clientId, elementName);
128            }
129        }
130    
131        public Submit()
132        {
133        }
134    
135        Submit(Request request)
136        {
137            this.request = request;
138        }
139    
140        void beginRender(MarkupWriter writer)
141        {
142            clientId = javascriptSupport.allocateClientId(resources);
143    
144            boolean isCancel = mode == SubmitMode.CANCEL;
145    
146            String name =
147                    isCancel ? InternalConstants.CANCEL_NAME :
148                            formSupport.allocateControlName(resources.getId());
149    
150            // Save the element, to see if an id is later requested.
151    
152            String type = image == null ? "submit" : "image";
153    
154            writer.element("input",
155    
156                    "type", type,
157    
158                    "name", name,
159    
160                    "id", clientId);
161    
162            if (disabled)
163            {
164                writer.attributes("disabled", "disabled");
165            }
166    
167            if (image != null)
168            {
169                writer.attributes("src", image.toClientURL());
170            }
171    
172            formSupport.store(this, new ProcessSubmission(clientId, name));
173    
174            resources.renderInformalParameters(writer);
175    
176            if (isCancel)
177            {
178                javascriptSupport.addInitializerCall("cancelButton", getClientId());
179            }
180    
181        }
182    
183        void afterRender(MarkupWriter writer)
184        {
185            writer.end();
186        }
187    
188        void processSubmission(String clientId, String elementName)
189        {
190            if (disabled || !selected(clientId, elementName))
191                return;
192    
193            Runnable sendNotification = new Runnable()
194            {
195                public void run()
196                {
197                    // TAP5-1024: allow for navigation result from the event callback
198                    resources.triggerEvent(event, context, eventCallback);
199                }
200            };
201    
202            // When not deferred, don't wait, fire the event now (actually, at the end of the current
203            // heartbeat). This is most likely because the Submit is inside a Loop and some contextual
204            // information will change if we defer.
205    
206            if (defer)
207                formSupport.defer(sendNotification);
208            else
209                heartbeat.defer(sendNotification);
210        }
211    
212        private boolean selected(String clientId, String elementName)
213        {
214            // Case #1: via JavaScript, the client id is passed up.
215    
216            String raw = request.getParameter(Form.SUBMITTING_ELEMENT_ID);
217    
218            if (raw != null &&
219                    new JSONArray(raw).getString(0).equals(clientId))
220            {
221                return true;
222            }
223    
224            // Case #2: No JavaScript, look for normal semantic (non-null value for the element's name).
225            // If configured as an image submit, look for a value for the x position. Ah, the ugliness
226            // of HTML.
227    
228            String name = image == null ? elementName : elementName + ".x";
229    
230            String value = request.getParameter(name);
231    
232            return value != null;
233        }
234    
235        /**
236         * Returns the component's client id. This must be called after the component has rendered.
237         *
238         * @return client id for the component
239         */
240        public String getClientId()
241        {
242            return clientId;
243        }
244    }