Skip to content

Commit

Permalink
feat: Throttle page resize events (#19122)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
3 people authored Apr 12, 2024
1 parent f0811e0 commit 6cdc736
Show file tree
Hide file tree
Showing 6 changed files with 76 additions and 224 deletions.
124 changes: 29 additions & 95 deletions flow-server/src/main/java/com/vaadin/flow/component/page/Page.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,39 +15,30 @@
*/
package com.vaadin.flow.component.page;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Serializable;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Objects;
import java.util.function.Function;

import com.vaadin.flow.component.ClientCallable;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.ComponentEvent;
import com.vaadin.flow.component.Direction;
import com.vaadin.flow.component.Tag;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.dependency.JavaScript;
import com.vaadin.flow.component.dependency.JsModule;
import com.vaadin.flow.component.dependency.StyleSheet;
import com.vaadin.flow.component.internal.AllowInert;
import com.vaadin.flow.component.internal.PendingJavaScriptInvocation;
import com.vaadin.flow.component.internal.UIInternals.JavaScriptInvocation;
import com.vaadin.flow.dom.DomListenerRegistration;
import com.vaadin.flow.dom.Element;
import com.vaadin.flow.function.SerializableConsumer;
import com.vaadin.flow.internal.UrlUtil;
import com.vaadin.flow.shared.Registration;
import com.vaadin.flow.shared.ui.Dependency;
import com.vaadin.flow.shared.ui.Dependency.Type;
import com.vaadin.flow.shared.ui.LoadMode;

import elemental.json.JsonObject;
import elemental.json.JsonType;
import elemental.json.JsonValue;
Expand All @@ -61,59 +52,10 @@
*/
public class Page implements Serializable {

@Tag(Tag.DIV)
private static class ResizeEventReceiver extends Component {

private int windowResizeListenersSize;

@ClientCallable
@AllowInert
private void windowResized(int width, int height) {
if (windowResizeListenersSize != 0) {
fireEvent(new ResizeEvent(this, width, height));
}
}

private Registration addListener(BrowserWindowResizeListener listener) {
windowResizeListenersSize++;
Registration registration = addListener(ResizeEvent.class,
event -> listener
.browserWindowResized(event.getApiEvent()));

Registration combined = Registration
.combine(this::listenerIsUnregistered, registration);
return Registration.once(combined::remove);
}

private void listenerIsUnregistered() {
windowResizeListenersSize--;
if (windowResizeListenersSize == 0) {
// remove JS listener
getUI().get().getPage().executeJs("$0.resizeRemove()", this);
}
}
}

private static class ResizeEvent
extends ComponentEvent<ResizeEventReceiver> {

private final BrowserWindowResizeEvent apiEvent;

private ResizeEvent(ResizeEventReceiver source, int width, int height) {
super(source, true);
apiEvent = new BrowserWindowResizeEvent(
source.getUI().get().getPage(), width, height);

}

private BrowserWindowResizeEvent getApiEvent() {
return apiEvent;
}
}

private ResizeEventReceiver resizeReceiver;
private final UI ui;
private final History history;
private DomListenerRegistration resizeReceiver;
private ArrayList<BrowserWindowResizeListener> resizeListeners;

/**
* Creates a page instance for the given UI.
Expand Down Expand Up @@ -373,17 +315,33 @@ public Registration addBrowserWindowResizeListener(
BrowserWindowResizeListener resizeListener) {
Objects.requireNonNull(resizeListener);
if (resizeReceiver == null) {
// lazy creation which is done only one time since there is no way
// to remove virtual children
resizeReceiver = new ResizeEventReceiver();
ui.getElement().appendVirtualChild(resizeReceiver.getElement());
// "republish" on the UI element, so can be listened with core APIs
ui.getElement().executeJs("""
const el = this;
window.addEventListener('resize', evt => {
const event = new Event("window-resize");
event.w = document.documentElement.clientWidth;
event.h = document.documentElement.clientHeight;
el.dispatchEvent(event);
});
""");
resizeReceiver = ui.getElement()
.addEventListener("window-resize", e -> {
var evt = new BrowserWindowResizeEvent(this,
(int) e.getEventData().getNumber("event.w"),
(int) e.getEventData().getNumber("event.h"));
// Clone list to avoid issues if listener unregisters
// itself
new ArrayList<>(resizeListeners)
.forEach(l -> l.browserWindowResized(evt));
}).addEventData("event.w").addEventData("event.h")
.debounce(300).allowInert();
}
if (resizeReceiver.windowResizeListenersSize == 0) {
// JS resize listener may be completely disabled if there are not
// listeners
executeJs(LazyJsLoader.WINDOW_LISTENER_JS, resizeReceiver);
if (resizeListeners == null) {
resizeListeners = new ArrayList<>(1);
}
return resizeReceiver.addListener(resizeListener);
resizeListeners.add(resizeListener);
return () -> resizeListeners.remove(resizeListener);
}

/**
Expand Down Expand Up @@ -467,30 +425,6 @@ private void addDependency(Dependency dependency) {
ui.getInternals().getDependencyList().add(dependency);
}

private static class LazyJsLoader implements Serializable {

private static final String JS_FILE_NAME = "windowResizeListener.js";

private static final String WINDOW_LISTENER_JS = readJS();

private static String readJS() {
try (InputStream stream = Page.class
.getResourceAsStream(JS_FILE_NAME);
BufferedReader bf = new BufferedReader(
new InputStreamReader(stream,
StandardCharsets.UTF_8))) {
StringBuilder builder = new StringBuilder();
bf.lines().forEach(builder::append);
return builder.toString();
} catch (IOException e) {
throw new RuntimeException(
"Couldn't read window resize listener JavaScript file "
+ JS_FILE_NAME + ". The package is broken",
e);
}
}
}

/**
* Callback for receiving extended client-side details.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -436,13 +436,13 @@ public void fireEvent(DomEvent event) {
if (listeners == null) {
return;
}

final boolean isElementEnabled = event.getSource().isEnabled();

final boolean isNavigationRequest = UI.BrowserNavigateEvent.EVENT_NAME
.equals(event.getType())
|| UI.BrowserLeaveNavigationEvent.EVENT_NAME
.equals(event.getType());

final boolean inert = event.getSource().getNode().isInert();

List<DomEventListenerWrapper> typeListeners = listeners
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,12 @@
import org.junit.Test;
import org.mockito.Mockito;

import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.function.SerializableConsumer;
import com.vaadin.flow.internal.JsonUtils;
import com.vaadin.flow.shared.Registration;
import com.vaadin.flow.shared.ui.Dependency;
import com.vaadin.flow.shared.ui.LoadMode;
import com.vaadin.tests.util.MockUI;

import elemental.json.Json;
import elemental.json.JsonValue;

Expand Down Expand Up @@ -88,85 +85,6 @@ public void addNullAsAListener_trows() {
page.addBrowserWindowResizeListener(null);
}

@Test
public void addListener_executeInitJs() {
page.addBrowserWindowResizeListener(listener);

MatcherAssert.assertThat(page.expression,
CoreMatchers.allOf(CoreMatchers.containsString("init"),
CoreMatchers.containsString("resize")));

Assert.assertTrue(page.firstParam instanceof Component);
}

@Test
public void addTwoListeners_jsIsExecutedOnce() {
page.addBrowserWindowResizeListener(listener);
page.addBrowserWindowResizeListener(event -> {
});

Assert.assertEquals(1, page.count);
}

@Test
public void addTwoListeners_unregisterOneListener_jsListenerIsNotRemoved() {
page.addBrowserWindowResizeListener(listener);
Registration registration = page
.addBrowserWindowResizeListener(event -> {
});

registration.remove();

Assert.assertEquals(1, page.count);

// remove the same listener one more time
registration.remove();

Assert.assertEquals(1, page.count);
}

@Test
public void addTwoListeners_unregisterTwoListeners_jsListenerIsRemoved() {
Registration registration1 = page
.addBrowserWindowResizeListener(listener);
Registration registration2 = page
.addBrowserWindowResizeListener(event -> {
});

registration1.remove();
registration2.remove();

Assert.assertEquals(2, page.count);

Assert.assertEquals("$0.resizeRemove()", page.expression);

Assert.assertTrue(page.firstParam instanceof Component);
}

@Test
public void addListener_unregisterListener_addListener_jsListenerIsRemovedAndInitialized() {
Registration registration = page
.addBrowserWindowResizeListener(listener);

registration.remove();
// remove several times
registration.remove();

Assert.assertEquals(2, page.count);

Assert.assertEquals("$0.resizeRemove()", page.expression);

page.addBrowserWindowResizeListener(listener);

Assert.assertEquals(3, page.count);

MatcherAssert.assertThat(page.expression,
CoreMatchers.allOf(CoreMatchers.containsString("init"),
CoreMatchers.containsString("resize")));

Assert.assertTrue(page.firstParam instanceof Component);
}

@Test
public void retrieveExtendedClientDetails_twice_theSecondResultComesDifferentBeforeCachedValueIsSet() {
// given
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package com.vaadin.flow.uitest.ui;

import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.NativeButton;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.uitest.servlet.ViewTestLayout;

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

windowSize.setId("size-info");

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

add(windowSize);

var modalBtn = new NativeButton("Open modal (should keep working");
modalBtn.setId("modal");
modalBtn.addClickListener(e -> {
add(new Div("Now modal, but resize events should still flow in"));
getUI().get().addModal(new Div());
});
add(modalBtn);
}
}
Loading

0 comments on commit 6cdc736

Please sign in to comment.