package fr.openwide.core.wicket.more.markup.html.form.observer.impl; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import java.util.Collection; import org.apache.wicket.Component; import org.apache.wicket.MetaDataKey; import org.apache.wicket.ajax.AjaxEventBehavior; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.ajax.attributes.AjaxAttributeName; import org.apache.wicket.ajax.attributes.AjaxCallListener; import org.apache.wicket.ajax.attributes.AjaxRequestAttributes; import org.apache.wicket.ajax.attributes.AjaxRequestAttributes.Method; import org.apache.wicket.ajax.form.AjaxFormChoiceComponentUpdatingBehavior; import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior; import org.apache.wicket.ajax.json.JSONException; import org.apache.wicket.ajax.json.JSONObject; import org.apache.wicket.markup.html.form.CheckBoxMultipleChoice; import org.apache.wicket.markup.html.form.CheckGroup; import org.apache.wicket.markup.html.form.Form; import org.apache.wicket.markup.html.form.FormComponent; import org.apache.wicket.markup.html.form.RadioChoice; import org.apache.wicket.markup.html.form.RadioGroup; import org.wicketstuff.wiquery.core.events.MouseEvent; import org.wicketstuff.wiquery.core.events.StateEvent; import org.wicketstuff.wiquery.core.javascript.JsStatement; import org.wicketstuff.wiquery.core.javascript.JsUtils; import com.google.common.collect.Sets; import fr.openwide.core.wicket.more.markup.html.form.observer.IFormComponentChangeObservable; import fr.openwide.core.wicket.more.markup.html.form.observer.IFormComponentChangeObserver; /** * A behavior for notifying observers when changes occur on a given {@link FormComponent}. * * <p>This behavior differs from {@link AjaxFormComponentUpdatingBehavior} and * {@link AjaxFormChoiceComponentUpdatingBehavior} in that:<ul> * <li>It's more low-level: it does not presume of the actions to be executed on change. Only * {@link FormComponent#inputChanged()} is called on change, the calls to {@link FormComponent#validate()}, * {@link FormComponent#valid()}, {@link FormComponent#updateModel()}, and so on being the responsibility of the * observers (if they want to). * <li>It supports both choice and non-choice components * <li>It supports choice components whose markup ID was not rendered (it relies on the form's markup ID). This allows * using <code>radioGroup.setRenderBodyOnly(true)</code>, in particular. * <li>It supports binding multiple, independent observers to the same {@link FormComponent}, in which case only * one Ajax call will be made for all the observers. * </ul> */ public class FormComponentChangeAjaxEventBehavior extends AjaxEventBehavior implements IFormComponentChangeObservable { private static final long serialVersionUID = -2099510409333557398L; public static IFormComponentChangeObservable get(FormComponent<?> component) { FormComponentChangeAjaxEventBehavior ajaxEventBehavior = getExisting(component); if (ajaxEventBehavior == null) { ajaxEventBehavior = new FormComponentChangeAjaxEventBehavior((FormComponent<?>)component); component.add(ajaxEventBehavior); } return ajaxEventBehavior; } public static FormComponentChangeAjaxEventBehavior getExisting(Component component) { Collection<FormComponentChangeAjaxEventBehavior> ajaxEventBehaviors = component.getBehaviors(FormComponentChangeAjaxEventBehavior.class); if (ajaxEventBehaviors.isEmpty()) { return null; } else if (ajaxEventBehaviors.size() > 1) { throw new IllegalStateException("There should not be more than ONE FormComponentChangeAjaxEventBehavior attached to " + component); } else { return ajaxEventBehaviors.iterator().next(); } } private static final MetaDataKey<Boolean> IS_SUBMITTED_USING_THIS_BEHAVIOR = new MetaDataKey<Boolean>() { private static final long serialVersionUID = 1L; }; private final Collection<IFormComponentChangeObserver> observers = Sets.newHashSet(); private final FormComponent<?> prerequisiteField; private final boolean choice; private FormComponentChangeAjaxEventBehavior(FormComponent<?> prerequisiteField) { this(prerequisiteField, isChoice(prerequisiteField)); } private FormComponentChangeAjaxEventBehavior(FormComponent<?> prerequisiteField, boolean choice) { super(choice ? MouseEvent.CLICK.getEventLabel() /* Internet Explorer... */ : StateEvent.CHANGE.getEventLabel()); this.prerequisiteField = checkNotNull(prerequisiteField); this.choice = choice; } private static boolean isChoice(Component component) { return (component instanceof RadioChoice) || (component instanceof CheckBoxMultipleChoice) || (component instanceof RadioGroup) || (component instanceof CheckGroup); } @Override protected void onBind() { super.onBind(); Component component = getComponent(); checkState(prerequisiteField.equals(component), "This behavior can only be attached to the component passed to its constructor (%s)", prerequisiteField); if (choice) { component.setRenderBodyOnly(false); } } protected FormComponent<?> getFormComponent() { return (FormComponent<?>)getComponent(); } /* Due to the fact that, for choice components, events are attached to the form and not to the component itself, * we must remove the handlers on ajax refreshes. * Thus we need a unique event name, so that we can call $('#formId').off('click.my.unique.namespace') */ protected String getUniqueEventName() { return getEvent() + ".formComponentChange." + getComponent().getMarkupId(); } @Override protected CharSequence getCallbackScript(Component component) { if (choice) { /* Due to the fact that, for choice components, events are attached to the form and not to the component itself, * we must remove the handlers on ajax refreshes. * See also: getUniqueEventName(), updateAjaxAttributes(), postprocessConfiguration() */ return new StringBuilder() .append(new JsStatement().$(component.findParent(Form.class)).chain("off", JsUtils.quotes(getUniqueEventName(), true)).render(true)) .append(super.getCallbackScript(component)); } else { return super.getCallbackScript(component); } } @Override protected void updateAjaxAttributes(AjaxRequestAttributes attributes) { super.updateAjaxAttributes(attributes); attributes.setMethod(Method.POST); /* Allows all sort of things to work properly: * * allows clicks on labels to work properly * * makes the radio/check buttons properly change their visual aspect on IE. */ attributes.setPreventDefault(false); if (choice) { // For explanations, see: getUniqueEventName(), getCallbackScript(), postprocessConfiguration() attributes.setEventNames(getUniqueEventName()); // Copied from AjaxFormChoiceComponentUpdatingBehavior attributes.setSerializeRecursively(true); attributes.getAjaxCallListeners().add(new AjaxCallListener() { private static final long serialVersionUID = 1L; @Override public CharSequence getPrecondition(Component component) { return String.format("return attrs.event.target.name === '%s'", getFormComponent().getInputName()); } }); } } @Override protected void postprocessConfiguration(JSONObject attributesJson, Component component) throws JSONException { super.postprocessConfiguration(attributesJson, component); if (choice) { /* RadioGroups *may* not have an ID in the resulting HTML, so we must attach the handler to each * input with the correct name in the same form. * See also: getUniqueEventName(), getCallbackScript(), updateAjaxAttributes() */ attributesJson.put(AjaxAttributeName.MARKUP_ID.jsonName(), component.findParent(Form.class).getMarkupId()); attributesJson.put(AjaxAttributeName.CHILD_SELECTOR.jsonName(), "input[name=\"" + ((FormComponent<?>)component).getInputName() + "\"]"); } } @Override public boolean isEnabled(Component component) { return super.isEnabled(component) && !observers.isEmpty(); } @Override protected void onEvent(AjaxRequestTarget target) { getComponent().setMetaData(IS_SUBMITTED_USING_THIS_BEHAVIOR, true); getFormComponent().inputChanged(); notify(target); } @Override public void detach(Component component) { super.detach(component); component.setMetaData(IS_SUBMITTED_USING_THIS_BEHAVIOR, null); } @Override public void subscribe(IFormComponentChangeObserver observer) { observers.add(observer); } @Override public void unsubscribe(IFormComponentChangeObserver observer) { observers.remove(observer); } @Override public boolean isBeingSubmitted() { Boolean submitted = getComponent().getMetaData(IS_SUBMITTED_USING_THIS_BEHAVIOR); return submitted != null && submitted; } @Override public void notify(AjaxRequestTarget target) { for (IFormComponentChangeObserver observer : observers) { observer.onChange(target); } } }