Skip to content

Commit 0528f4f

Browse files
authored
fix: add allowInert support for synchronized properties (#21119)
Fixes #21085
1 parent ee26a22 commit 0528f4f

File tree

9 files changed

+224
-9
lines changed

9 files changed

+224
-9
lines changed

flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeDetails.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ public void setContent(Component content) {
193193
*
194194
* @return whether details are expanded or collapsed
195195
*/
196-
@Synchronize(property = "open", value = "toggle")
196+
@Synchronize(property = "open", value = "toggle", allowInert = true)
197197
public boolean isOpen() {
198198
return getElement().getProperty("open", false);
199199
}

flow-server/src/main/java/com/vaadin/flow/component/Component.java

+7-2
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import com.vaadin.flow.component.internal.ComponentMetaData;
3232
import com.vaadin.flow.component.internal.ComponentTracker;
3333
import com.vaadin.flow.component.template.Id;
34+
import com.vaadin.flow.dom.DomListenerRegistration;
3435
import com.vaadin.flow.dom.Element;
3536
import com.vaadin.flow.dom.ElementUtil;
3637
import com.vaadin.flow.dom.PropertyChangeListener;
@@ -178,9 +179,13 @@ private void addSynchronizedProperty(
178179
throw new IllegalArgumentException(getClass().getName()
179180
+ ": event type must not be null for @Synchronize annotation");
180181
}
181-
element.addPropertyChangeListener(info.getProperty(), eventType,
182-
NOOP_PROPERTY_LISTENER)
182+
DomListenerRegistration propertyListener = element
183+
.addPropertyChangeListener(info.getProperty(), eventType,
184+
NOOP_PROPERTY_LISTENER)
183185
.setDisabledUpdateMode(info.getUpdateMode());
186+
if (info.getAllowInert()) {
187+
propertyListener.allowInert();
188+
}
184189
});
185190
}
186191

flow-server/src/main/java/com/vaadin/flow/component/Synchronize.java

+9
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,13 @@
7171
* @return the property update mode for disabled element
7272
*/
7373
DisabledUpdateMode allowUpdates() default DisabledUpdateMode.ONLY_WHEN_ENABLED;
74+
75+
/**
76+
* Makes this property able to synchronize even when the related node is
77+
* inert.
78+
*
79+
* @return {@code true} to allow inert synchronization, {@code false} to
80+
* disallow. Defaults to {@code false}.
81+
*/
82+
boolean allowInert() default false;
7483
}

flow-server/src/main/java/com/vaadin/flow/component/internal/ComponentMetaData.java

+11-3
Original file line numberDiff line numberDiff line change
@@ -90,13 +90,15 @@ public boolean isEmpty() {
9090
public static class SynchronizedPropertyInfo {
9191
private final String property;
9292
private final DisabledUpdateMode mode;
93+
private final boolean allowInert;
9394
private final String[] eventNames;
9495

9596
SynchronizedPropertyInfo(String property, String[] eventNames,
96-
DisabledUpdateMode mode) {
97+
DisabledUpdateMode mode, boolean allowInert) {
9798
this.property = property;
9899
this.eventNames = eventNames;
99100
this.mode = mode;
101+
this.allowInert = allowInert;
100102
}
101103

102104
public String getProperty() {
@@ -110,6 +112,10 @@ public Stream<String> getEventNames() {
110112
public DisabledUpdateMode getUpdateMode() {
111113
return mode;
112114
}
115+
116+
public boolean getAllowInert() {
117+
return allowInert;
118+
}
113119
}
114120

115121
private final Collection<SynchronizedPropertyInfo> synchronizedProperties;
@@ -262,8 +268,10 @@ private static void doCollectSynchronizedProperties(Class<?> clazz,
262268
}
263269

264270
String[] eventNames = annotation.value();
265-
infos.put(method.getName(), new SynchronizedPropertyInfo(
266-
propertyName, eventNames, annotation.allowUpdates()));
271+
infos.put(method.getName(),
272+
new SynchronizedPropertyInfo(propertyName, eventNames,
273+
annotation.allowUpdates(),
274+
annotation.allowInert()));
267275
}
268276
}
269277
}

flow-server/src/main/java/com/vaadin/flow/internal/nodefeature/ElementListenerMap.java

+21
Original file line numberDiff line numberDiff line change
@@ -512,4 +512,25 @@ public DisabledUpdateMode getPropertySynchronizationMode(
512512
.reduce(DisabledUpdateMode::mostPermissive).orElse(null);
513513
}
514514

