Skip to content

Commit 6cdc736

Browse files
mstahvknoobietepi
authored
feat: Throttle page resize events (#19122)
* A start to fix #19114 * Allow inter, programmatic API for dom event registration * Works also now when UI is inert (modal component visible) * formatter:format * refactoring * cleanup & fixed comments * IT test now takes debouncing into account and also tests with modal component * formatter:format * modified IT to cover height * Update flow-server/src/main/java/com/vaadin/flow/component/page/Page.java Co-authored-by: Knoobie <Knoobie@gmx.de> * fixed IT test * formatterformat * Removed obsoleted unit tests Not built with complex JS hacks anymore, but with normal event listening system. * formatterformat * cleanup --------- Co-authored-by: Knoobie <Knoobie@gmx.de> Co-authored-by: Teppo Kurki <teppo.kurki@vaadin.com>
1 parent f0811e0 commit 6cdc736

File tree

6 files changed

+76
-224
lines changed

6 files changed

+76
-224
lines changed

flow-server/src/main/java/com/vaadin/flow/component/page/Page.java

Lines changed: 29 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -15,39 +15,30 @@
1515
*/
1616
package com.vaadin.flow.component.page;
1717

18-
import java.io.BufferedReader;
19-
import java.io.IOException;
20-
import java.io.InputStream;
21-
import java.io.InputStreamReader;
2218
import java.io.Serializable;
2319
import java.net.MalformedURLException;
2420
import java.net.URI;
2521
import java.net.URL;
26-
import java.nio.charset.StandardCharsets;
22+
import java.util.ArrayList;
2723
import java.util.Arrays;
2824
import java.util.Objects;
2925
import java.util.function.Function;
3026

31-
import com.vaadin.flow.component.ClientCallable;
32-
import com.vaadin.flow.component.Component;
33-
import com.vaadin.flow.component.ComponentEvent;
3427
import com.vaadin.flow.component.Direction;
35-
import com.vaadin.flow.component.Tag;
3628
import com.vaadin.flow.component.UI;
3729
import com.vaadin.flow.component.dependency.JavaScript;
3830
import com.vaadin.flow.component.dependency.JsModule;
3931
import com.vaadin.flow.component.dependency.StyleSheet;
40-
import com.vaadin.flow.component.internal.AllowInert;
4132
import com.vaadin.flow.component.internal.PendingJavaScriptInvocation;
4233
import com.vaadin.flow.component.internal.UIInternals.JavaScriptInvocation;
34+
import com.vaadin.flow.dom.DomListenerRegistration;
4335
import com.vaadin.flow.dom.Element;
4436
import com.vaadin.flow.function.SerializableConsumer;
4537
import com.vaadin.flow.internal.UrlUtil;
4638
import com.vaadin.flow.shared.Registration;
4739
import com.vaadin.flow.shared.ui.Dependency;
4840
import com.vaadin.flow.shared.ui.Dependency.Type;
4941
import com.vaadin.flow.shared.ui.LoadMode;
50-
5142
import elemental.json.JsonObject;
5243
import elemental.json.JsonType;
5344
import elemental.json.JsonValue;
@@ -61,59 +52,10 @@
6152
*/
6253
public class Page implements Serializable {
6354

64-
@Tag(Tag.DIV)
65-
private static class ResizeEventReceiver extends Component {
66-
67-
private int windowResizeListenersSize;
68-
69-
@ClientCallable
70-
@AllowInert
71-
private void windowResized(int width, int height) {
72-
if (windowResizeListenersSize != 0) {
73-
fireEvent(new ResizeEvent(this, width, height));
74-
}
75-
}
76-
77-
private Registration addListener(BrowserWindowResizeListener listener) {
78-
windowResizeListenersSize++;
79-
Registration registration = addListener(ResizeEvent.class,
80-
event -> listener
81-
.browserWindowResized(event.getApiEvent()));
82-
83-
Registration combined = Registration
84-
.combine(this::listenerIsUnregistered, registration);
85-
return Registration.once(combined::remove);
86-
}
87-
88-
private void listenerIsUnregistered() {
89-
windowResizeListenersSize--;
90-
if (windowResizeListenersSize == 0) {
91-
// remove JS listener
92-
getUI().get().getPage().executeJs("$0.resizeRemove()", this);
93-
}
94-
}
95-
}
96-
97-
private static class ResizeEvent
98-
extends ComponentEvent<ResizeEventReceiver> {
99-
100-
private final BrowserWindowResizeEvent apiEvent;
101-
102-
private ResizeEvent(ResizeEventReceiver source, int width, int height) {
103-
super(source, true);
104-
apiEvent = new BrowserWindowResizeEvent(
105-
source.getUI().get().getPage(), width, height);
106-
107-
}
108-
109-
private BrowserWindowResizeEvent getApiEvent() {
110-
return apiEvent;
111-
}
112-
}
113-
114-
private ResizeEventReceiver resizeReceiver;
11555
private final UI ui;
11656
private final History history;
57+
private DomListenerRegistration resizeReceiver;
58+
private ArrayList<BrowserWindowResizeListener> resizeListeners;
11759

11860
/**
11961
* Creates a page instance for the given UI.
@@ -373,17 +315,33 @@ public Registration addBrowserWindowResizeListener(
373315
BrowserWindowResizeListener resizeListener) {
374316
Objects.requireNonNull(resizeListener);
375317
if (resizeReceiver == null) {
376-
// lazy creation which is done only one time since there is no way
377-
// to remove virtual children
378-
resizeReceiver = new ResizeEventReceiver();
379-
ui.getElement().appendVirtualChild(resizeReceiver.getElement());
318+
// "republish" on the UI element, so can be listened with core APIs
319+
ui.getElement().executeJs("""
320+
const el = this;
321+
window.addEventListener('resize', evt => {
322+
const event = new Event("window-resize");
323+
event.w = document.documentElement.clientWidth;
324+
event.h = document.documentElement.clientHeight;
325+
el.dispatchEvent(event);
326+
});
327+
""");
328+
resizeReceiver = ui.getElement()
329+
.addEventListener("window-resize", e -> {
330+
var evt = new BrowserWindowResizeEvent(this,
331+
(int) e.getEventData().getNumber("event.w"),
332+
(int) e.getEventData().getNumber("event.h"));
333+
// Clone list to avoid issues if listener unregisters
334+
// itself
335+
new ArrayList<>(resizeListeners)
336+
.forEach(l -> l.browserWindowResized(evt));
337+
}).addEventData("event.w").addEventData("event.h")
338+
.debounce(300).allowInert();
380339
}
381-
if (resizeReceiver.windowResizeListenersSize == 0) {
382-
// JS resize listener may be completely disabled if there are not
383-
// listeners
384-
executeJs(LazyJsLoader.WINDOW_LISTENER_JS, resizeReceiver);
340+
if (resizeListeners == null) {
341+
resizeListeners = new ArrayList<>(1);
385342
}
386-
return resizeReceiver.addListener(resizeListener);
343+
resizeListeners.add(resizeListener);
344+
return () -> resizeListeners.remove(resizeListener);
387345
}
388346

389347
/**
@@ -467,30 +425,6 @@ private void addDependency(Dependency dependency) {
467425
ui.getInternals().getDependencyList().add(dependency);
468426
}
469427

470-
private static class LazyJsLoader implements Serializable {
471-
472-
private static final String JS_FILE_NAME = "windowResizeListener.js";
473-
474-
private static final String WINDOW_LISTENER_JS = readJS();
475-
476-
private static String readJS() {
477-
try (InputStream stream = Page.class
478-
.getResourceAsStream(JS_FILE_NAME);
479-
BufferedReader bf = new BufferedReader(
480-
new InputStreamReader(stream,
481-
StandardCharsets.UTF_8))) {
482-
StringBuilder builder = new StringBuilder();
483-
bf.lines().forEach(builder::append);
484-
return builder.toString();
485-
} catch (IOException e) {
486-
throw new RuntimeException(
487-
"Couldn't read window resize listener JavaScript file "
488-
+ JS_FILE_NAME + ". The package is broken",
489-
e);
490-
}
491-
}
492-
}
493-
494428
/**
495429
* Callback for receiving extended client-side details.
496430
*/

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -436,13 +436,13 @@ public void fireEvent(DomEvent event) {
436436
if (listeners == null) {
437437
return;
438438
}
439-
440439
final boolean isElementEnabled = event.getSource().isEnabled();
441440

442441
final boolean isNavigationRequest = UI.BrowserNavigateEvent.EVENT_NAME
443442
.equals(event.getType())
444443
|| UI.BrowserLeaveNavigationEvent.EVENT_NAME
445444
.equals(event.getType());
445+
446446
final boolean inert = event.getSource().getNode().isInert();
447447

448448
List<DomEventListenerWrapper> typeListeners = listeners

flow-server/src/main/resources/com/vaadin/flow/component/page/windowResizeListener.js

Lines changed: 0 additions & 39 deletions
This file was deleted.

flow-server/src/test/java/com/vaadin/flow/component/page/PageTest.java

Lines changed: 0 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,12 @@
3232
import org.junit.Test;
3333
import org.mockito.Mockito;
3434

35-
import com.vaadin.flow.component.Component;
3635
import com.vaadin.flow.component.UI;
3736
import com.vaadin.flow.function.SerializableConsumer;
3837
import com.vaadin.flow.internal.JsonUtils;
39-
import com.vaadin.flow.shared.Registration;
4038
import com.vaadin.flow.shared.ui.Dependency;
4139
import com.vaadin.flow.shared.ui.LoadMode;
4240
import com.vaadin.tests.util.MockUI;
43-
4441
import elemental.json.Json;
4542
import elemental.json.JsonValue;
4643

@@ -88,85 +85,6 @@ public void addNullAsAListener_trows() {
8885
page.addBrowserWindowResizeListener(null);
8986
}
9087

91-
@Test
92-
public void addListener_executeInitJs() {
93-
page.addBrowserWindowResizeListener(listener);
94-
95-
MatcherAssert.assertThat(page.expression,
96-
CoreMatchers.allOf(CoreMatchers.containsString("init"),
97-
CoreMatchers.containsString("resize")));
98-
99-
Assert.assertTrue(page.firstParam instanceof Component);
100-
}
101-
102-
@Test
103-
public void addTwoListeners_jsIsExecutedOnce() {
104-
page.addBrowserWindowResizeListener(listener);
105-
page.addBrowserWindowResizeListener(event -> {
106-
});
107-
108-
Assert.assertEquals(1, page.count);
109-
}
110-
111-
@Test
112-
public void addTwoListeners_unregisterOneListener_jsListenerIsNotRemoved() {
113-
page.addBrowserWindowResizeListener(listener);
114-
Registration registration = page
115-
.addBrowserWindowResizeListener(event -> {
116-
});
117-
118-
registration.remove();
119-
120-
Assert.assertEquals(1, page.count);
121-
122-
// remove the same listener one more time
123-
registration.remove();
124-
125-
Assert.assertEquals(1, page.count);
126-
}
127-
128-
@Test
129-
public void addTwoListeners_unregisterTwoListeners_jsListenerIsRemoved() {
130-
Registration registration1 = page
131-
.addBrowserWindowResizeListener(listener);
132-
Registration registration2 = page
133-
.addBrowserWindowResizeListener(event -> {
134-
});
135-
136-
registration1.remove();
137-
registration2.remove();
138-
139-
Assert.assertEquals(2, page.count);
140-
141-
Assert.assertEquals("$0.resizeRemove()", page.expression);
142-
143-
Assert.assertTrue(page.firstParam instanceof Component);
144-
}
145-
146-
@Test
147-
public void addListener_unregisterListener_addListener_jsListenerIsRemovedAndInitialized() {
148-
Registration registration = page
149-
.addBrowserWindowResizeListener(listener);
150-
151-
registration.remove();
152-
// remove several times
153-
registration.remove();
154-
155-
Assert.assertEquals(2, page.count);
156-
157-
Assert.assertEquals("$0.resizeRemove()", page.expression);
158-
159-
page.addBrowserWindowResizeListener(listener);
160-
161-
Assert.assertEquals(3, page.count);
162-
163-
MatcherAssert.assertThat(page.expression,
164-
CoreMatchers.allOf(CoreMatchers.containsString("init"),
165-
CoreMatchers.containsString("resize")));
166-
167-
Assert.assertTrue(page.firstParam instanceof Component);
168-
}
169-
17088
@Test
17189
public void retrieveExtendedClientDetails_twice_theSecondResultComesDifferentBeforeCachedValueIsSet() {
17290
// given

flow-tests/test-root-context/src/main/java/com/vaadin/flow/uitest/ui/BrowserWindowResizeView.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package com.vaadin.flow.uitest.ui;
1717

1818
import com.vaadin.flow.component.html.Div;
19+
import com.vaadin.flow.component.html.NativeButton;
1920
import com.vaadin.flow.router.Route;
2021
import com.vaadin.flow.uitest.servlet.ViewTestLayout;
2122

@@ -28,9 +29,17 @@ protected void onShow() {
2829

2930
windowSize.setId("size-info");
3031

31-
getPage().addBrowserWindowResizeListener(
32-
event -> windowSize.setText(String.valueOf(event.getWidth())));
32+
getPage().addBrowserWindowResizeListener(event -> windowSize.setText(
33+
"%sx%s".formatted(event.getWidth(), event.getHeight())));
3334

3435
add(windowSize);
36+
37+
var modalBtn = new NativeButton("Open modal (should keep working");
38+
modalBtn.setId("modal");
39+
modalBtn.addClickListener(e -> {
40+
add(new Div("Now modal, but resize events should still flow in"));
41+
getUI().get().addModal(new Div());
42+
});
43+
add(modalBtn);
3544
}
3645
}

0 commit comments

Comments
 (0)