Skip to content

Commit 569612b

Browse files
authored
feat: add getUI() to ComponentEvent (#24205)
`ComponentEvent` exposes the source component but offers no direct way to reach its UI. Listeners that need the UI must dereference an `Optional` via `event.getSource().getUI().ifPresent(...)`, which is verbose and silently swallows the case where the source is detached. Add `ComponentEvent.getUI()` returning `UI` directly. When the source is itself a `UI`, it is returned as-is; otherwise the method returns the resolved UI or throws `IllegalStateException` with a message pointing to `getSource().getUI()` for code that must handle a detached source. This matches the convention already used by `AbstractAttachDetachEvent`, `UIInitEvent`, `BeforeEvent` and similar event types. Fixes #18818
1 parent c52a381 commit 569612b

2 files changed

Lines changed: 96 additions & 0 deletions

File tree

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,38 @@ public T getSource() {
5656
return (T) super.getSource();
5757
}
5858

59+
/**
60+
* Gets the UI the source component is attached to.
61+
* <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.
65+
* <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.
73+
*
74+
* @return the UI the source component is attached to, never {@code null}
75+
* @throws IllegalStateException
76+
* if the source component is not currently attached to a UI
77+
*/
78+
public UI getUI() {
79+
T source = getSource();
80+
if (source instanceof UI ui) {
81+
return ui;
82+
}
83+
return source.getUI()
84+
.orElseThrow(() -> new IllegalStateException(
85+
"Cannot resolve UI for event source " + source
86+
+ ": the component is not currently attached "
87+
+ "to a UI. Use getSource().getUI() to handle "
88+
+ "the detached case explicitly."));
89+
}
90+
5991
/**
6092
* Checks if this event originated from the client side.
6193
*
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright 2000-2026 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 org.junit.jupiter.api.Test;
19+
20+
import com.vaadin.flow.component.ComponentTest.TestComponent;
21+
import com.vaadin.tests.util.MockUI;
22+
23+
import static org.junit.jupiter.api.Assertions.assertEquals;
24+
import static org.junit.jupiter.api.Assertions.assertNotNull;
25+
import static org.junit.jupiter.api.Assertions.assertSame;
26+
import static org.junit.jupiter.api.Assertions.assertThrows;
27+
28+
class ComponentEventTest {
29+
30+
@Test
31+
void getUI_sourceAttached_returnsUI() {
32+
MockUI ui = new MockUI();
33+
TestComponent source = new TestComponent();
34+
ui.add(source);
35+
36+
ComponentEvent<TestComponent> event = new ComponentEvent<>(source,
37+
false);
38+
39+
assertSame(ui, event.getUI());
40+
}
41+
42+
@Test
43+
void getUI_sourceIsUI_returnsSource() {
44+
MockUI ui = new MockUI();
45+
46+
ComponentEvent<UI> event = new ComponentEvent<>(ui, false);
47+
48+
assertSame(ui, event.getUI());
49+
}
50+
51+
@Test
52+
void getUI_sourceDetached_throwsIllegalStateException() {
53+
TestComponent source = new TestComponent();
54+
55+
ComponentEvent<TestComponent> event = new ComponentEvent<>(source,
56+
false);
57+
58+
IllegalStateException exception = assertThrows(
59+
IllegalStateException.class, event::getUI);
60+
assertNotNull(exception.getMessage());
61+
assertEquals(true, exception.getMessage().contains("not")
62+
&& exception.getMessage().contains("attached"));
63+
}
64+
}

0 commit comments

Comments
 (0)