001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.wicket.markup.html.form;
018
019import org.apache.wicket.Component;
020import org.apache.wicket.IRequestListener;
021import org.apache.wicket.WicketRuntimeException;
022import org.apache.wicket.ajax.AjaxRequestTarget;
023import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior;
024import org.apache.wicket.behavior.Behavior;
025import org.apache.wicket.markup.head.IHeaderResponse;
026import org.apache.wicket.markup.head.OnEventHeaderItem;
027import org.apache.wicket.request.mapper.parameter.PageParameters;
028import org.apache.wicket.util.lang.Args;
029
030/**
031 * A behavior to get notifications when a {@link FormComponent} changes its value.
032 * <p>
033 * Contrary to {@link AjaxFormComponentUpdatingBehavior} all notification are sent via
034 * standard HTTP requests and the full page is rendered as a response.
035 * <p>
036 * Notification is triggered by a {@code change} JavaScript event - if needed {@link #getEvent()} can be overridden
037 * to deviate from this default.
038 * <p>
039 * Note: This behavior has limited support for {@link FormComponent}s outside of a form, i.e. multiple
040 * choice components ({@link ListMultipleChoice} and {@link RadioGroup}) will send their last selected
041 * choice only.
042 * 
043 * @see FormComponentUpdatingBehavior#onUpdate()
044 */
045public class FormComponentUpdatingBehavior extends Behavior implements IRequestListener
046{
047
048        private FormComponent<?> formComponent;
049
050        @Override
051        public boolean getStatelessHint(Component component)
052        {
053                return false;
054        }
055
056        @Override
057        public final void bind(final Component component)
058        {
059                Args.notNull(component, "component");
060
061                if (!(component instanceof FormComponent))
062                {
063                        throw new WicketRuntimeException("Behavior " + getClass().getName()
064                                + " can only be added to an instance of a FormComponent");
065                }
066
067                if (formComponent != null)
068                {
069                        throw new IllegalStateException("this kind of handler cannot be attached to " +
070                                "multiple components; it is already attached to component " + formComponent +
071                                ", but component " + component + " wants to be attached too");
072                }
073
074                this.formComponent = (FormComponent<?>)component;
075
076                formComponent.setRenderBodyOnly(false);
077
078                // call the callback
079                onBind();
080        }
081
082        /**
083         * Called when the component was bound to it's host component. You can get the bound host
084         * component by calling {@link #getFormComponent()}.
085         */
086        protected void onBind()
087        {
088        }
089
090        /**
091         * Get the hosting component.
092         * 
093         * @return hosting component
094         */
095        public final FormComponent<?> getFormComponent()
096        {
097                return formComponent;
098        }
099        
100        @Override
101        public void renderHead(Component component, IHeaderResponse response)
102        {
103                CharSequence url = component.urlForListener(this, new PageParameters());
104
105                String event = getEvent();
106
107                String condition = String.format("if (event.target.name !== '%s') return; ",
108                        formComponent.getInputName());
109
110                Form<?> form = component.findParent(Form.class);
111                if (form != null)
112                {
113                        response.render(OnEventHeaderItem.forComponent(component, event,
114                                condition + form.getJsForListenerUrl(url.toString())));
115                }
116                else
117                {
118                        char separator = url.toString().indexOf('?') > -1 ? '&' : '?';
119
120                        response.render(OnEventHeaderItem.forComponent(component, event,
121                                condition + String.format("window.location.href='%s%s%s=' + %s;", url, separator,
122                                        formComponent.getInputName(), getJSValue())));
123                }
124        }
125
126        /**
127         * Which JavaScript event triggers notification.
128         * 
129         * @return {@code change} by default
130         */
131        protected String getEvent()
132        {
133                return "change";
134        }
135
136        /**
137         * How to get the current value via JavaScript. 
138         */
139        private String getJSValue()
140        {
141                if (formComponent instanceof DropDownChoice)
142                {
143                        return "this.options[this.selectedIndex].value";
144                }
145                else if (formComponent instanceof CheckBox)
146                {
147                        return "this.checked";
148                }
149                else
150                {
151                        return "event.target.value";
152                }
153        }
154
155        /**
156         * Process the form component.
157         */
158        private void process()
159        {
160                try
161                {
162                        formComponent.validate();
163                        if (formComponent.isValid())
164                        {
165                                if (getUpdateModel())
166                                {
167                                        formComponent.valid();
168                                        formComponent.updateModel();
169                                }
170        
171                                onUpdate();
172                        }
173                        else
174                        {
175                                formComponent.invalid();
176                                
177                                onError(null);
178                        }
179                }
180                catch (RuntimeException e)
181                {
182                        onError(e);
183                }
184        }
185
186        /**
187         * Gives the control to the application to decide whether the form component model should
188         * be updated automatically or not. Make sure to call {@link org.apache.wicket.markup.html.form.FormComponent#valid()}
189         * additionally in case the application want to update the model manually.
190         *
191         * @return true if the model of form component should be updated, false otherwise
192         */
193        protected boolean getUpdateModel()
194        {
195                return true;
196        }
197
198        /**
199         * Hook method invoked when the component is updated.
200         * <p>
201         * Note: {@link #onError(RuntimeException)} is called instead when processing
202         * of the {@link FormComponent} failed with conversion or validation errors!  
203         */
204        protected void onUpdate()
205        {
206        }
207
208        /**
209         * Hook method invoked when updating of the component resulted in an error.
210         * <p>
211         * The {@link RuntimeException} will be null if it was just a validation or conversion error of the
212         * FormComponent.
213         * 
214         * @param e optional runtime exception
215         */
216        protected void onError(RuntimeException e)
217        {
218                if (e != null)
219                {
220                        throw e;
221                }
222        }
223        
224        @Override
225        public final void onRequest()
226        {
227                Form<?> form = formComponent.findParent(Form.class);
228                if (form == null)
229                {
230                        // let form component change its input, so it is available
231                        // in case of any errors
232                        formComponent.inputChanged();
233
234                        process();
235                }
236                else
237                {
238                        form.getRootForm().onFormSubmitted(new IFormSubmitter()
239                        {
240                                @Override
241                                public void onSubmit()
242                                {
243                                        process();
244                                }
245
246                                @Override
247                                public void onError()
248                                {
249                                }
250
251                                @Override
252                                public void onAfterSubmit()
253                                {
254                                }
255
256                                @Override
257                                public Form<?> getForm()
258                                {
259                                        return formComponent.getForm();
260                                }
261
262                                @Override
263                                public boolean getDefaultFormProcessing()
264                                {
265                                        // do not process the whole form
266                                        return false;
267                                }
268                        });
269                }
270        }
271}