Skip to content

Commit ba7d3cb

Browse files
authored
feat: support explicit UI in ComponentEvent constructor (#24262)
Allows the UI to be provided at construction time so that `getUI()` can return it even when the source component is not yet attached. Related to #18818
1 parent 266c51f commit ba7d3cb

2 files changed

Lines changed: 68 additions & 15 deletions

File tree

‎flow-server/src/main/java/com/vaadin/flow/component/ComponentEvent.java‎

Lines changed: 58 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public class ComponentEvent<T extends Component> extends EventObject {
3434

3535
private boolean fromClient = false;
3636
private Command unregisterListenerCommand = null;
37+
private UI ui;
3738

3839
/**
3940
* Creates a new event using the given source and indicator whether the
@@ -50,36 +51,78 @@ public ComponentEvent(T source, boolean fromClient) {
5051
this.fromClient = fromClient;
5152
}
5253

54+
/**
55+
* Creates a new event using the given source, indicator whether the event
56+
* originated from the client side or the server side, and an explicit UI to
57+
* associate with the event.
58+
* <p>
59+
* Use this constructor when the source component may not be attached to a
60+
* UI at the time the event is fired, but the UI is still known (for
61+
* example, when the event is dispatched from code that has access to the
62+
* current UI). The supplied UI is then returned by {@link #getUI()} without
63+
* relying on the source's attachment state.
64+
*
65+
* @param source
66+
* the source component
67+
* @param fromClient
68+
* <code>true</code> if the event originated from the client
69+
* side, <code>false</code> otherwise
70+
* @param ui
71+
* the UI associated with the event, or <code>null</code> if not
72+
* available
73+
*/
74+
public ComponentEvent(T source, boolean fromClient, UI ui) {
75+
super(source);
76+
this.fromClient = fromClient;
77+
this.ui = ui;
78+
}
79+
5380
@SuppressWarnings("unchecked")
5481
@Override
5582
public T getSource() {
5683
return (T) super.getSource();
5784
}
5885

5986
/**
60-
* Gets the UI the source component is attached to.
87+
* Gets the UI associated with this event.
6188
* <p>
62-
* This is a convenience for {@code getSource().getUI().get()} when the
63-
* event is fired while the source is attached to a UI, which is the common
64-
* case.
89+
* The UI is resolved in the following order:
90+
* <ol>
91+
* <li>If a {@link UI} was explicitly provided at
92+
* {@linkplain #ComponentEvent(Component, boolean, UI) construction time},
93+
* that instance is returned.</li>
94+
* <li>If the source component is itself a {@link UI}, it is returned
95+
* directly.</li>
96+
* <li>Otherwise, the UI is obtained from
97+
* {@code getSource().getUI().get()}.</li>
98+
* </ol>
6599
* <p>
66-
* If the source component is not currently attached to a UI, this method
67-
* throws an {@link IllegalStateException}. This can happen, for example,
68-
* when an initial value is set on a field before it is added to the UI and
69-
* a value-change listener is invoked. If your listener can run while the
70-
* source is detached, use {@code getSource().getUI()} instead, which
71-
* returns an {@link java.util.Optional} and lets you handle the detached
72-
* case explicitly.
100+
* In the common case the source is attached to a UI when the event fires,
101+
* so this method is a convenient shorthand for
102+
* {@code getSource().getUI().get()}.
103+
* <p>
104+
* If none of the above applies and the source component is not currently
105+
* attached to a UI, this method throws an {@link IllegalStateException}.
106+
* This can happen, for example, when an initial value is set on a field
107+
* before it is added to the UI and a value-change listener is invoked. If
108+
* your listener can run while the source is detached, use
109+
* {@code getSource().getUI()} instead, which returns an
110+
* {@link java.util.Optional} and lets you handle the detached case
111+
* explicitly.
73112
*
74-
* @return the UI the source component is attached to, never {@code null}
113+
* @return the UI associated with this event, never {@code null}
75114
* @throws IllegalStateException
76-
* if the source component is not currently attached to a UI
115+
* if the source component is not currently attached to a UI and
116+
* no UI was provided at construction time
77117
*/
78118
public UI getUI() {
79-
T source = getSource();
80-
if (source instanceof UI ui) {
119+
if (ui != null) {
81120
return ui;
82121
}
122+
T source = getSource();
123+
if (source instanceof UI sourceUI) {
124+
return sourceUI;
125+
}
83126
return source.getUI()
84127
.orElseThrow(() -> new IllegalStateException(
85128
"Cannot resolve UI for event source " + source

‎flow-server/src/test/java/com/vaadin/flow/component/ComponentEventTest.java‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,14 @@ void getUI_sourceDetached_throwsIllegalStateException() {
6161
assertEquals(true, exception.getMessage().contains("not")
6262
&& exception.getMessage().contains("attached"));
6363
}
64+
65+
@Test
66+
void getUI_explicitlyProvided_returnsProvidedUI() {
67+
MockUI ui = new MockUI();
68+
TestComponent source = new TestComponent();
69+
ComponentEvent<TestComponent> event = new ComponentEvent<>(source,
70+
false, ui);
71+
72+
assertSame(ui, event.getUI());
73+
}
6474
}

0 commit comments

Comments
 (0)