Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<Void>("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("<form wicket:id=\"form\"><input type=\"file\" wicket:id=\"upload\"></input></form>"));

behavior.onEvent(new AjaxRequestHandler(form.getPage()));

assertTrue(hasProcessInputOfChildreenBeenCalled.get());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:wicket="http://wicket.apache.org">
<body>
<wicket:panel>
</wicket:panel>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:wicket="http://wicket.apache.org">
<body>
<wicket:panel>
<span wicket:id="inner"></span>
</wicket:panel>
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<Void>("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("<form wicket:id=\"form\"><span wicket:id=\"outer\"></span></form>"));

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<Void>("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("<form wicket:id=\"form\"><span wicket:id=\"outer\"></span></form>"));

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<Serializable>
Expand All @@ -79,7 +131,7 @@ private static class TestFormComponentPanel extends FormComponentPanel<Serializa
private TestFormComponentPanel(String id, IModel<Serializable> model)
{
super(id, model);
add(new TextField<Serializable>("text", new Model<>())
add(new TextField<>("text", new Model<>())
{
private static final long serialVersionUID = 1L;

Expand All @@ -106,7 +158,8 @@ public void clearInput()
});
}

private boolean isChildClearInputCalled() {
private boolean isChildClearInputCalled()
{
return childClearInputCalled;
}

Expand Down Expand Up @@ -169,4 +222,48 @@ public IResourceStream getMarkupResourceStream(MarkupContainer container,
}

}

private static class OuterFormComponentPanel extends FormComponentPanel<String>
{
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<String>
{
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();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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())
Expand Down Expand Up @@ -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.
*
* <p>
* The RuntimeException will be null if it was just a validation or conversion error of the
* FormComponent
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -111,6 +114,34 @@
*/
public abstract class FormComponentPanel<T> extends FormComponent<T> implements IQueueRegion
{
/**
* By setting this key to <code>true</code> (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.
* <p>
* The panel must override {@link #processInputOfChildren()} and (should) call
* {@link #processInputOfChild(FormComponent)} for each of its sub form components.
* <p>
* <code>AjaxFormComponentUpdatingBehavior</code> works for <code>FormComponentPanel</code>s 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}.
* <p>
* The <code>AjaxFormComponentUpdatingBehavior</code> should use <code>"input change"</code> for the events in most
* cases, so changes to all descendent form components will result in an Ajax update. <strong>Warning</strong>: some
* form components will result in 2 events being emitted. For example, <code>&lt;input type="number"&gt;</code>.
* <p>
* <strong>Warning</strong>: 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.
* <p>
* 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<Boolean> WANT_CHILDREN_TO_PROCESS_INPUT_IN_AJAX_UPDATE = new MetaDataKey<>()
{
private static final long serialVersionUID = 1L;
};

private static final long serialVersionUID = 1L;

/**
Expand Down Expand Up @@ -165,11 +196,38 @@ public void clearInput()
super.clearInput();

// Visit all the (visible) form components and clear the input on each.
visitFormComponentsPostOrder(this, (IVisitor<FormComponent<?>, Void>) (formComponent, visit) -> {
visitFormComponentsPostOrder(this, (IVisitor<FormComponent<?>, 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 <code>true</code>. Each nested form component
* must be asked to process its input. You should use {@link #processInputOfChild(FormComponent)} as this method
* takes child <code>FormComponentPanel</code>s 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 <code>FormComponentPanel</code>
* 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();
}
}
Loading