From eb9ad621d4f4535dff6f8c909402fc4cb259ac4e Mon Sep 17 00:00:00 2001 From: Johan Stuyts Date: Thu, 4 Dec 2025 20:42:53 +0100 Subject: [PATCH] WICKET-7144: added option for `FormComponentPanel` implementations to indicate they want to process their children on Ajax requests. --- ...AjaxFormComponentUpdatingBehaviorTest.java | 87 +++++ ...rocessingTest$InnerFormComponentPanel.html | 7 + ...rocessingTest$OuterFormComponentPanel.html | 8 + .../FormComponentPanelProcessingTest.java | 107 +++++- .../AjaxFormComponentUpdatingBehavior.java | 19 +- .../markup/html/form/FormComponentPanel.java | 60 +++- wicket-examples/pom.xml | 82 ++++- .../ajax/builtin/FormComponentPanelPage.html | 76 +++++ .../ajax/builtin/FormComponentPanelPage.java | 314 ++++++++++++++++++ .../ajax/builtin/GroupsAndChoicesPanel.html | 20 ++ .../ajax/builtin/GroupsAndChoicesPanel.java | 84 +++++ .../ajax/builtin/GroupsAndChoicesValues.java | 80 +++++ .../wicket/examples/ajax/builtin/Index.html | 6 +- .../wicket/examples/forminput/Multiply.java | 21 +- .../form/datetime/AbstractDateTimeField.java | 62 ++-- .../form/datetime/LocalDateTimeField.java | 56 +++- .../markup/html/form/datetime/TimeField.java | 95 +++++- .../form/datetime/ZonedDateTimeField.java | 57 +++- .../markup/html/form/palette/Palette.java | 61 ++-- .../src/main/asciidoc/forms2/forms2_9.adoc | 86 ++++- 20 files changed, 1288 insertions(+), 100 deletions(-) create mode 100644 wicket-core-tests/src/test/java/org/apache/wicket/ajax/form/AjaxFormComponentUpdatingBehaviorTest.java create mode 100644 wicket-core-tests/src/test/java/org/apache/wicket/markup/html/form/FormComponentPanelProcessingTest$InnerFormComponentPanel.html create mode 100644 wicket-core-tests/src/test/java/org/apache/wicket/markup/html/form/FormComponentPanelProcessingTest$OuterFormComponentPanel.html create mode 100644 wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/FormComponentPanelPage.html create mode 100644 wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/FormComponentPanelPage.java create mode 100644 wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/GroupsAndChoicesPanel.html create mode 100644 wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/GroupsAndChoicesPanel.java create mode 100644 wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/GroupsAndChoicesValues.java diff --git a/wicket-core-tests/src/test/java/org/apache/wicket/ajax/form/AjaxFormComponentUpdatingBehaviorTest.java b/wicket-core-tests/src/test/java/org/apache/wicket/ajax/form/AjaxFormComponentUpdatingBehaviorTest.java new file mode 100644 index 00000000000..128ea5c9f9d --- /dev/null +++ b/wicket-core-tests/src/test/java/org/apache/wicket/ajax/form/AjaxFormComponentUpdatingBehaviorTest.java @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.wicket.ajax.form; + +import static java.lang.Boolean.TRUE; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.wicket.ajax.AjaxRequestHandler; +import org.apache.wicket.ajax.AjaxRequestTarget; +import org.apache.wicket.ajax.attributes.AjaxRequestAttributes; +import org.apache.wicket.markup.Markup; +import org.apache.wicket.markup.html.form.Form; +import org.apache.wicket.markup.html.form.FormComponentPanel; +import org.apache.wicket.markup.html.form.upload.MultiFileUploadField; +import org.apache.wicket.model.Model; +import org.apache.wicket.util.tester.WicketTestCase; +import org.junit.jupiter.api.Test; + +class AjaxFormComponentUpdatingBehaviorTest extends WicketTestCase +{ + @Test + void enablesRecursiveSerializationIfComponentMarkedToWantToProcessInputOfChildrenInAjaxUpdate() + { + var behavior = new AjaxFormComponentUpdatingBehavior("some event") + { + @Override + protected void onUpdate(AjaxRequestTarget target) + { + } + }; + var formComponentPanel = new MultiFileUploadField("someId"); + formComponentPanel.setMetaData(FormComponentPanel.WANT_CHILDREN_TO_PROCESS_INPUT_IN_AJAX_UPDATE, TRUE); + formComponentPanel.add(behavior); + var attributes = new AjaxRequestAttributes(); + + behavior.updateAjaxAttributes(attributes); + + assertTrue(attributes.isSerializeRecursively()); + } + + @Test + void invokesProcessChildrenOnEventIfComponentMarkedToWantToProcessInputOfChildrenInAjaxUpdate() + { + var behavior = new AjaxFormComponentUpdatingBehavior("some event") + { + @Override + protected void onUpdate(AjaxRequestTarget target) + { + } + }; + var form = new Form("form"); + var hasProcessInputOfChildreenBeenCalled = new AtomicBoolean(false); + var formComponentPanel = new MultiFileUploadField("upload", Model.of(List.of())) + { + @Override + public void processInputOfChildren() + { + hasProcessInputOfChildreenBeenCalled.set(true); + } + }; + formComponentPanel.setMetaData(FormComponentPanel.WANT_CHILDREN_TO_PROCESS_INPUT_IN_AJAX_UPDATE, TRUE); + formComponentPanel.add(behavior); + form.add(formComponentPanel); + tester.startComponentInPage(form, Markup.of("
")); + + behavior.onEvent(new AjaxRequestHandler(form.getPage())); + + assertTrue(hasProcessInputOfChildreenBeenCalled.get()); + } +} diff --git a/wicket-core-tests/src/test/java/org/apache/wicket/markup/html/form/FormComponentPanelProcessingTest$InnerFormComponentPanel.html b/wicket-core-tests/src/test/java/org/apache/wicket/markup/html/form/FormComponentPanelProcessingTest$InnerFormComponentPanel.html new file mode 100644 index 00000000000..5ac5f66a4c1 --- /dev/null +++ b/wicket-core-tests/src/test/java/org/apache/wicket/markup/html/form/FormComponentPanelProcessingTest$InnerFormComponentPanel.html @@ -0,0 +1,7 @@ + + + + + + + diff --git a/wicket-core-tests/src/test/java/org/apache/wicket/markup/html/form/FormComponentPanelProcessingTest$OuterFormComponentPanel.html b/wicket-core-tests/src/test/java/org/apache/wicket/markup/html/form/FormComponentPanelProcessingTest$OuterFormComponentPanel.html new file mode 100644 index 00000000000..d26ae0255d4 --- /dev/null +++ b/wicket-core-tests/src/test/java/org/apache/wicket/markup/html/form/FormComponentPanelProcessingTest$OuterFormComponentPanel.html @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/wicket-core-tests/src/test/java/org/apache/wicket/markup/html/form/FormComponentPanelProcessingTest.java b/wicket-core-tests/src/test/java/org/apache/wicket/markup/html/form/FormComponentPanelProcessingTest.java index cb047e8fc5b..21deee9b3f4 100644 --- a/wicket-core-tests/src/test/java/org/apache/wicket/markup/html/form/FormComponentPanelProcessingTest.java +++ b/wicket-core-tests/src/test/java/org/apache/wicket/markup/html/form/FormComponentPanelProcessingTest.java @@ -16,13 +16,19 @@ */ package org.apache.wicket.markup.html.form; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static java.lang.Boolean.TRUE; +import static org.apache.wicket.markup.html.form.FormComponentPanel.WANT_CHILDREN_TO_PROCESS_INPUT_IN_AJAX_UPDATE; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import java.io.Serializable; import org.apache.wicket.MarkupContainer; +import org.apache.wicket.ajax.AjaxRequestTarget; +import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior; import org.apache.wicket.markup.IMarkupResourceStreamProvider; +import org.apache.wicket.markup.Markup; import org.apache.wicket.markup.html.WebPage; import org.apache.wicket.model.IModel; import org.apache.wicket.model.Model; @@ -60,10 +66,56 @@ void clearInput() tester.assertRenderedPage(TestPage.class); TestFormComponentPanel fcp = (TestFormComponentPanel) tester.getComponentFromLastRenderedPage("form:panel"); - assertEquals(false, fcp.isChildClearInputCalled()); + assertFalse(fcp.isChildClearInputCalled()); fcp.clearInput(); - assertEquals(true, fcp.isChildClearInputCalled()); + assertTrue(fcp.isChildClearInputCalled()); + } + + @Test + void processInputOfChildAsksChildFormComponentPanelWithProcessingOfChildrenEnabledToProcessInputOnly() + { + var behavior = new AjaxFormComponentUpdatingBehavior("theEvent") + { + @Override + protected void onUpdate(AjaxRequestTarget target) + { + } + }; + var form = new Form("form"); + var outer = new OuterFormComponentPanel("outer", false); + outer.setMetaData(WANT_CHILDREN_TO_PROCESS_INPUT_IN_AJAX_UPDATE, TRUE); + outer.add(behavior); + form.add(outer); + tester.startComponentInPage(form, Markup.of("
")); + + tester.executeAjaxEvent(outer, "theEvent"); + + assertFalse(outer.inner.wasAskedToProcessInputOfChildren, "Inner must not be asked to process input of children"); + assertTrue(outer.inner.wasAskedToProcessInput, "Inner must be asked to process input"); + } + + @Test + void processInputOfChildAsksChildFormComponentPanelWithProcessingOfChildrenEnabledToProcessChildrenAndInput() + { + var behavior = new AjaxFormComponentUpdatingBehavior("theEvent") + { + @Override + protected void onUpdate(AjaxRequestTarget target) + { + } + }; + var form = new Form("form"); + var outer = new OuterFormComponentPanel("outer", true); + outer.setMetaData(WANT_CHILDREN_TO_PROCESS_INPUT_IN_AJAX_UPDATE, TRUE); + outer.add(behavior); + form.add(outer); + tester.startComponentInPage(form, Markup.of("
")); + + tester.executeAjaxEvent(outer, "theEvent"); + + assertTrue(outer.inner.wasAskedToProcessInputOfChildren, "Inner must be asked to process input of children"); + assertTrue(outer.inner.wasAskedToProcessInput, "Inner must be asked to process input"); } private static class TestFormComponentPanel extends FormComponentPanel @@ -79,7 +131,7 @@ private static class TestFormComponentPanel extends FormComponentPanel model) { super(id, model); - add(new TextField("text", new Model<>()) + add(new TextField<>("text", new Model<>()) { private static final long serialVersionUID = 1L; @@ -106,7 +158,8 @@ public void clearInput() }); } - private boolean isChildClearInputCalled() { + private boolean isChildClearInputCalled() + { return childClearInputCalled; } @@ -169,4 +222,48 @@ public IResourceStream getMarkupResourceStream(MarkupContainer container, } } + + private static class OuterFormComponentPanel extends FormComponentPanel + { + public InnerFormComponentPanel inner = new InnerFormComponentPanel("inner"); + + public OuterFormComponentPanel(String id, boolean mustEnableProcessingOfChilderenInAjaxUpdateOfInner) + { + super(id, Model.of()); + + inner.setMetaData(WANT_CHILDREN_TO_PROCESS_INPUT_IN_AJAX_UPDATE, mustEnableProcessingOfChilderenInAjaxUpdateOfInner); + add(inner); + } + + @Override + public void processInputOfChildren() + { + processInputOfChild(inner); + } + } + + private static class InnerFormComponentPanel extends FormComponentPanel + { + public boolean wasAskedToProcessInputOfChildren; + public boolean wasAskedToProcessInput; + + public InnerFormComponentPanel(String id) + { + super(id, Model.of()); + } + + @Override + public void processInputOfChildren() + { + wasAskedToProcessInputOfChildren = true; + } + + @Override + public void validate() + { + wasAskedToProcessInput = true; + + super.validate(); + } + } } diff --git a/wicket-core/src/main/java/org/apache/wicket/ajax/form/AjaxFormComponentUpdatingBehavior.java b/wicket-core/src/main/java/org/apache/wicket/ajax/form/AjaxFormComponentUpdatingBehavior.java index 27308271751..62f84891e34 100644 --- a/wicket-core/src/main/java/org/apache/wicket/ajax/form/AjaxFormComponentUpdatingBehavior.java +++ b/wicket-core/src/main/java/org/apache/wicket/ajax/form/AjaxFormComponentUpdatingBehavior.java @@ -16,6 +16,9 @@ */ package org.apache.wicket.ajax.form; +import static java.lang.Boolean.TRUE; +import static org.apache.wicket.markup.html.form.FormComponentPanel.WANT_CHILDREN_TO_PROCESS_INPUT_IN_AJAX_UPDATE; + import java.util.Locale; import org.apache.wicket.Application; @@ -25,8 +28,8 @@ import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.ajax.attributes.AjaxRequestAttributes; import org.apache.wicket.ajax.attributes.AjaxRequestAttributes.Method; -import org.apache.wicket.core.request.handler.IPartialPageRequestHandler; import org.apache.wicket.markup.html.form.FormComponent; +import org.apache.wicket.markup.html.form.FormComponentPanel; import org.apache.wicket.markup.html.form.validation.IFormValidator; import org.apache.wicket.util.lang.Args; import org.danekja.java.util.function.serializable.SerializableConsumer; @@ -121,6 +124,12 @@ protected void updateAjaxAttributes(AjaxRequestAttributes attributes) super.updateAjaxAttributes(attributes); attributes.setMethod(Method.POST); + + if (getComponent() instanceof FormComponentPanel formComponentPanel + && formComponentPanel.getMetaData(WANT_CHILDREN_TO_PROCESS_INPUT_IN_AJAX_UPDATE) == TRUE) + { + attributes.setSerializeRecursively(true); + } } @Override @@ -135,6 +144,12 @@ protected final void onEvent(final AjaxRequestTarget target) try { + if (getComponent() instanceof FormComponentPanel formComponentPanel + && formComponentPanel.getMetaData(WANT_CHILDREN_TO_PROCESS_INPUT_IN_AJAX_UPDATE) == TRUE) + { + formComponentPanel.processInputOfChildren(); + } + formComponent.inputChanged(); formComponent.validate(); if (formComponent.isValid()) @@ -199,7 +214,7 @@ protected boolean disableFocusOnBlur() /** * Called to handle any error resulting from updating form component. Errors thrown from * {@link #onUpdate(org.apache.wicket.ajax.AjaxRequestTarget)} will not be caught here. - * + *

* The RuntimeException will be null if it was just a validation or conversion error of the * FormComponent * diff --git a/wicket-core/src/main/java/org/apache/wicket/markup/html/form/FormComponentPanel.java b/wicket-core/src/main/java/org/apache/wicket/markup/html/form/FormComponentPanel.java index 50c48463761..3cb0b4d463c 100644 --- a/wicket-core/src/main/java/org/apache/wicket/markup/html/form/FormComponentPanel.java +++ b/wicket-core/src/main/java/org/apache/wicket/markup/html/form/FormComponentPanel.java @@ -16,7 +16,10 @@ */ package org.apache.wicket.markup.html.form; +import static java.lang.Boolean.TRUE; + import org.apache.wicket.IQueueRegion; +import org.apache.wicket.MetaDataKey; import org.apache.wicket.markup.ComponentTag; import org.apache.wicket.markup.html.panel.IMarkupSourcingStrategy; import org.apache.wicket.markup.html.panel.PanelMarkupSourcingStrategy; @@ -111,6 +114,34 @@ */ public abstract class FormComponentPanel extends FormComponent implements IQueueRegion { + /** + * By setting this key to true (and implementing {@link #processInputOfChildren()}) it will be possible + * to add a {@link org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior AjaxFormComponentUpdatingBehavior} + * to this panel, and be able to access the updated model object of this panel in that behavior. + *

+ * The panel must override {@link #processInputOfChildren()} and (should) call + * {@link #processInputOfChild(FormComponent)} for each of its sub form components. + *

+ * AjaxFormComponentUpdatingBehavior works for FormComponentPanels that contain + * {@link CheckBoxMultipleChoice}, {@link CheckGroup}, {@link RadioChoice} and/or {@link RadioGroup} fields. There + * is no need to use + * {@link org.apache.wicket.ajax.form.AjaxFormChoiceComponentUpdatingBehavior AjaxFormChoiceComponentUpdatingBehavior}. + *

+ * The AjaxFormComponentUpdatingBehavior should use "input change" for the events in most + * cases, so changes to all descendent form components will result in an Ajax update. Warning: some + * form components will result in 2 events being emitted. For example, <input type="number">. + *

+ * Warning: some components may send excessive Ajax updates. Make sure the extra updates are not an + * issue for your situation, or take steps to prevent them. + *

+ * Note that the values of all form components of the panel will be submitted on each event, so use with panels with + * possibly large values should probably be avoided. + */ + public static final MetaDataKey WANT_CHILDREN_TO_PROCESS_INPUT_IN_AJAX_UPDATE = new MetaDataKey<>() + { + private static final long serialVersionUID = 1L; + }; + private static final long serialVersionUID = 1L; /** @@ -165,11 +196,38 @@ public void clearInput() super.clearInput(); // Visit all the (visible) form components and clear the input on each. - visitFormComponentsPostOrder(this, (IVisitor, Void>) (formComponent, visit) -> { + visitFormComponentsPostOrder(this, (IVisitor, Void>) (formComponent, visit) -> + { if (formComponent != FormComponentPanel.this && formComponent.isVisibleInHierarchy()) { formComponent.clearInput(); } }); } + + /** + * Called by {@link org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior AjaxFormComponentUpdatingBehavior} + * if {@link #WANT_CHILDREN_TO_PROCESS_INPUT_IN_AJAX_UPDATE} is set to true. Each nested form component + * must be asked to process its input. You should use {@link #processInputOfChild(FormComponent)} as this method + * takes child FormComponentPanels that also want their children to process the input into account. + */ + public void processInputOfChildren() + { + } + + /** + * Tell the given child component to process its input. If the child component is a FormComponentPanel + * that wants its children to process their input, it will be told to do so. + * + * @param child the component that must be told to process its children. + */ + protected final void processInputOfChild(FormComponent child) + { + if (child instanceof FormComponentPanel formComponentPanel + && formComponentPanel.getMetaData(WANT_CHILDREN_TO_PROCESS_INPUT_IN_AJAX_UPDATE) == TRUE) + { + formComponentPanel.processInputOfChildren(); + } + child.processInput(); + } } diff --git a/wicket-examples/pom.xml b/wicket-examples/pom.xml index d498081e84a..6465e6bd906 100644 --- a/wicket-examples/pom.xml +++ b/wicket-examples/pom.xml @@ -34,7 +34,8 @@ 1.4.13 true - + 2.2.20 + @@ -185,7 +186,18 @@ org.httpunit httpunit - + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + ${kotlin.version} + + + org.jetbrains.kotlin + kotlin-test + ${kotlin.version} + test + + @@ -240,7 +252,71 @@ org.eclipse.jetty jetty-maven-plugin - + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + compile + compile + + compile + + + + src/main/java + target/generated-sources/annotations + + + + + test-compile + test-compile + + test-compile + + + + src/test/java + target/generated-test-sources/test-annotations + + + + + + 1.8 + + + + org.apache.maven.plugins + maven-compiler-plugin + + + default-compile + none + + + default-testCompile + none + + + compile + compile + + compile + + + + testCompile + test-compile + + testCompile + + + + + diff --git a/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/FormComponentPanelPage.html b/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/FormComponentPanelPage.html new file mode 100644 index 00000000000..9d8d9a1c1b7 --- /dev/null +++ b/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/FormComponentPanelPage.html @@ -0,0 +1,76 @@ + + + +This example demonstrates how Ajax can be used with FormComponentPanels: + +

    +
  • Ajax behaviors on the descendent components.
  • +
  • Ajax behaviors on the FormComponentPanel itself.
  • +
+ +

+ +
+

LocalDateTimeField, and its subfield TimeField

+

On descendents

+
+ +
+
    +
  • +
  • +
  • +
+ +

On panel

+

Note: this method generates excessive Ajax updates for some events.

+
+ +
+
    +
  • +
  • +
  • +
+ +

Multiply

+

On descendents

+

+ Multiply was not designed with Ajax on its subfields in mind. +

+ +

On panel

+
+ +
+

+ Result: +

+ +

Palette

+

On descendents

+
+

+ Selected people: +

+ +

On panel

+

Note: this method generates excessive Ajax updates for some events.

+
+

+ Selected people: +

+ +

Groups and Choices

+

On descendents

+

+ GroupsAndChoicesPanel was not designed with Ajax on its subfields in mind. +

+ +

On panel

+
+

+ +

+
+ diff --git a/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/FormComponentPanelPage.java b/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/FormComponentPanelPage.java new file mode 100644 index 00000000000..cd803d16ec6 --- /dev/null +++ b/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/FormComponentPanelPage.java @@ -0,0 +1,314 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.wicket.examples.ajax.builtin; + +import static org.apache.wicket.markup.html.form.FormComponentPanel.WANT_CHILDREN_TO_PROCESS_INPUT_IN_AJAX_UPDATE; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.stream.Collectors; + +import org.apache.wicket.ajax.AjaxRequestTarget; +import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior; +import org.apache.wicket.examples.compref.ComponentReferenceApplication; +import org.apache.wicket.examples.compref.Person; +import org.apache.wicket.examples.forminput.Multiply; +import org.apache.wicket.extensions.markup.html.form.datetime.LocalDateTextField; +import org.apache.wicket.extensions.markup.html.form.datetime.LocalDateTimeField; +import org.apache.wicket.extensions.markup.html.form.datetime.TimeField; +import org.apache.wicket.extensions.markup.html.form.palette.Palette; +import org.apache.wicket.extensions.markup.html.form.palette.component.Recorder; +import org.apache.wicket.extensions.markup.html.form.palette.theme.DefaultTheme; +import org.apache.wicket.markup.html.WebMarkupContainer; +import org.apache.wicket.markup.html.basic.Label; +import org.apache.wicket.markup.html.form.ChoiceRenderer; +import org.apache.wicket.markup.html.form.Form; +import org.apache.wicket.markup.html.form.FormComponent; +import org.apache.wicket.model.IModel; +import org.apache.wicket.model.Model; +import org.apache.wicket.model.util.CollectionModel; +import org.apache.wicket.model.util.ListModel; + +/** + * Demonstraties doing Ajax with FormComponentPanels + * + * @author Johan Stuyts + */ +public class FormComponentPanelPage extends BasePage +{ + /** + * Constructor + */ + public FormComponentPanelPage() + { + var now = LocalDateTime.now(); + + Form form = new Form<>("form"); + add(form); + + addLocalDateTimeOnDescendents(form, now); + addLocalDateTimeOnPanel(form, now); + + addMultiplyOnPanel(form); + + addPaletteOnDescendents(form); + addPaletteOnPanel(form); + + addGroupsAndChoicesOnPanel(form); + } + + private void addLocalDateTimeOnDescendents(Form form, LocalDateTime now) + { + var descendentsModel = Model.of(now); + + var descendentsData = new WebMarkupContainer("localDateTimeDescendentsValue"); + descendentsData.setOutputMarkupId(true); + + var ajaxOnDescendents = new LocalDateTimeField("localDateTimeOnDescendents", descendentsModel) + { + @Override + protected void onInitialize() + { + super.onInitialize(); + + getTimeField().getHoursField().add(new AjaxFormComponentUpdatingBehavior("change") + { + @Override + protected void onUpdate(AjaxRequestTarget target) + { + // IModel#setObject is a no-op for the model used for the hours field, so use the converted input instead of the model object. + // And use it update the LocalDateTimeField manually. + descendentsModel.setObject(descendentsModel.getObject().withHour(((FormComponent)getComponent()).getConvertedInput())); + target.add(descendentsData); + } + }); + getTimeField().getMinutesField().add(new AjaxFormComponentUpdatingBehavior("change") + { + @Override + protected void onUpdate(AjaxRequestTarget target) + { + // IModel#setObject is a no-op for the model used for the minutes field, so use the converted input instead of the model object. + // And use it update the LocalDateTimeField manually. + descendentsModel.setObject(descendentsModel.getObject().withMinute(((FormComponent)getComponent()).getConvertedInput())); + target.add(descendentsData); + } + }); + getTimeField().getAmOrPmChoice().add(new AjaxFormComponentUpdatingBehavior("change") + { + @Override + protected void onUpdate(AjaxRequestTarget target) + { + // IModel#setObject is a no-op for the model used for the AM/PM field, so use the converted input instead of the model object. + // And use it update the LocalDateTimeField manually. + switch (((FormComponent)getComponent()).getConvertedInput()) + { + case AM -> descendentsModel.setObject(descendentsModel.getObject().withHour(descendentsModel.getObject().getHour() / 12)); + case PM -> + { + var hour = descendentsModel.getObject().getHour(); + descendentsModel.setObject(descendentsModel.getObject().withHour(hour < 12 ? hour + 12 : hour)); + } + } + target.add(descendentsData); + } + }); + } + + @Override + protected LocalDateTextField newDateField(String id, IModel dateFieldModel) + { + var dateField = super.newDateField(id, dateFieldModel); + dateField.add(new AjaxFormComponentUpdatingBehavior("input change") + { + @Override + protected void onUpdate(AjaxRequestTarget target) + { + // IModel#setObject is a no-op for the model used for the date field, so use the converted input instead of the model object. + var date = ((LocalDateTextField)getComponent()).getConvertedInput(); + // And use it update the LocalDateTimeField manually. + descendentsModel.setObject(descendentsModel.getObject() + .withYear(date.getYear()) + .withMonth(date.getMonthValue()) + .withDayOfMonth(date.getDayOfMonth())); + target.add(descendentsData); + } + }); + return dateField; + } + }; + ajaxOnDescendents.setRequired(true); + + form.add( + ajaxOnDescendents, + descendentsData.add( + new Label("descendentsDate", descendentsModel.map(LocalDateTime::toLocalDate)), + new Label("descendentsHour", descendentsModel.map(LocalDateTime::getHour)), + new Label("descendentsMinute", descendentsModel.map(LocalDateTime::getMinute)) + ) + ); + } + + private void addLocalDateTimeOnPanel(Form form, LocalDateTime now) + { + var panelModel = Model.of(now); + + var panelData = new WebMarkupContainer("localDateTimePanelValue"); + panelData.setOutputMarkupId(true); + + var ajaxOnPanel = new LocalDateTimeField("localDateTimeOnPanel", panelModel) + { + @Override + protected TimeField newTimeField(String id, IModel timeFieldModel) + { + var timeField = super.newTimeField(id, timeFieldModel); + timeField.setMetaData(WANT_CHILDREN_TO_PROCESS_INPUT_IN_AJAX_UPDATE, true); + return timeField; + } + }; + ajaxOnPanel.setRequired(true); + ajaxOnPanel.setMetaData(WANT_CHILDREN_TO_PROCESS_INPUT_IN_AJAX_UPDATE, true); + // Unfortunately 2 events for . See documentation of WANT_CHILDREN_TO_PROCESS_INPUT_IN_AJAX_UPDATE. + ajaxOnPanel.add(new AjaxFormComponentUpdatingBehavior("input change") + { + @Override + protected void onUpdate(AjaxRequestTarget target) + { + target.add(panelData); + } + }); + + form.add( + ajaxOnPanel, + panelData.add( + new Label("panelDate", panelModel.map(LocalDateTime::toLocalDate)), + new Label("panelHour", panelModel.map(LocalDateTime::getHour)), + new Label("panelMinute", panelModel.map(LocalDateTime::getMinute)) + ) + ); + } + + private void addMultiplyOnPanel(Form form) + { + var panelModel = Model.of(0); + + var panelValue = new Label("multiplyPanelValue", panelModel); + panelValue.setOutputMarkupId(true); + + var ajaxOnPanel = new Multiply("multiplyOnPanel", panelModel); + ajaxOnPanel.setMetaData(WANT_CHILDREN_TO_PROCESS_INPUT_IN_AJAX_UPDATE, true); + ajaxOnPanel.add(new AjaxFormComponentUpdatingBehavior("input change") + { + @Override + protected void onUpdate(AjaxRequestTarget target) + { + target.add(panelValue); + } + }); + + form.add(ajaxOnPanel, panelValue); + } + + private void addPaletteOnDescendents(Form form) + { + var descendentsModel = new ListModel<>(new ArrayList()); + + var descendentsValue = new Label("paletteDescendentsValue", descendentsModel.map(people -> people.stream().map(Person::getFullName).collect(Collectors.joining(", ")))); + descendentsValue.setOutputMarkupId(true); + + var persons = ComponentReferenceApplication.getPersons(); + var renderer = new ChoiceRenderer<>("fullName", "fullName"); + var ajaxOnDescendents = new Palette<>("paletteOnDescendents", descendentsModel, new CollectionModel<>(persons), renderer, 10, true, true) + { + @Override + protected Recorder newRecorderComponent() + { + var recorder = super.newRecorderComponent(); + recorder.add(new AjaxFormComponentUpdatingBehavior("change") + { + @Override + protected void onUpdate(AjaxRequestTarget target) + { + processInput(); + target.add(descendentsValue); + } + }); + return recorder; + } + }; + ajaxOnDescendents.add(new DefaultTheme()); + + form.add(ajaxOnDescendents, descendentsValue); + } + + private void addPaletteOnPanel(Form form) + { + var panelModel = new ListModel<>(new ArrayList()); + + var panelValue = new Label("palettePanelValue", panelModel.map(people -> people.stream().map(Person::getFullName).collect(Collectors.joining(", ")))); + panelValue.setOutputMarkupId(true); + + var persons = ComponentReferenceApplication.getPersons(); + var renderer = new ChoiceRenderer<>("fullName", "fullName"); + var ajaxOnPanel = new Palette<>("paletteOnPanel", panelModel, new CollectionModel<>(persons), renderer, 10, true, true); + ajaxOnPanel.add(new DefaultTheme()); + ajaxOnPanel.setMetaData(WANT_CHILDREN_TO_PROCESS_INPUT_IN_AJAX_UPDATE, true); + ajaxOnPanel.add(new AjaxFormComponentUpdatingBehavior("change") + { + @Override + protected void onUpdate(AjaxRequestTarget target) + { + target.add(panelValue); + } + }); + + form.add(ajaxOnPanel, panelValue); + } + + private static void addGroupsAndChoicesOnPanel(Form form) + { + var groupsAndChoicesModel = Model.of(new GroupsAndChoicesValues()); + var groupsAndChoicesValues = new Label("groupsAndChoicesValues", groupsAndChoicesModel.map(groupsAndChoices -> + "CheckGroup: " + + (groupsAndChoices.getCheckGroup().isEmpty() + ? "-" + : groupsAndChoices.getCheckGroup().stream().map(String::valueOf).collect(Collectors.joining(", "))) + + ". RadioGroup: " + + (groupsAndChoices.getRadioGroup() == null ? "-" : groupsAndChoices.getRadioGroup().toString()) + + ". CheckBoxMultipleChoice: " + + (groupsAndChoices.getCheckBoxMultiple().isEmpty() + ? "-" + : groupsAndChoices.getCheckBoxMultiple().stream().map(String::valueOf).collect(Collectors.joining(", "))) + + ". RadioChoice: " + + (groupsAndChoices.getRadioChoice() == null ? "-" : groupsAndChoices.getRadioChoice().toString()))); + groupsAndChoicesValues.setOutputMarkupId(true); + + var groupsAndChoices = new GroupsAndChoicesPanel("groupsAndChoices", groupsAndChoicesModel); + groupsAndChoices.setMetaData(WANT_CHILDREN_TO_PROCESS_INPUT_IN_AJAX_UPDATE, true); + groupsAndChoices.add(new AjaxFormComponentUpdatingBehavior("change") + { + @Override + protected void onUpdate(AjaxRequestTarget target) + { + target.add(groupsAndChoicesValues); + } + }); + + form.add(groupsAndChoices, groupsAndChoicesValues); + } +} diff --git a/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/GroupsAndChoicesPanel.html b/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/GroupsAndChoicesPanel.html new file mode 100644 index 00000000000..ec83ae5afa9 --- /dev/null +++ b/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/GroupsAndChoicesPanel.html @@ -0,0 +1,20 @@ + + + +
+
1
+
2
+
3
+
+
+
4
+
5
+
+
+ +
+
+ +
+
+ diff --git a/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/GroupsAndChoicesPanel.java b/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/GroupsAndChoicesPanel.java new file mode 100644 index 00000000000..b0a7a3b680f --- /dev/null +++ b/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/GroupsAndChoicesPanel.java @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.wicket.examples.ajax.builtin; + +import java.util.List; + +import org.apache.wicket.markup.html.form.*; +import org.apache.wicket.model.IModel; +import org.apache.wicket.model.LambdaModel; +import org.apache.wicket.model.Model; + +public class GroupsAndChoicesPanel extends FormComponentPanel +{ + private CheckGroup checkGroup = + new CheckGroup<>("checkGroup", LambdaModel.of(getModel(), GroupsAndChoicesValues::getCheckGroup, GroupsAndChoicesValues::setCheckGroup)); + private Check check1 = new Check<>("check1", Model.of(1)); + private Check check2 = new Check<>("check2", Model.of(2)); + private Check check3 = new Check<>("check3", Model.of(3)); + + private RadioGroup radioGroup = + new RadioGroup<>("radioGroup", LambdaModel.of(getModel(), GroupsAndChoicesValues::getRadioGroup, GroupsAndChoicesValues::setRadioGroup)); + private Radio radio4 = new Radio<>("radio4", Model.of(4)); + private Radio radio5 = new Radio<>("radio5", Model.of(5)); + + private CheckBoxMultipleChoice checkBoxMultipleChoice = + new CheckBoxMultipleChoice<>("checkBoxMultipleChoice", LambdaModel.of(getModel(), GroupsAndChoicesValues::getCheckBoxMultiple, GroupsAndChoicesValues::setCheckBoxMultiple), List.of(6, 7, 8)); + + private RadioChoice radioChoice = + new RadioChoice<>("radioChoice", LambdaModel.of(getModel(), GroupsAndChoicesValues::getRadioChoice, GroupsAndChoicesValues::setRadioChoice), List.of(9, 0)); + + public GroupsAndChoicesPanel(String id, IModel model) + { + super(id, model); + } + + @Override + protected void onInitialize() + { + super.onInitialize(); + + add( + checkGroup.add( + check1, + check2, + check3 + ), + radioGroup.add( + radio4, + radio5 + ), + checkBoxMultipleChoice, + radioChoice + ); + } + + @Override + public void convertInput() + { + setConvertedInput(getModelObject()); + } + + @Override + public void processInputOfChildren() + { + processInputOfChild(checkGroup); + processInputOfChild(radioGroup); + processInputOfChild(checkBoxMultipleChoice); + processInputOfChild(radioChoice); + } +} diff --git a/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/GroupsAndChoicesValues.java b/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/GroupsAndChoicesValues.java new file mode 100644 index 00000000000..094218de915 --- /dev/null +++ b/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/GroupsAndChoicesValues.java @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.wicket.examples.ajax.builtin; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +public class GroupsAndChoicesValues implements Serializable +{ + private List checkGroup = new ArrayList<>(); + private Integer radioGroup; + private List checkBoxMultiple = new ArrayList<>(); + private Integer radioChoice; + + public List getCheckGroup() + { + return checkGroup; + } + + public void setCheckGroup(List checkGroup) + { + this.checkGroup = checkGroup; + } + + public Integer getRadioGroup() + { + return radioGroup; + } + + public void setRadioGroup(Integer radioGroup) + { + this.radioGroup = radioGroup; + } + + public List getCheckBoxMultiple() + { + return checkBoxMultiple; + } + + public void setCheckBoxMultiple(List checkBoxMultiple) + { + this.checkBoxMultiple = checkBoxMultiple; + } + + public Integer getRadioChoice() + { + return radioChoice; + } + + public void setRadioChoice(Integer radioChoice) + { + this.radioChoice = radioChoice; + } + + @Override + public String toString() + { + return "GroupsAndChoicesValues{" + + "checkGroup=" + checkGroup + + ", radioGroup=" + radioGroup + + ", checkBoxMultiple=" + checkBoxMultiple + + ", radioChoice=" + radioChoice + + '}'; + } +} diff --git a/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/Index.html b/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/Index.html index 92a7049ab4f..183d7dc11b8 100644 --- a/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/Index.html +++ b/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/Index.html @@ -20,7 +20,9 @@

Links Example: demonstrates ajax enabled links

-File Upload Example: demonstrates trasnparent ajax handling of a multipart form +File Upload Example: demonstrates transparent ajax handling of a multipart form +

+Form Component Panel Example: demonstrates the 2 ways to do ajax handling of a FormComponentPanel

Modal dialog: javascript modal dialog example

@@ -39,4 +41,4 @@ Ajax Download: download initiated via Ajax - \ No newline at end of file + diff --git a/wicket-examples/src/main/java/org/apache/wicket/examples/forminput/Multiply.java b/wicket-examples/src/main/java/org/apache/wicket/examples/forminput/Multiply.java index 337cec16ceb..4430eceb475 100644 --- a/wicket-examples/src/main/java/org/apache/wicket/examples/forminput/Multiply.java +++ b/wicket-examples/src/main/java/org/apache/wicket/examples/forminput/Multiply.java @@ -16,6 +16,7 @@ */ package org.apache.wicket.examples.forminput; +import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior; import org.apache.wicket.markup.html.form.FormComponentPanel; import org.apache.wicket.markup.html.form.TextField; import org.apache.wicket.model.IModel; @@ -29,7 +30,18 @@ * with the lhs and rhs. You would use this component's model (value) primarily to write the result * to some object, without ever directly setting it in code yourself. *

- * + *

+ * To Ajaxify this component with an {@link AjaxFormComponentUpdatingBehavior}, create an instance and: + *

+ *
    + *
  • + * Set {@link #WANT_CHILDREN_TO_PROCESS_INPUT_IN_AJAX_UPDATE} to true. + *
  • + *
  • + * Add the AjaxFormComponentUpdatingBehavior with event "input change" to it. + *
  • + *
+ * * @author eelcohillenius */ public class Multiply extends FormComponentPanel @@ -110,6 +122,13 @@ private void init() right.setRequired(true); } + @Override + public void processInputOfChildren() + { + processInputOfChild(left); + processInputOfChild(right); + } + /** * @see org.apache.wicket.markup.html.form.FormComponent#convertInput() */ diff --git a/wicket-extensions/src/main/java/org/apache/wicket/extensions/markup/html/form/datetime/AbstractDateTimeField.java b/wicket-extensions/src/main/java/org/apache/wicket/extensions/markup/html/form/datetime/AbstractDateTimeField.java index cd81b566f24..5658c7609d1 100644 --- a/wicket-extensions/src/main/java/org/apache/wicket/extensions/markup/html/form/datetime/AbstractDateTimeField.java +++ b/wicket-extensions/src/main/java/org/apache/wicket/extensions/markup/html/form/datetime/AbstractDateTimeField.java @@ -22,7 +22,6 @@ import java.time.temporal.Temporal; import java.util.Date; -import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior; import org.apache.wicket.core.util.string.CssUtils; import org.apache.wicket.markup.ComponentTag; import org.apache.wicket.markup.html.basic.Label; @@ -33,28 +32,7 @@ /** * Works on a {@link java.time.temporal.Temporal} object, aggregating a {@link LocalDateTextField} and a {@link TimeField}. - *

- * Ajaxifying an AbstractDateTimeField: - * If you want to update this component with an {@link AjaxFormComponentUpdatingBehavior}, you have to attach it - * to the contained components by overriding {@link #newDateField(String, IModel)}: - * - *

{@code
- *  DateTimeField dateTimeField = new DateTimeField(...) {
- *    protected DateTextField newDateTextField(String id, PropertyModel dateFieldModel)
- *    {
- *      DateTextField dateField = super.newDateTextField(id, dateFieldModel);     
- *      dateField.add(new AjaxFormComponentUpdatingBehavior("change") {
- *        protected void onUpdate(AjaxRequestTarget target) {
- *          processInput(); // let DateTimeField process input too
  *
- *          ...
- *        }
- *      });
- *      return recorder;
- *    }
- *  }
- * }
- * * @author eelcohillenius */ abstract class AbstractDateTimeField extends FormComponentPanel @@ -99,17 +77,17 @@ protected void onConfigure() super.onConfigure(); timeField.configure(); - + setVisible(timeField.isVisible()); } }); } - + @Override protected void onInitialize() { super.onInitialize(); - + add(localDateField = newDateField("date", new DateModel())); add(timeField = newTimeField("time", new TimeModel())); } @@ -140,6 +118,13 @@ public String getInput() return String.format("%s, %s", localDateField.getInput(), timeField.getInput()); } + @Override + public void processInputOfChildren() + { + processInputOfChild(localDateField); + processInputOfChild(timeField); + } + /** * Sets the converted input, which is an instance of {@link Date}, possibly null. It combines * the inputs of the nested date, hours, minutes and am/pm fields and constructs a date from it. @@ -172,16 +157,17 @@ public void convertInput() if (time == null) { time = getDefaultTime(); - if (time == null) { + if (time == null) + { error(newValidationError(new ConversionException("Cannot create temporal without time").setTargetType(getType()))); return; } } - + // Use the input to create proper date-time temporal = createTemporal(date, time); } - + setConvertedInput(temporal); } @@ -206,14 +192,15 @@ protected LocalTime getDefaultTime() */ protected LocalDateTextField newDateField(String id, IModel dateFieldModel) { - return new LocalDateTextField(id, dateFieldModel, FormatStyle.SHORT) { + return new LocalDateTextField(id, dateFieldModel, FormatStyle.SHORT) + { private static final long serialVersionUID = 1L; @Override protected void onComponentTag(ComponentTag tag) { super.onComponentTag(tag); - + tag.append("class", getString(DATE_CSS_CLASS_KEY), " "); } }; @@ -230,14 +217,15 @@ protected void onComponentTag(ComponentTag tag) */ protected TimeField newTimeField(String id, IModel timeFieldModel) { - return new TimeField(id, timeFieldModel) { + return new TimeField(id, timeFieldModel) + { private static final long serialVersionUID = 1L; @Override protected void onComponentTag(ComponentTag tag) { super.onComponentTag(tag); - + tag.append("class", getString(TIME_CSS_CLASS_KEY), " "); } }; @@ -288,10 +276,11 @@ private class DateModel implements IModel public LocalDate getObject() { T temporal = getModelObject(); - if (temporal == null) { + if (temporal == null) + { return null; } - + return getLocalDate(temporal); } @@ -310,10 +299,11 @@ private class TimeModel implements IModel public LocalTime getObject() { T temporal = getModelObject(); - if (temporal == null) { + if (temporal == null) + { return null; } - + return getLocalTime(temporal); } diff --git a/wicket-extensions/src/main/java/org/apache/wicket/extensions/markup/html/form/datetime/LocalDateTimeField.java b/wicket-extensions/src/main/java/org/apache/wicket/extensions/markup/html/form/datetime/LocalDateTimeField.java index c803c317928..85f07530eff 100644 --- a/wicket-extensions/src/main/java/org/apache/wicket/extensions/markup/html/form/datetime/LocalDateTimeField.java +++ b/wicket-extensions/src/main/java/org/apache/wicket/extensions/markup/html/form/datetime/LocalDateTimeField.java @@ -20,11 +20,64 @@ import java.time.LocalDateTime; import java.time.LocalTime; +import org.apache.wicket.Component; +import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior; +import org.apache.wicket.markup.html.form.FormComponent; import org.apache.wicket.model.IModel; /** * Works on a {@link java.time.LocalDateTime} object. See {@link AbstractDateTimeField} for * further details. + *

+ * If you want to Ajaxify this component with an {@link AjaxFormComponentUpdatingBehavior}, it be done in 2 ways: + *

+ *
    + *
  • + * On LocalDateTimeField: easy, less code, larger requests, and (unfortunately) excessive requests. + *

    + * Create a subclass and: + *

      + *
    • + * Set {@link #WANT_CHILDREN_TO_PROCESS_INPUT_IN_AJAX_UPDATE} to true. + *
    • + *
    • + * Add the AjaxFormComponentUpdatingBehavior with event "input change" to it. + *
    • + *
    • + * Override {@link #newTimeField(String, IModel)} and also set + * {@link #WANT_CHILDREN_TO_PROCESS_INPUT_IN_AJAX_UPDATE} to true on the component created + * by the superclass. + *
    • + *
    + *
  • + *
  • + * On the descendent form components: cumbersone, quite a bit of code, but few, smallest possible requests. + *

    + * Create a subclass and: + *

      + *
    • + * Override {@link AbstractDateTimeField#newDateField(String, IModel)} and add a + * AjaxFormComponentUpdatingBehavior with event "input change" to the + * component created by the superclass. + *

      + * {@link IModel#setObject(Object)} of the date field is a no-op. So use + * {@link FormComponent#getConvertedInput()} to get the submitted date, and update the model object of + * this field manually. + *

    • + *
    • + * Override {@link Component#onInitialize()} and + * {@link AbstractDateTimeField#getTimeField() get the timefield}. Use + * {@link TimeField#getHoursField()}, {@link TimeField#getMinutesField()} and + * {@link TimeField#getAmOrPmChoice()} to get the subfields, and add + * AjaxFormComponentUpdatingBehavior with event "change" to them. + *

      + * {@link IModel#setObject(Object)} of these fields is also a no-op. So again use + * getConvertedInput() to get the submitted values, and update the model object of these + * fields manually. + *

    • + *
    + *
  • + *
* * @author eelcohillenius */ @@ -57,7 +110,8 @@ public LocalDateTimeField(final String id, final IModel model) } @Override - protected LocalDateTime createTemporal(LocalDate date, LocalTime time) { + protected LocalDateTime createTemporal(LocalDate date, LocalTime time) + { return LocalDateTime.of(date, time); } diff --git a/wicket-extensions/src/main/java/org/apache/wicket/extensions/markup/html/form/datetime/TimeField.java b/wicket-extensions/src/main/java/org/apache/wicket/extensions/markup/html/form/datetime/TimeField.java index 382c19276aa..99abdf32796 100644 --- a/wicket-extensions/src/main/java/org/apache/wicket/extensions/markup/html/form/datetime/TimeField.java +++ b/wicket-extensions/src/main/java/org/apache/wicket/extensions/markup/html/form/datetime/TimeField.java @@ -26,6 +26,7 @@ import java.util.Arrays; import java.util.Locale; +import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior; import org.apache.wicket.core.util.string.CssUtils; import org.apache.wicket.markup.ComponentTag; import org.apache.wicket.markup.html.basic.Label; @@ -44,7 +45,41 @@ * AM/PM field. The format (12h/24h) of the hours field depends on the time format of this * {@link TimeField}'s {@link Locale}, as does the visibility of the AM/PM field (see * {@link TimeField#use12HourFormat}). - * + *

+ * If you want to Ajaxify this component with an {@link AjaxFormComponentUpdatingBehavior}, it be done in 2 ways: + *

+ *
    + *
  • + * On TimeField: easy, less code, larger requests, and (unfortunately) excessive requests. + *

    + * Create an instance and: + *

      + *
    • + * Set {@link #WANT_CHILDREN_TO_PROCESS_INPUT_IN_AJAX_UPDATE} to true. + *
    • + *
    • + * Add the AjaxFormComponentUpdatingBehavior with event "input change" to it. + *
    • + *
    + *
  • + *
  • + * On the descendent form components: cumbersone, quite a bit of code, but few, smallest possible requests. + *

    + * Create an instance and: + *

      + *
    • + * Use {@link TimeField#getHoursField()}, {@link TimeField#getMinutesField()} and + * {@link TimeField#getAmOrPmChoice()} to get the subfields, and add + * AjaxFormComponentUpdatingBehavior with event "change" to them. + *

      + * {@link IModel#setObject(Object)} of these fields is also a no-op. So use + * getConvertedInput() to get the submitted values, and update the model object of these + * fields manually. + *

    • + *
    + *
  • + *
+ * * @author eelcohillenius */ public class TimeField extends FormComponentPanel @@ -60,7 +95,7 @@ public class TimeField extends FormComponentPanel */ public enum AM_PM { - AM, PM; + AM, PM } private static final IConverter MINUTES_CONVERTER = new IntegerConverter() @@ -125,6 +160,39 @@ protected void onConfigure() }); } + /** + * Get the hours field to customize it with (Ajax) behaviors, adding it to + * {@link org.apache.wicket.ajax.AjaxRequestTarget AjaxRequestTarget}s, etc. + * + * @return the hours field. + */ + public TextField getHoursField() + { + return hoursField; + } + + /** + * Get the minutes field to customize it with (Ajax) behaviors, adding it to + * {@link org.apache.wicket.ajax.AjaxRequestTarget AjaxRequestTarget}s, etc. + * + * @return the minutes field. + */ + public TextField getMinutesField() + { + return minutesField; + } + + /** + * Get the AM/PM field to customize it with (Ajax) behaviors, adding it to + * {@link org.apache.wicket.ajax.AjaxRequestTarget AjaxRequestTarget}s, etc. + * + * @return the AM/PM field. + */ + public DropDownChoice getAmOrPmChoice() + { + return amOrPmChoice; + } + @Override protected void onInitialize() { @@ -137,9 +205,10 @@ protected void onInitialize() add(minutesField = newMinutesTextField("minutes", new MinutesModel(), Integer.class)); // Create and add the "AM/PM" choice - add(amOrPmChoice = new DropDownChoice("amOrPmChoice", new AmPmModel(), - Arrays.asList(AM_PM.values())) { - private static final long serialVersionUID = 1L; + add(amOrPmChoice = new DropDownChoice<>("amOrPmChoice", new AmPmModel(), + Arrays.asList(AM_PM.values())) + { + private static final long serialVersionUID = 1L; @Override protected boolean localizeDisplayValues() @@ -163,14 +232,14 @@ protected boolean localizeDisplayValues() protected TextField newHoursTextField(final String id, IModel model, Class type) { - TextField hoursTextField = new TextField(id, model, type) + TextField hoursTextField = new TextField<>(id, model, type) { private static final long serialVersionUID = 1L; @Override protected String[] getInputTypes() { - return new String[] { "number" }; + return new String[]{"number"}; } @Override @@ -203,7 +272,7 @@ protected void onComponentTag(ComponentTag tag) protected TextField newMinutesTextField(final String id, IModel model, Class type) { - TextField minutesField = new TextField(id, model, type) + TextField minutesField = new TextField<>(id, model, type) { private static final long serialVersionUID = 1L; @@ -220,7 +289,7 @@ protected IConverter createConverter(Class type) @Override protected String[] getInputTypes() { - return new String[] { "number" }; + return new String[]{"number"}; } @Override @@ -246,6 +315,14 @@ public String getInput() return String.format("%s:%s", hoursField.getInput(), minutesField.getInput()); } + @Override + public void processInputOfChildren() + { + processInputOfChild(hoursField); + processInputOfChild(minutesField); + processInputOfChild(amOrPmChoice); + } + @Override public void convertInput() { diff --git a/wicket-extensions/src/main/java/org/apache/wicket/extensions/markup/html/form/datetime/ZonedDateTimeField.java b/wicket-extensions/src/main/java/org/apache/wicket/extensions/markup/html/form/datetime/ZonedDateTimeField.java index 7deba38bd30..50754eb5d0d 100644 --- a/wicket-extensions/src/main/java/org/apache/wicket/extensions/markup/html/form/datetime/ZonedDateTimeField.java +++ b/wicket-extensions/src/main/java/org/apache/wicket/extensions/markup/html/form/datetime/ZonedDateTimeField.java @@ -21,12 +21,64 @@ import java.time.ZoneId; import java.time.ZonedDateTime; +import org.apache.wicket.Component; +import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior; +import org.apache.wicket.markup.html.form.FormComponent; import org.apache.wicket.model.IModel; /** * Works on a {@link java.time.ZonedDateTime} object. See {@link AbstractDateTimeField} for * further details. - * + *

+ * If you want to Ajaxify this component with an {@link AjaxFormComponentUpdatingBehavior}, it be done in 2 ways: + *

+ *
    + *
  • + * On ZonedDateTimeField: easy, less code, larger requests, and (unfortunately) excessive requests. + *

    + * Create a subclass and: + *

      + *
    • + * Set {@link #WANT_CHILDREN_TO_PROCESS_INPUT_IN_AJAX_UPDATE} to true. + *
    • + *
    • + * Add the AjaxFormComponentUpdatingBehavior with event "input change" to it. + *
    • + *
    • + * Override {@link #newTimeField(String, IModel)} and also set + * {@link #WANT_CHILDREN_TO_PROCESS_INPUT_IN_AJAX_UPDATE} to true on the component created + * by the superclass. + *
    • + *
    + *
  • + *
  • + * On the descendent form components: cumbersone, quite a bit of code, but few, smallest possible requests. + *

    + * Create a subclass and: + *

      + *
    • + * Override {@link AbstractDateTimeField#newDateField(String, IModel)} and add a + * AjaxFormComponentUpdatingBehavior to the component created by the superclass. + *

      + * {@link IModel#setObject(Object)} of the date field is a no-op. So use + * {@link FormComponent#getConvertedInput()} to get the submitted date, and update the model object of + * this field manually. + *

    • + *
    • + * Override {@link Component#onInitialize()} and + * {@link AbstractDateTimeField#getTimeField() get the timefield}. Use + * {@link TimeField#getHoursField()}, {@link TimeField#getMinutesField()} and + * {@link TimeField#getAmOrPmChoice()} to get the subfields, and add + * AjaxFormComponentUpdatingBehavior with event "change" to them. + *

      + * {@link IModel#setObject(Object)} of these fields is also a no-op. So again use + * getConvertedInput() to get the submitted values, and update the model object of these + * fields manually. + *

    • + *
    + *
  • + *
+ * * @author eelcohillenius */ public class ZonedDateTimeField extends AbstractDateTimeField @@ -63,7 +115,8 @@ public ZonedDateTimeField(final String id, final IModel model) * @see ZoneId#systemDefault() */ @Override - protected ZonedDateTime createTemporal(LocalDate date, LocalTime time) { + protected ZonedDateTime createTemporal(LocalDate date, LocalTime time) + { return ZonedDateTime.of(date, time, ZoneId.systemDefault()); } diff --git a/wicket-extensions/src/main/java/org/apache/wicket/extensions/markup/html/form/palette/Palette.java b/wicket-extensions/src/main/java/org/apache/wicket/extensions/markup/html/form/palette/Palette.java index cfe47be160a..ab83c0a5d21 100644 --- a/wicket-extensions/src/main/java/org/apache/wicket/extensions/markup/html/form/palette/Palette.java +++ b/wicket-extensions/src/main/java/org/apache/wicket/extensions/markup/html/form/palette/Palette.java @@ -16,12 +16,14 @@ */ package org.apache.wicket.extensions.markup.html.form.palette; +import java.io.Serializable; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Map; import org.apache.wicket.Component; +import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior; import org.apache.wicket.extensions.markup.html.form.palette.component.Choices; import org.apache.wicket.extensions.markup.html.form.palette.component.Recorder; @@ -50,28 +52,33 @@ * When creating a Palette object make sure your IChoiceRenderer returns a specific ID, not the * index. *

- * Ajaxifying the palette: If you want to update a Palette with an - * {@link AjaxFormComponentUpdatingBehavior}, you have to attach it to the contained - * {@link Recorder} by overriding {@link #newRecorderComponent()} and calling - * {@link #processInput()}: - * - *

{@code
- *  Palette palette=new Palette(...) {
- *    protected Recorder newRecorderComponent()
- *    {
- *      Recorder recorder=super.newRecorderComponent();     
- *      recorder.add(new AjaxFormComponentUpdatingBehavior("change") {
- *        protected void onUpdate(AjaxRequestTarget target) {
- *          processInput(); // let Palette process input too
+ * If you want to Ajaxify this component with an {@link AjaxFormComponentUpdatingBehavior}, it be done in 2 ways:
+ * 

+ *
    + *
  • + * On Palette: just as with simple fields, but with larger and (unfortunately) excessive requests. + *

    + * Create an instance and: + *

      + *
    • + * Set {@link #WANT_CHILDREN_TO_PROCESS_INPUT_IN_AJAX_UPDATE} to true. + *
    • + *
    • + * Add the AjaxFormComponentUpdatingBehavior with event "change" to it. + *
    • + *
    + *
  • + *
  • + * On the descendent form components: a bit more code, but minimal number of smallest possible requests. + *

    + * Create a subclass and override {@link #newRecorderComponent()}. Add a + * AjaxFormComponentUpdatingBehavior with event "change" to the component created by + * the superclass. In {@link AjaxFormComponentUpdatingBehavior#onUpdate(AjaxRequestTarget)} call + * {@link FormComponent#processInput()}. + *

  • + *
* - * ... - * } - * }); - * return recorder; - * } - * } - * }
- * + *

* You can add a {@link DefaultTheme} to style this component in a left to right fashion. * * @author Igor Vaynberg ( ivaynberg ) @@ -392,7 +399,7 @@ public void renderHead(IHeaderResponse response) */ protected Component newSelectionComponent() { - return new Selection("selection", this) + return new Selection<>("selection", this) { private static final long serialVersionUID = 1L; @@ -470,7 +477,7 @@ protected Map getAdditionalAttributesForSelection(final Object c */ protected Component newChoicesComponent() { - return new Choices("choices", this) + return new Choices<>("choices", this) { private static final long serialVersionUID = 1L; @@ -565,6 +572,12 @@ public int getRows() return rows; } + @Override + public void processInputOfChildren() + { + processInputOfChild(recorderComponent); + } + @Override public void convertInput() { @@ -581,7 +594,7 @@ public void convertInput() /** * The model object is assumed to be a Collection, and it is modified in-place. Then - * {@link Model#setObject(Object)} is called with the same instance: it allows the Model to be + * {@link Model#setObject(Serializable)} is called with the same instance: it allows the Model to be * notified of changes even when {@link Model#getObject()} returns a different * {@link Collection} at every invocation. * diff --git a/wicket-user-guide/src/main/asciidoc/forms2/forms2_9.adoc b/wicket-user-guide/src/main/asciidoc/forms2/forms2_9.adoc index b6295f38910..0b501c03a38 100644 --- a/wicket-user-guide/src/main/asciidoc/forms2/forms2_9.adoc +++ b/wicket-user-guide/src/main/asciidoc/forms2/forms2_9.adoc @@ -39,23 +39,23 @@ As shown in the markup above FormComponentPanel uses the same tag [source,java] ---- public class TemperatureDegreeField extends FormComponentPanel { - + private TextField userDegree; public TemperatureDegreeField(String id) { super(id); } - + public TemperatureDegreeField(String id, IModel model) { super(id, model); } - + @Override protected void onInitialize() { super.onInitialize(); - + IModel labelModel = () -> getLocale().equals(Locale.US) ? "°F" : "°C"; - + add(new Label("measurementUnit", labelModel)); add(userDegree=new TextField("registeredTemperature", new Model())); @@ -77,39 +77,39 @@ Now we can look at the rest of the code containing the convertInput and onBefore protected void convertInput() { Double userDegreeVal = userDegree.getConvertedInput(); Double kelvinDegree; - + if(getLocale().equals(Locale.US)){ kelvinDegree = userDegreeVal + 459.67; BigDecimal bdKelvin = new BigDecimal(kelvinDegree); BigDecimal fraction = new BigDecimal(5).divide(new BigDecimal(9)); - + kelvinDegree = bdKelvin.multiply(fraction).doubleValue(); }else{ kelvinDegree = userDegreeVal + 273.15; } - + setConvertedInput(kelvinDegree); } - + @Override protected void onBeforeRender() { super.onBeforeRender(); - + Double kelvinDegree = (Double) getDefaultModelObject(); Double userDegreeVal = null; - + if(kelvinDegree == null) return; - + if(getLocale().equals(Locale.US)){ BigDecimal bdKelvin = new BigDecimal(kelvinDegree); BigDecimal fraction = new BigDecimal(9).divide(new BigDecimal(5)); - + kelvinDegree = bdKelvin.multiply(fraction).doubleValue(); userDegreeVal = kelvinDegree - 459.67; }else{ userDegreeVal = kelvinDegree - 273.15; } - + userDegree.setModelObject(userDegreeVal); } } @@ -119,3 +119,61 @@ Since our component does not directly receive the user input, convertInput() mus Method onBeforeRender() is responsible for synchronizing the model of the inner text field with the model of our custom component. To do this we retrieve the model object of the custom component with the getDefaultModelObject() method, then we convert it to the temperature scale adopted by the user and finally we use this value to set the model object of the text field. +=== FormComponentPanel and Ajax + +There are 2 ways to use FormComponentPanel with Ajax: + +* Add Ajax behaviors to 1 or more of the child components. This allows fine-grained updates, but during an Ajax request only the updated value of the child component is available. You will have to update the model of the panel using the child value manually. +* Indicate that the panel wants its children to be processed in an Ajax update, and add an Ajax behavior to the panel. This makes all values of the child components available during an Ajax request. Do not use this method if the panel contains 1 or more inputs that can have large values (e.g. large text areas). + +==== Ajax on child components + +This method is a bit more cumbersome than Ajax on the panel itself, and requires a bit more code. But fewer requests will be made, and the requests will have the smallest possible size. + +[source,java] +---- +var descendentsModel = new ListModel<>(new ArrayList()); +var persons = ...; +var renderer = new ChoiceRenderer<>("fullName", "fullName"); +var ajaxOnDescendents = new Palette<>("paletteOnDescendents", descendentsModel, new CollectionModel<>(persons), renderer, 10, true, true) +{ + @Override + protected Recorder newRecorderComponent() + { + var recorder = super.newRecorderComponent(); + recorder.add(new AjaxFormComponentUpdatingBehavior("change") + { + @Override + protected void onUpdate(AjaxRequestTarget target) + { + processInput(); + // The model will now contain the correct people. + } + }); + return recorder; + } +}; +ajaxOnDescendents.add(new DefaultTheme()); +---- + +==== Ajax on panel with processing of children + +With this method adding Ajax behavior to a panel works just like with simple form fields. Ajax can be used with less code than Ajax on child components. But the requests will be larger because data of all children will be sent with each request. And in some cases (and unfortunately) requests will be sent for more JavaScript events, even if there was no change in the form data. + +[source,java] +---- +var panelModel = new ListModel<>(new ArrayList()); +var persons = ...; +var renderer = new ChoiceRenderer<>("fullName", "fullName"); +var ajaxOnPanel = new Palette<>("paletteOnPanel", panelModel, new CollectionModel<>(persons), renderer, 10, true, true); +ajaxOnPanel.add(new DefaultTheme()); +ajaxOnPanel.setMetaData(WANT_CHILDREN_TO_PROCESS_INPUT_IN_AJAX_UPDATE, true); +ajaxOnPanel.add(new AjaxFormComponentUpdatingBehavior("change") +{ + @Override + protected void onUpdate(AjaxRequestTarget target) + { + // The model will now contain the correct people. + } +}); +----