Skip to content

Commit e5e414a

Browse files
feat: Add ComponentEffect helper (#21674) (#21695)
Fixes #21629 Co-authored-by: Mikhail Shabarov <[email protected]>
1 parent c6c3fd1 commit e5e414a

File tree

3 files changed

+503
-0
lines changed

3 files changed

+503
-0
lines changed
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
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.component;
17+
18+
import java.io.Serializable;
19+
import java.util.Locale;
20+
import java.util.Objects;
21+
import java.util.stream.Stream;
22+
23+
import com.vaadin.flow.function.SerializableBiConsumer;
24+
import com.vaadin.flow.internal.LocaleUtil;
25+
import com.vaadin.flow.shared.Registration;
26+
import com.vaadin.signals.NumberSignal;
27+
import com.vaadin.signals.Signal;
28+
import com.vaadin.signals.ValueSignal;
29+
30+
/**
31+
* The utility class that provides helper methods for using Signal effects in a
32+
* context of a given component's life-cycle.
33+
* <p>
34+
* It ultimately creates a Signal effect, i.e. a call to
35+
* {@link Signal#effect(Runnable)}, that is automatically enabled when a
36+
* component is attached and disabled when the component is detached.
37+
* Additionally it provides methods to bind signals to component according to a
38+
* given value settng function and format strings based on signal values.
39+
*
40+
* @since 24.8
41+
*/
42+
public final class ComponentEffect {
43+
private final Runnable effectFunction;
44+
private boolean closed = false;
45+
private Runnable effectShutdown = null;
46+
47+
private <C extends Component> ComponentEffect(C owner,
48+
Runnable effectFunction) {
49+
Objects.requireNonNull(owner, "Owner component cannot be null");
50+
Objects.requireNonNull(effectFunction,
51+
"Effect function cannot be null");
52+
this.effectFunction = effectFunction;
53+
owner.addAttachListener(attach -> {
54+
enableEffect();
55+
56+
owner.addDetachListener(detach -> {
57+
disableEffect();
58+
detach.unregisterListener();
59+
});
60+
});
61+
62+
if (owner.isAttached()) {
63+
enableEffect();
64+
}
65+
}
66+
67+
/**
68+
* Creates a Signal effect that is owned by a given component. The effect is
69+
* enabled when the component is attached and automatically disabled when it
70+
* is detached.
71+
* <p>
72+
* Examle of usage:
73+
*
74+
* <pre>
75+
* Registration effect = ComponentEffect.effect(myComponent, () -> {
76+
* Notification.show("Component is attached and signal value is "
77+
* + someSignal.value());
78+
* });
79+
* effect.remove(); // to remove the effect when no longer needed
80+
* </pre>
81+
*
82+
* @see Signal#effect(Runnable)
83+
* @param <C>
84+
* the type of the component
85+
* @param owner
86+
* the owner component for which the effect is applied, must not
87+
* be <code>null</code>
88+
* @param effectFunction
89+
* the effect function to be executed when any dependency is
90+
* changed, must not be <code>null</code>
91+
* @return a {@link Registration} that can be used to remove the effect
92+
* function
93+
*/
94+
public static <C extends Component> Registration effect(C owner,
95+
Runnable effectFunction) {
96+
ComponentEffect effect = new ComponentEffect(owner, effectFunction);
97+
return effect::close;
98+
}
99+
100+
/**
101+
* Binds a <code>signal</code>'s value to a given owner component in a way
102+
* defined in <code>setter</code> function and creates a Signal effect
103+
* function executing the setter whenever the signal value changes.
104+
* <p>
105+
* Example of usage:
106+
*
107+
* <pre>
108+
* Registration effect = ComponentEffect.bind(mySpan, stringSignal,
109+
* Span::setText);
110+
* effect.remove(); // to remove the effect when no longer needed
111+
*
112+
* ComponentEffect.bind(mySpan, stringSignal.map(value -> !value.isEmpty()),
113+
* Span::setVisible);
114+
* </pre>
115+
*
116+
* @see Signal#effect(Runnable)
117+
* @param owner
118+
* the owner component for which the effect is applied, must not
119+
* be <code>null</code>
120+
* @param signal
121+
* the signal whose value is to be bound to the component, must
122+
* not be <code>null</code>
123+
* @param setter
124+
* the setter function that defines how the signal value is
125+
* applied to the component, must not be <code>null</code>
126+
* @return a {@link Registration} that can be used to remove the effect
127+
* function
128+
* @param <C>
129+
* the type of the component
130+
* @param <T>
131+
* the type of the signal value
132+
*/
133+
public static <C extends Component, T> Registration bind(C owner,
134+
Signal<T> signal, SerializableBiConsumer<C, T> setter) {
135+
return effect(owner, () -> {
136+
setter.accept(owner, signal.value());
137+
});
138+
}
139+
140+
/**
141+
* Formats a string using the values of the provided signals and the given
142+
* locale, sets the formatted string on the owner component using the
143+
* provided setter function.
144+
* <p>
145+
* Binds a formatted string using the values of the provided signals to a
146+
* given owner component in a way defined in <code>setter</code> function
147+
* and creates a Signal effect function executing the setter whenever the
148+
* signal value changes.
149+
* <p>
150+
* Example of usage:
151+
*
152+
* <pre>
153+
* ComponentEffect.format(mySpan, Span::setText, "The price of %s is %.2f",
154+
* nameSignal, priceSignal);
155+
* </pre>
156+
*
157+
* @see Signal#effect(Runnable)
158+
* @param owner
159+
* the owner component for which the effect is applied, must not
160+
* be <code>null</code>
161+
* @param setter
162+
* the setter function that defines how the formatted string is
163+
* applied to the component, must not be <code>null</code>
164+
* @param locale
165+
* the locale to be used for formatting the string, if
166+
* <code>null</code>, then no localization is applied
167+
* @param format
168+
* the format string to be used for formatting the signal values,
169+
* must not be <code>null</code>
170+
* @param signals
171+
* the signals whose values are to be used for formatting the
172+
* string, must not be <code>null</code>
173+
* @return a {@link Registration} that can be used to remove the effect
174+
* function
175+
* @param <C>
176+
* the type of the component
177+
*/
178+
public static <C extends Component> Registration format(C owner,
179+
SerializableBiConsumer<C, String> setter, Locale locale,
180+
String format, Signal<?>... signals) {
181+
return effect(owner, () -> {
182+
Object[] values = Stream.of(signals).map(Signal::value).toArray();
183+
setter.accept(owner, String.format(locale, format, values));
184+
});
185+
}
186+
187+
/**
188+
* Formats a string using the values of the provided signals and sets it on
189+
* the owner component using the provided setter function.
190+
* <p>
191+
* Binds a formatted string using the values of the provided signals to a
192+
* given owner component in a way defined in <code>setter</code> function
193+
* and creates a Signal effect function executing the setter whenever the
194+
* signal value changes.
195+
* <p>
196+
* Formats using locale from the current UI, I18NProvider or default locale
197+
* depending on what is available.
198+
* <p>
199+
* Example of usage:
200+
*
201+
* <pre>
202+
* ComponentEffect.format(mySpan, Span::setText, "The price of %s is %.2f",
203+
* nameSignal, priceSignal);
204+
* </pre>
205+
*
206+
* @see Signal#effect(Runnable)
207+
* @param owner
208+
* the owner component for which the effect is applied, must not
209+
* be <code>null</code>
210+
* @param setter
211+
* the setter function that defines how the formatted string is
212+
* applied to the component, must not be <code>null</code>
213+
* @param format
214+
* the format string to be used for formatting the signal values,
215+
* must not be <code>null</code>
216+
* @param signals
217+
* the signals whose values are to be used for formatting the
218+
* string, must not be <code>null</code>
219+
* @return a {@link Registration} that can be used to remove the effect
220+
* function
221+
* @param <C>
222+
* the type of the component
223+
*/
224+
public static <C extends Component> Registration format(C owner,
225+
SerializableBiConsumer<C, String> setter, String format,
226+
Signal<?>... signals) {
227+
Locale locale = LocaleUtil.getLocale();
228+
return format(owner, setter, locale, format, signals);
229+
}
230+
231+
private void enableEffect() {
232+
if (closed) {
233+
return;
234+
}
235+
236+
assert effectShutdown == null;
237+
effectShutdown = Signal.effect(effectFunction);
238+
}
239+
240+
private void disableEffect() {
241+
if (effectShutdown != null) {
242+
effectShutdown.run();
243+
effectShutdown = null;
244+
}
245+
}
246+
247+
private void close() {
248+
disableEffect();
249+
closed = true;
250+
}
251+
}

0 commit comments

Comments
 (0)