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}