515+
/**
516+
* Returns {@code true} if any listener for the given property has
517+
* allowInert enabled. Note that this means that enabling allowInert for any
518+
* listener for a certain property will effectively allow it for all
519+
* listeners for said property.
520+
*
521+
* @param propertyName
522+
* the property name to check, not <code>null</code>
523+
* @return {@code true} if allowInert is enabled for any listener for the
524+
* given property, {@code false otherwise}
525+
*/
526+
public boolean hasAllowInertForProperty(String propertyName) {
527+
assert propertyName != null;
528+
529+
if (listeners == null) {
530+
return false;
531+
}
532+
return listeners.values().stream().flatMap(List::stream)
533+
.filter(wrapper -> wrapper.isPropertySynchronized(propertyName))
534+
.anyMatch(wrapper -> wrapper.allowInert);
535+
}
515536
}

flow-server/src/main/java/com/vaadin/flow/server/communication/rpc/AbstractRpcInvocationHandler.java

+9-3
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,15 @@ public Optional<Runnable> handle(UI ui, JsonObject invocationJson) {
6666
if (node.isInactive()) {
6767
logHandlingIgnoredMessage(node, "inactive (disabled or invisible)");
6868
return Optional.empty();
69-
} else if (!allowInert(ui, invocationJson) && node.isInert()) {
70-
logHandlingIgnoredMessage(node, "inert");
71-
return Optional.empty();
69+
} else if (node.isInert()) {
70+
if (allowInert(ui, invocationJson)) {
71+
// Allow handling of RPC request if any listener for the event
72+
// type or the synchronized property have enabled allowInert.
73+
return handleNode(node, invocationJson);
74+
} else {
75+
logHandlingIgnoredMessage(node, "inert");
76+
return Optional.empty();
77+
}
7278
} else {
7379
return handleNode(node, invocationJson);
7480
}

flow-server/src/main/java/com/vaadin/flow/server/communication/rpc/MapSyncRpcHandler.java

+16
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.slf4j.LoggerFactory;
2525

2626
import com.vaadin.flow.component.Component;
27+
import com.vaadin.flow.component.UI;
2728
import com.vaadin.flow.dom.DisabledUpdateMode;
2829
import com.vaadin.flow.dom.Element;
2930
import com.vaadin.flow.internal.JsonCodec;
@@ -121,6 +122,21 @@ protected Optional<Runnable> handleNode(StateNode node,
121122
return Optional.empty();
122123
}
123124

125+
@Override
126+
protected boolean allowInert(UI ui, JsonObject invocationJson) {
127+
StateNode node = ui.getInternals().getStateTree()
128+
.getNodeById(getNodeId(invocationJson));
129+
if (node != null && node.hasFeature(ElementListenerMap.class)) {
130+
ElementListenerMap listenerMap = node
131+
.getFeature(ElementListenerMap.class);
132+
return invocationJson.hasKey(JsonConstants.RPC_PROPERTY)
133+
&& listenerMap.hasAllowInertForProperty(invocationJson
134+
.getString(JsonConstants.RPC_PROPERTY));
135+
} else {
136+
return super.allowInert(ui, invocationJson);
137+
}
138+
}
139+
124140
private Optional<Runnable> enqueuePropertyUpdate(StateNode node,
125141
JsonObject invocationJson, String property) {
126142
Serializable value = JsonCodec.decodeWithoutTypeInfo(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* Copyright 2000-2025 Vaadin Ltd.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
17+
package com.vaadin.flow.uitest.ui;
18+
19+
import com.vaadin.flow.component.ClickEvent;
20+
import com.vaadin.flow.component.Component;
21+
import com.vaadin.flow.component.ComponentEventListener;
22+
import com.vaadin.flow.component.Text;
23+
import com.vaadin.flow.component.UI;
24+
import com.vaadin.flow.component.html.Div;
25+
import com.vaadin.flow.component.html.NativeButton;
26+
import com.vaadin.flow.component.html.NativeDetails;
27+
import com.vaadin.flow.component.html.Span;
28+
import com.vaadin.flow.router.Route;
29+
30+
@Route(value = "com.vaadin.flow.uitest.ui.AllowInertSynchronizedPropertyView")
31+
public class AllowInertSynchronizedPropertyView extends AbstractDivView {
32+
33+
public static final String OPEN_MODAL_BUTTON = "modal-dialog-button";
34+
public static final String READ_NATIVE_DETAILS_STATE_BUTTON = "read-native-details-state-button";
35+
public static final String NATIVE_DETAILS_STATE = "native-details-state";
36+
public static final String NATIVE_DETAILS_SUMMARY = "native-details-summary";
37+
38+
private NativeDetails nativeDetails;
39+
private Span state;
40+
41+
@Override
42+
protected void onShow() {
43+
add(createOpenDialogButton(OPEN_MODAL_BUTTON));
44+
45+
nativeDetails = new NativeDetails();
46+
add(nativeDetails);
47+
48+
Span summary = new Span("Native details summary");
49+
summary.setId(NATIVE_DETAILS_SUMMARY);
50+
nativeDetails.setSummary(summary);
51+
52+
state = new Span("unknown");
53+
state.setId(NATIVE_DETAILS_STATE);
54+
add(state);
55+
}
56+
57+
private Component createOpenDialogButton(String id) {
58+
final NativeButton button = createButton("Open modal dialog",
59+
event -> new Dialog().open());
60+
button.setId(id);
61+
return button;
62+
}
63+
64+
private NativeButton createButton(String caption,
65+
ComponentEventListener<ClickEvent<NativeButton>> listener) {
66+
final NativeButton button = new NativeButton();
67+
button.setText(caption);
68+
button.addClickListener(listener);
69+
button.getStyle().set("border", "1px solid black");
70+
button.setWidth("100px");
71+
return button;
72+
}
73+
74+
public class Dialog extends Div {
75+
76+
public Dialog() {
77+
final NativeButton readNativeDetailsStateButton = new NativeButton(
78+
"Read Native Details State", event -> {
79+
if (nativeDetails.isOpen()) {
80+
state.setText("opened");
81+
} else {
82+
state.setText("closed");
83+
}
84+
});
85+
readNativeDetailsStateButton
86+
.setId(READ_NATIVE_DETAILS_STATE_BUTTON);
87+
88+
add(new Text("A modal dialog"), readNativeDetailsStateButton);
89+
90+
getStyle().set("position", "fixed").set("inset", "50% 50%")
91+
.set("border", "1px solid black");
92+
}
93+
94+
public void open() {
95+
final UI ui = UI.getCurrent();
96+
ui.addModal(this);
97+
}
98+
}
99+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package com.vaadin.flow.uitest.ui;
2+
3+
import org.junit.Assert;
4+
import org.junit.Test;
5+
import org.openqa.selenium.By;
6+
7+
import com.vaadin.flow.component.html.testbench.NativeButtonElement;
8+
import com.vaadin.flow.component.html.testbench.NativeDetailsElement;
9+
import com.vaadin.flow.component.html.testbench.SpanElement;
10+
import com.vaadin.flow.testutil.ChromeBrowserTest;
11+
import com.vaadin.testbench.TestBenchElement;
12+
13+
public class AllowInertSynchronizedPropertyIT extends ChromeBrowserTest {
14+
15+
private TestBenchElement modalDialogButton;
16+
17+
@Override
18+
protected void open(String... parameters) {
19+
super.open(parameters);
20+
modalDialogButton = $(NativeButtonElement.class)
21+
.id(ModalDialogView.OPEN_MODAL_BUTTON);
22+
}
23+
24+
@Test
25+
public void modalDialogOpened_toggleNativeDetailsVisibility_allowInertSynchronizedPropertyShouldChange() {
26+
open();
27+
28+
modalDialogButton.click();
29+
30+
$(NativeButtonElement.class).id(
31+
AllowInertSynchronizedPropertyView.READ_NATIVE_DETAILS_STATE_BUTTON)
32+
.click();
33+
34+
Assert.assertEquals("closed", getStateText());
35+
36+
$(NativeDetailsElement.class).first().findElement(By
37+
.id(AllowInertSynchronizedPropertyView.NATIVE_DETAILS_SUMMARY))
38+
.click();
39+
$(NativeButtonElement.class).id(
40+
AllowInertSynchronizedPropertyView.READ_NATIVE_DETAILS_STATE_BUTTON)
41+
.click();
42+
43+
Assert.assertEquals("opened", getStateText());
44+
}
45+
46+
private String getStateText() {
47+
return $(SpanElement.class)
48+
.id(AllowInertSynchronizedPropertyView.NATIVE_DETAILS_STATE)
49+
.getText();
50+
}
51+
}

0 commit comments

Comments
 (0)