Skip to content

Commit 2e5eb45

Browse files
authored
feat: add ElementEffect (#22540)
Adds ElementEffect that does exactly same that ComponentEffect did before, but with Element API. Refactored ComponentEffect to use ElementEffect. Part of #22395
1 parent 8bcef18 commit 2e5eb45

File tree

3 files changed

+201
-66
lines changed

3 files changed

+201
-66
lines changed

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

Lines changed: 7 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,12 @@
2525
import java.util.stream.Collectors;
2626

2727
import com.vaadin.flow.dom.Element;
28+
import com.vaadin.flow.dom.ElementEffect;
2829
import com.vaadin.flow.function.SerializableBiConsumer;
2930
import com.vaadin.flow.function.SerializableFunction;
30-
import com.vaadin.flow.server.ErrorEvent;
3131
import com.vaadin.flow.shared.Registration;
3232
import com.vaadin.signals.ListSignal;
3333
import com.vaadin.signals.Signal;
34-
import com.vaadin.signals.SignalEnvironment;
3534
import com.vaadin.signals.ValueSignal;
3635
import com.vaadin.signals.impl.Effect;
3736

@@ -48,28 +47,13 @@
4847
* @since 24.8
4948
*/
5049
public final class ComponentEffect {
51-
private final Runnable effectFunction;
52-
private boolean closed = false;
53-
private Effect effect = null;
50+
51+
private ElementEffect elementEffect;
5452

5553
private <C extends Component> ComponentEffect(C owner,
5654
Runnable effectFunction) {
57-
Objects.requireNonNull(owner, "Owner component cannot be null");
58-
Objects.requireNonNull(effectFunction,
59-
"Effect function cannot be null");
60-
this.effectFunction = effectFunction;
61-
owner.addAttachListener(attach -> {
62-
enableEffect(attach.getSource());
63-
64-
owner.addDetachListener(detach -> {
65-
disableEffect();
66-
detach.unregisterListener();
67-
});
68-
});
69-
70-
if (owner.isAttached()) {
71-
enableEffect(owner);
72-
}
55+
this.elementEffect = new ElementEffect(owner.getElement(),
56+
effectFunction);
7357
}
7458

7559
/**
@@ -259,52 +243,9 @@ private static <T> void runEffect(BindChildrenEffectContext<T> context) {
259243
validate(context);
260244
}
261245

262-
private void enableEffect(Component owner) {
263-
if (closed) {
264-
return;
265-
}
266-
267-
UI ui = owner.getUI().get();
268-
269-
Runnable errorHandlingEffectFunction = () -> {
270-
try {
271-
effectFunction.run();
272-
} catch (Exception e) {
273-
ui.getSession().getErrorHandler()
274-
.error(new ErrorEvent(e, owner.getElement().getNode()));
275-
}
276-
};
277-
278-
assert effect == null;
279-
effect = new Effect(errorHandlingEffectFunction, command -> {
280-
if (UI.getCurrent() == ui) {
281-
// Run immediately if on the same UI
282-
command.run();
283-
} else {
284-
SignalEnvironment.getDefaultEffectDispatcher().execute(() -> {
285-
try {
286-
// Guard against detach while waiting for lock
287-
if (effect != null) {
288-
ui.access(command::run);
289-
}
290-
} catch (UIDetachedException e) {
291-
// Effect was concurrently disabled -> nothing do to
292-
}
293-
});
294-
}
295-
});
296-
}
297-
298-
private void disableEffect() {
299-
if (effect != null) {
300-
effect.dispose();
301-
effect = null;
302-
}
303-
}
304-
305246
private void close() {
306-
disableEffect();
307-
closed = true;
247+
elementEffect.close();
248+
elementEffect = null;
308249
}
309250

310251
/**
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
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+
package com.vaadin.flow.dom;
17+
18+
import java.util.Objects;
19+
20+
import com.vaadin.flow.component.Component;
21+
import com.vaadin.flow.component.ComponentUtil;
22+
import com.vaadin.flow.component.UI;
23+
import com.vaadin.flow.component.UIDetachedException;
24+
import com.vaadin.flow.function.SerializableBiConsumer;
25+
import com.vaadin.flow.server.ErrorEvent;
26+
import com.vaadin.flow.shared.Registration;
27+
import com.vaadin.signals.Signal;
28+
import com.vaadin.signals.SignalEnvironment;
29+
import com.vaadin.signals.impl.Effect;
30+
31+
/**
32+
* The utility class that provides helper methods for using Signal effects in a
33+
* context of a given element's life-cycle.
34+
* <p>
35+
* It ultimately creates a Signal effect, i.e. a call to
36+
* {@link Signal#effect(Runnable)}, that is automatically enabled when an
37+
* element is attached and disabled when the element is detached. Additionally,
38+
* it provides methods to bind signals to element according to a given value
39+
* setting function.
40+
*
41+
* @since 25.0
42+
*/
43+
public final class ElementEffect {
44+
private final Runnable effectFunction;
45+
private boolean closed = false;
46+
private Effect effect = null;
47+
private Registration detachRegistration;
48+
49+
public ElementEffect(Element owner, Runnable effectFunction) {
50+
Objects.requireNonNull(owner, "Owner element cannot be null");
51+
Objects.requireNonNull(effectFunction,
52+
"Effect function cannot be null");
53+
this.effectFunction = effectFunction;
54+
owner.addAttachListener(attach -> {
55+
enableEffect(attach.getSource());
56+
57+
detachRegistration = owner.addDetachListener(detach -> {
58+
disableEffect();
59+
detachRegistration.remove();
60+
detachRegistration = null;
61+
});
62+
});
63+
64+
if (owner.getNode().isAttached()) {
65+
enableEffect(owner);
66+
67+
detachRegistration = owner.addDetachListener(detach -> {
68+
disableEffect();
69+
detachRegistration.remove();
70+
detachRegistration = null;
71+
});
72+
}
73+
}
74+
75+
/**
76+
* Creates a Signal effect that is owned by a given element. The effect is
77+
* enabled when the element is attached and automatically disabled when it
78+
* is detached.
79+
* <p>
80+
* Example of usage:
81+
*
82+
* <pre>
83+
* Registration effect = ElementEffect.effect(myElement, () -> {
84+
* Notification.show("Element is attached and signal value is "
85+
* + someSignal.value());
86+
* });
87+
* effect.remove(); // to remove the effect when no longer needed
88+
* </pre>
89+
*
90+
* @see Signal#effect(Runnable)
91+
* @param owner
92+
* the owner element for which the effect is applied, must not be
93+
* <code>null</code>
94+
* @param effectFunction
95+
* the effect function to be executed when any dependency is
96+
* changed, must not be <code>null</code>
97+
* @return a {@link Registration} that can be used to remove the effect
98+
* function
99+
*/
100+
public static Registration effect(Element owner, Runnable effectFunction) {
101+
ElementEffect effect = new ElementEffect(owner, effectFunction);
102+
return effect::close;
103+
}
104+
105+
/**
106+
* Binds a <code>signal</code>'s value to a given owner element in a way
107+
* defined in <code>setter</code> function and creates a Signal effect
108+
* function executing the setter whenever the signal value changes.
109+
* <p>
110+
* Example of usage:
111+
*
112+
* <pre>
113+
* Element mySpan = new Element("span");
114+
* Registration effect = ElementEffect.bind(mySpan, stringSignal,
115+
* Element::setText);
116+
* effect.remove(); // to remove the effect when no longer needed
117+
*
118+
* ElementEffect.bind(mySpan, stringSignal.map(value -> !value.isEmpty()),
119+
* Element::setVisible);
120+
* </pre>
121+
*
122+
* @see Signal#effect(Runnable)
123+
* @param owner
124+
* the owner element for which the effect is applied, must not be
125+
* <code>null</code>
126+
* @param signal
127+
* the signal whose value is to be bound to the element, must not
128+
* be <code>null</code>
129+
* @param setter
130+
* the setter function that defines how the signal value is
131+
* applied to the element, must not be <code>null</code>
132+
* @return a {@link Registration} that can be used to remove the effect
133+
* function
134+
* @param <T>
135+
* the type of the signal value
136+
*/
137+
public static <T> Registration bind(Element owner, Signal<T> signal,
138+
SerializableBiConsumer<Element, T> setter) {
139+
return effect(owner, () -> {
140+
setter.accept(owner, signal.value());
141+
});
142+
}
143+
144+
private void enableEffect(Element owner) {
145+
if (closed) {
146+
return;
147+
}
148+
149+
Component parentComponent = ComponentUtil.findParentComponent(owner)
150+
.get();
151+
UI ui = parentComponent.getUI().get();
152+
153+
Runnable errorHandlingEffectFunction = () -> {
154+
try {
155+
effectFunction.run();
156+
} catch (Exception e) {
157+
ui.getSession().getErrorHandler()
158+
.error(new ErrorEvent(e, owner.getNode()));
159+
}
160+
};
161+
162+
assert effect == null;
163+
effect = new Effect(errorHandlingEffectFunction, command -> {
164+
if (UI.getCurrent() == ui) {
165+
// Run immediately if on the same UI
166+
command.run();
167+
} else {
168+
SignalEnvironment.getDefaultEffectDispatcher().execute(() -> {
169+
try {
170+
// Guard against detach while waiting for lock
171+
if (effect != null) {
172+
ui.access(command::run);
173+
}
174+
} catch (UIDetachedException e) {
175+
// Effect was concurrently disabled -> nothing do to
176+
}
177+
});
178+
}
179+
});
180+
}
181+
182+
private void disableEffect() {
183+
if (effect != null) {
184+
effect.dispose();
185+
effect = null;
186+
}
187+
}
188+
189+
public void close() {
190+
disableEffect();
191+
closed = true;
192+
}
193+
}

flow-test-generic/src/main/java/com/vaadin/flow/testutil/ClassesSerializableTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ protected Stream<String> getExcludedPatterns() {
205205
"com\\.vaadin\\.flow\\.component\\.internal\\.ComponentMetaData(\\$.*)?",
206206
"com\\.vaadin\\.flow\\.component\\.internal\\.ComponentTracker",
207207
"com\\.vaadin\\.flow\\.component\\.ComponentEffect",
208+
"com\\.vaadin\\.flow\\.dom\\.ElementEffect",
208209
"com\\.vaadin\\.flow\\.dom\\.ElementFactory",
209210
"com\\.vaadin\\.flow\\.dom\\.NodeVisitor",
210211
"com\\.vaadin\\.flow\\.internal\\.nodefeature\\.NodeList(\\$.*)?",

0 commit comments

Comments
 (0)