From 1e5e53ee830123e5fee443f62f2ef56652a7f6d6 Mon Sep 17 00:00:00 2001 From: Vaadin Bot Date: Fri, 9 Aug 2024 10:16:43 +0200 Subject: [PATCH] feat: push and replace without navigation callback (#19736) (#19768) * feat: push and replace without navigation callback Add feature to support replace and push without getting a callback to the server for the change. fixes #19613 * change default callback for replace also to false * history gets same events as previously with vaadin-router. * Forward should callback to update url. * Fix java doc link Co-authored-by: caalador --- .../vaadin/flow/component/page/History.java | 95 +++++++++++++++++-- .../AbstractNavigationStateRenderer.java | 2 +- .../com/vaadin/flow/server/frontend/Flow.tsx | 4 +- .../internal/JavaScriptBootstrapUITest.java | 4 +- .../flow/component/page/HistoryTest.java | 4 +- .../com/vaadin/flow/uitest/ui/HistoryIT.java | 5 +- 6 files changed, 98 insertions(+), 16 deletions(-) diff --git a/flow-server/src/main/java/com/vaadin/flow/component/page/History.java b/flow-server/src/main/java/com/vaadin/flow/component/page/History.java index fdb32bc892d..59b648a037f 100644 --- a/flow-server/src/main/java/com/vaadin/flow/component/page/History.java +++ b/flow-server/src/main/java/com/vaadin/flow/component/page/History.java @@ -172,7 +172,29 @@ public void pushState(JsonValue state, String location) { /** * Invokes history.pushState in the browser with the given - * parameters. + * parameters. This is a shorthand method for + * {@link History#pushState(JsonValue, Location, boolean)}, creating + * {@link Location} from the string provided. + * + * @param state + * the JSON state to push to the history stack, or + * null to only change the location + * @param location + * the new location to set in the browser, or null + * to only change the JSON state + * @param callback + * {@code true} if the change should make a return call to the + * server + */ + public void pushState(JsonValue state, String location, boolean callback) { + pushState(state, + Optional.ofNullable(location).map(Location::new).orElse(null), + callback); + } + + /** + * Invokes history.pushState in the browser with the given + * parameters. Does not make a callback to the server by default. * * @param state * the JSON state to push to the history stack, or @@ -182,14 +204,33 @@ public void pushState(JsonValue state, String location) { * to only change the JSON state */ public void pushState(JsonValue state, Location location) { + pushState(state, location, false); + } + + /** + * Invokes history.pushState in the browser with the given + * parameters. + * + * @param state + * the JSON state to push to the history stack, or + * null to only change the location + * @param location + * the new location to set in the browser, or null + * to only change the JSON state + * @param callback + * {@code true} if the change should make a return call to the + * server + */ + public void pushState(JsonValue state, Location location, + boolean callback) { final String pathWithQueryParameters = getPathWithQueryParameters( location); // Second parameter is title which is currently ignored according to // https://developer.mozilla.org/en-US/docs/Web/API/History_API if (ui.getSession().getConfiguration().isReactEnabled()) { ui.getPage().executeJs( - "window.dispatchEvent(new CustomEvent('vaadin-navigate', { detail: { state: $0, url: $1, replace: false } }));", - state, pathWithQueryParameters); + "window.dispatchEvent(new CustomEvent('vaadin-navigate', { detail: { state: $0, url: $1, replace: false, callback: $2 } }));", + state, pathWithQueryParameters, callback); } else { ui.getPage().executeJs( "setTimeout(() => { window.history.pushState($0, '', $1); window.dispatchEvent(new CustomEvent('vaadin-navigated')); })", @@ -217,7 +258,30 @@ public void replaceState(JsonValue state, String location) { /** * Invokes history.replaceState in the browser with the given - * parameters. + * parameters. This is a shorthand method for + * {@link History#replaceState(JsonValue, Location, boolean)}, creating + * {@link Location} from the string provided. + * + * @param state + * the JSON state to push to the history stack, or + * null to only change the location + * @param location + * the new location to set in the browser, or null + * to only change the JSON state + * @param callback + * {@code true} if the change should make a return call to the + * server + */ + public void replaceState(JsonValue state, String location, + boolean callback) { + replaceState(state, + Optional.ofNullable(location).map(Location::new).orElse(null), + callback); + } + + /** + * Invokes history.replaceState in the browser with the given + * parameters. Does not make a callback to the server by default. * * @param state * the JSON state to push to the history stack, or @@ -227,14 +291,33 @@ public void replaceState(JsonValue state, String location) { * to only change the JSON state */ public void replaceState(JsonValue state, Location location) { + replaceState(state, location, false); + } + + /** + * Invokes history.replaceState in the browser with the given + * parameters. + * + * @param state + * the JSON state to push to the history stack, or + * null to only change the location + * @param location + * the new location to set in the browser, or null + * to only change the JSON state + * @param callback + * {@code true} if the change should make a return call to the + * server + */ + public void replaceState(JsonValue state, Location location, + boolean callback) { final String pathWithQueryParameters = getPathWithQueryParameters( location); // Second parameter is title which is currently ignored according to // https://developer.mozilla.org/en-US/docs/Web/API/History_API if (ui.getSession().getConfiguration().isReactEnabled()) { ui.getPage().executeJs( - "window.dispatchEvent(new CustomEvent('vaadin-navigate', { detail: { state: $0, url: $1, replace: true } }));", - state, pathWithQueryParameters); + "window.dispatchEvent(new CustomEvent('vaadin-navigate', { detail: { state: $0, url: $1, replace: true, callback: $2 } }));", + state, pathWithQueryParameters, callback); } else { ui.getPage().executeJs( "setTimeout(() => { window.history.replaceState($0, '', $1); window.dispatchEvent(new CustomEvent('vaadin-navigated')); })", diff --git a/flow-server/src/main/java/com/vaadin/flow/router/internal/AbstractNavigationStateRenderer.java b/flow-server/src/main/java/com/vaadin/flow/router/internal/AbstractNavigationStateRenderer.java index 21077ef8d6f..5d3293d1b8b 100644 --- a/flow-server/src/main/java/com/vaadin/flow/router/internal/AbstractNavigationStateRenderer.java +++ b/flow-server/src/main/java/com/vaadin/flow/router/internal/AbstractNavigationStateRenderer.java @@ -729,7 +729,7 @@ private int forward(NavigationEvent event, BeforeEvent beforeNavigation) { NavigationEvent newNavigationEvent = getNavigationEvent(event, beforeNavigation); newNavigationEvent.getUI().getPage().getHistory().replaceState(null, - newNavigationEvent.getLocation()); + newNavigationEvent.getLocation(), true); return handler.handle(newNavigationEvent); } diff --git a/flow-server/src/main/resources/com/vaadin/flow/server/frontend/Flow.tsx b/flow-server/src/main/resources/com/vaadin/flow/server/frontend/Flow.tsx index 65765234066..af41127f33b 100644 --- a/flow-server/src/main/resources/com/vaadin/flow/server/frontend/Flow.tsx +++ b/flow-server/src/main/resources/com/vaadin/flow/server/frontend/Flow.tsx @@ -201,9 +201,9 @@ function Flow() { navigate(path); }, [navigate]); - const vaadinNavigateEventHandler = useCallback((event: CustomEvent<{state: unknown, url: string, replace?: boolean}>) => { + const vaadinNavigateEventHandler = useCallback((event: CustomEvent<{state: unknown, url: string, replace?: boolean, callback: boolean}>) => { const path = '/' + event.detail.url; - navigated.current = !event.detail.replace; + navigated.current = !event.detail.callback; navigate(path, { state: event.detail.state, replace: event.detail.replace}); }, [navigate]); diff --git a/flow-server/src/test/java/com/vaadin/flow/component/internal/JavaScriptBootstrapUITest.java b/flow-server/src/test/java/com/vaadin/flow/component/internal/JavaScriptBootstrapUITest.java index c85fe7cfa04..f43815d7b47 100644 --- a/flow-server/src/test/java/com/vaadin/flow/component/internal/JavaScriptBootstrapUITest.java +++ b/flow-server/src/test/java/com/vaadin/flow/component/internal/JavaScriptBootstrapUITest.java @@ -50,7 +50,7 @@ public class JavaScriptBootstrapUITest { private static final String CLIENT_PUSHSTATE_TO = "setTimeout(() => { window.history.pushState($0, '', $1); window.dispatchEvent(new CustomEvent('vaadin-navigated')); })"; - private static final String REACT_PUSHSTATE_TO = "window.dispatchEvent(new CustomEvent('vaadin-navigate', { detail: { state: $0, url: $1, replace: false } }));"; + private static final String REACT_PUSHSTATE_TO = "window.dispatchEvent(new CustomEvent('vaadin-navigate', { detail: { state: $0, url: $1, replace: false, callback: $2 } }));"; private MockServletServiceSessionSetup mocks; private UI ui; @@ -501,7 +501,7 @@ public void server_should_not_doClientRoute_when_navigatingToServer() { } else { assertEquals(CLIENT_PUSHSTATE_TO, execJs.getValue()); } - assertEquals(2, execValues.length); + assertEquals(3, execValues.length); assertNull(execValues[0]); assertEquals("dirty", execValues[1]); } diff --git a/flow-server/src/test/java/com/vaadin/flow/component/page/HistoryTest.java b/flow-server/src/test/java/com/vaadin/flow/component/page/HistoryTest.java index c788c72565f..146102775c3 100644 --- a/flow-server/src/test/java/com/vaadin/flow/component/page/HistoryTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/component/page/HistoryTest.java @@ -73,8 +73,8 @@ public PendingJavaScriptResult executeJs(String expression, private static final String PUSH_STATE_JS = "setTimeout(() => { window.history.pushState($0, '', $1); window.dispatchEvent(new CustomEvent('vaadin-navigated')); })"; private static final String REPLACE_STATE_JS = "setTimeout(() => { window.history.replaceState($0, '', $1); window.dispatchEvent(new CustomEvent('vaadin-navigated')); })"; - private static final String PUSH_STATE_REACT = "window.dispatchEvent(new CustomEvent('vaadin-navigate', { detail: { state: $0, url: $1, replace: false } }));"; - private static final String REPLACE_STATE_REACT = "window.dispatchEvent(new CustomEvent('vaadin-navigate', { detail: { state: $0, url: $1, replace: true } }));"; + private static final String PUSH_STATE_REACT = "window.dispatchEvent(new CustomEvent('vaadin-navigate', { detail: { state: $0, url: $1, replace: false, callback: $2 } }));"; + private static final String REPLACE_STATE_REACT = "window.dispatchEvent(new CustomEvent('vaadin-navigate', { detail: { state: $0, url: $1, replace: true, callback: $2 } }));"; @Before public void setup() { diff --git a/flow-tests/test-root-context/src/test/java/com/vaadin/flow/uitest/ui/HistoryIT.java b/flow-tests/test-root-context/src/test/java/com/vaadin/flow/uitest/ui/HistoryIT.java index b33c9938e6e..beb95bcc2ba 100644 --- a/flow-tests/test-root-context/src/test/java/com/vaadin/flow/uitest/ui/HistoryIT.java +++ b/flow-tests/test-root-context/src/test/java/com/vaadin/flow/uitest/ui/HistoryIT.java @@ -73,9 +73,8 @@ public void testHistory() throws URISyntaxException { // Forward to originally pushed state forwardButton.click(); Assert.assertEquals(baseUrl.resolve("asdf"), getCurrentUrl()); - Assert.assertEquals(Arrays.asList("New location: qwerty", - "New location: asdf", "New state: {\"foo\":true}"), - getStatusMessages()); + Assert.assertEquals(Arrays.asList("New location: asdf", + "New state: {\"foo\":true}"), getStatusMessages()); clearButton.click(); // Back to the replaced state