Skip to content

Commit

Permalink
feat: refresh browser using dev-tools connection (#19650)
Browse files Browse the repository at this point in the history
Uses the dev-tools websocket connection to refresh the UI
when PUSH is not available, instead of forcing a page reload.

Co-authored-by: Teppo Kurki <teppo.kurki@vaadin.com>
  • Loading branch information
mcollovati and tepi authored Jul 26, 2024
1 parent cf3abf3 commit dc12630
Show file tree
Hide file tree
Showing 9 changed files with 142 additions and 44 deletions.
43 changes: 43 additions & 0 deletions flow-server/src/main/java/com/vaadin/flow/component/UI.java
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,8 @@ public void doInit(VaadinRequest request, int uiId, String appId) {
this::leaveNavigation);
getEventBus().addListener(BrowserNavigateEvent.class,
this::browserNavigate);
getEventBus().addListener(BrowserRefreshEvent.class,
this::browserRefresh);

}

Expand Down Expand Up @@ -1257,6 +1259,10 @@ public void refreshCurrentRoute(boolean refreshRouteChain) {
getInternals().refreshCurrentRoute(refreshRouteChain);
}

private void browserRefresh(BrowserRefreshEvent event) {
refreshCurrentRoute(event.refreshRouteChain);
}

/**
* Returns true if this UI instance supports navigation.
*
Expand Down Expand Up @@ -1772,6 +1778,43 @@ public BrowserNavigateEvent(UI source, boolean fromClient,

}

/**
* Event fired by the client to request a refresh of the user interface, by
* re-navigating to the current route.
* <p>
* </p>
* The route target component is re-instantiated, as well as all layouts in
* the route chain if the {@code fullRefresh} event flag is active.
*
* @see #refreshCurrentRoute(boolean)
*/
@DomEvent(BrowserRefreshEvent.EVENT_NAME)
public static class BrowserRefreshEvent extends ComponentEvent<UI> {
public static final String EVENT_NAME = "ui-refresh";

private final boolean refreshRouteChain;

/**
* Creates a new event instance.
*
* @param source
* the UI for which the refresh is requested.
* @param fromClient
* <code>true</code> if the event originated from the client
* side, <code>false</code> otherwise. NOTE: for technical
* reason the argument must be added to the constructor, but
* this event the value is always true.
* @param refreshRouteChain
* {@code true} to refresh all layouts in the route chain,
* {@code false} to only refresh the route instance
*/
public BrowserRefreshEvent(UI source, boolean fromClient,
@EventData("fullRefresh") boolean refreshRouteChain) {
super(source, true);
this.refreshRouteChain = refreshRouteChain;
}
}

/**
* Connect a client with the server side UI. This method is invoked each
* time client router navigates to a server route.
Expand Down
57 changes: 32 additions & 25 deletions flow-server/src/main/java/com/vaadin/flow/hotswap/Hotswapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -219,11 +219,7 @@ private void onHotswapInternal(HashSet<Class<?>> classes,
}
EnumMap<UIRefreshStrategy, List<UI>> refreshActions = computeRefreshStrategies(
vaadinSessions, classes);
forceBrowserReload |= refreshActions
.containsKey(UIRefreshStrategy.RELOAD);
boolean uiTreeNeedsRefresh = refreshActions
.containsKey(UIRefreshStrategy.REFRESH_CHAIN)
|| refreshActions.containsKey(UIRefreshStrategy.REFRESH_ROUTE);
boolean uiTreeNeedsRefresh = !refreshActions.isEmpty();
if (forceBrowserReload || uiTreeNeedsRefresh) {
triggerClientUpdate(refreshActions, forceBrowserReload);
}
Expand All @@ -239,13 +235,17 @@ private enum UIRefreshStrategy {
*/
RELOAD,
/**
* Refresh only route instance.
* Refresh UI without a page reload.
*/
REFRESH_ROUTE,
REFRESH,
/**
* Refresh all layouts in the route chain.
* Refresh only route instance via UI PUSH connection.
*/
REFRESH_CHAIN,
PUSH_REFRESH_ROUTE,
/**
* Refresh all layouts in the route chain via UI PUSH connection.
*/
PUSH_REFRESH_CHAIN,
/**
* Refresh not needed.
*/
Expand Down Expand Up @@ -298,8 +298,8 @@ private UIRefreshStrategy computeRefreshStrategy(UI ui,
if (!targetChainChangedItems.isEmpty()) {
refreshStrategy = targetChainChangedItems.stream()
.allMatch(chainItem -> chainItem == route)
? UIRefreshStrategy.REFRESH_ROUTE
: UIRefreshStrategy.REFRESH_CHAIN;
? UIRefreshStrategy.PUSH_REFRESH_ROUTE
: UIRefreshStrategy.PUSH_REFRESH_CHAIN;
} else {
// Look into the UI tree to find if any component is instance of
// a changed class. If so, detect its parent route or layout to
Expand All @@ -308,9 +308,9 @@ private UIRefreshStrategy computeRefreshStrategy(UI ui,
changedClasses, targetsChain, route);
}

// If push is not enabled we can only request a full page reload
// If push is not enabled we can only request a full page refresh
if (refreshStrategy != UIRefreshStrategy.SKIP && !pushEnabled) {
refreshStrategy = UIRefreshStrategy.RELOAD;
refreshStrategy = UIRefreshStrategy.REFRESH;
}
return refreshStrategy;
}
Expand All @@ -336,10 +336,10 @@ private static UIRefreshStrategy computeRefreshStrategyForUITree(UI ui,
if (!targetsChain.contains(parent)) {
parent = parent.getParent().orElse(null);
} else if (parent == route) {
refreshStrategy = UIRefreshStrategy.REFRESH_ROUTE;
refreshStrategy = UIRefreshStrategy.PUSH_REFRESH_ROUTE;
parent = null;
} else {
refreshStrategy = UIRefreshStrategy.REFRESH_CHAIN;
refreshStrategy = UIRefreshStrategy.PUSH_REFRESH_CHAIN;
parent = null;
stack.clear();
}
Expand All @@ -355,27 +355,34 @@ private void triggerClientUpdate(
EnumMap<UIRefreshStrategy, List<UI>> uisToRefresh,
boolean forceReload) {

// If some UI has push not enabled, BrowserLiveReload should be used
// to trigger a client update. However, BrowserLiveReload broadcasts
// the reload request to all active client connection, making calls to
// UI.refreshCurrentRoute() useless.
if (forceReload) {
if (liveReload != null) {
boolean refreshRequested = uisToRefresh
.containsKey(UIRefreshStrategy.REFRESH);

// If some UI has push not enabled, BrowserLiveReload should be used to
// trigger a client update. However, BrowserLiveReload broadcasts the
// reload/refresh request to all active client connection, making calls
// to UI.refreshCurrentRoute() useless.
if (forceReload || refreshRequested) {
if (liveReload == null) {
LOGGER.debug(
"A change to one or more classes requires a browser page reload, but BrowserLiveReload is not available. "
+ "Please reload the browser page manually to make changes effective.");
} else if (forceReload) {
LOGGER.debug(
"Triggering browser live reload triggered because of classes changes");
"Triggering browser live reload because of classes changes");
liveReload.reload();
} else {
LOGGER.debug(
"A change to one or more classes requires a browser page reload, but BrowserLiveReload is not available. "
+ "Please reload the browser page manually to make changes effective.");
"Triggering browser live refresh because of classes changes");
liveReload.refresh(true);
}
} else {
LOGGER.debug(
"Triggering re-navigation to current route for UIs affected by classes changes.");
for (UIRefreshStrategy action : uisToRefresh.keySet()) {
uisToRefresh.get(action)
.forEach(ui -> ui.access(() -> ui.refreshCurrentRoute(
action == UIRefreshStrategy.REFRESH_CHAIN)));
action == UIRefreshStrategy.PUSH_REFRESH_CHAIN)));
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import org.atmosphere.cpr.AtmosphereResource;

import com.vaadin.flow.component.UI;
import com.vaadin.flow.server.communication.FragmentedMessageHolder;

/**
Expand Down Expand Up @@ -84,6 +85,18 @@ enum Backend {
*/
void reload();

/**
* Requests a refresh of the current view in the browser, without reloading
* the whole page.
*
* @param refreshLayouts
* {@code true} to refresh all layouts in the route chain,
* {@code false} to only refresh the route component
*/
default void refresh(boolean refreshLayouts) {
reload();
}

/**
* Request an update of the resource with the given path.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import com.vaadin.flow.internal.JsonUtils;
import com.vaadin.flow.internal.StateNode;
import com.vaadin.flow.shared.JsonConstants;

import elemental.json.Json;
import elemental.json.JsonObject;
import elemental.json.JsonValue;
Expand Down Expand Up @@ -441,7 +442,8 @@ public void fireEvent(DomEvent event) {
final boolean isNavigationRequest = UI.BrowserNavigateEvent.EVENT_NAME
.equals(event.getType())
|| UI.BrowserLeaveNavigationEvent.EVENT_NAME
.equals(event.getType());
.equals(event.getType())
|| UI.BrowserRefreshEvent.EVENT_NAME.equals(event.getType());

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ public void onHotswap_pushDisabled_routeClassChanged_UINotRefreshedButLiveReload
hotswapper.onHotswap(new String[] { MyRoute.class.getName() }, true);

ui.assertNotRefreshed();
Mockito.verify(liveReload).reload();
Mockito.verify(liveReload).refresh(anyBoolean());
}

@Test
Expand All @@ -277,7 +277,7 @@ public void onHotswap_pushDisabled_routeLayoutClassChanged_UINotRefreshedButLive
hotswapper.onHotswap(new String[] { MyLayout.class.getName() }, true);

ui.assertNotRefreshed();
Mockito.verify(liveReload).reload();
Mockito.verify(liveReload).refresh(anyBoolean());
}

@Test
Expand All @@ -292,7 +292,7 @@ public void onHotswap_pushDisabled_routeChildClassChanged_UINotRefreshedButLiveR
true);

ui.assertNotRefreshed();
Mockito.verify(liveReload).reload();
Mockito.verify(liveReload).refresh(anyBoolean());
}

@Test
Expand All @@ -307,7 +307,7 @@ public void onHotswap_pushDisabled_layoutChildClassChanged_UINotRefreshedButLive
true);

ui.assertNotRefreshed();
Mockito.verify(liveReload).reload();
Mockito.verify(liveReload).refresh(anyBoolean());
}

@Test
Expand All @@ -322,7 +322,7 @@ public void onHotswap_pushDisabled_routeAndLayoutClassesChanged_UINotRefreshedBu
MyLayout.class.getName() }, true);

ui.assertNotRefreshed();
Mockito.verify(liveReload).reload();
Mockito.verify(liveReload).refresh(anyBoolean());
}

@Test
Expand All @@ -337,7 +337,7 @@ public void onHotswap_pushDisabled_routeAndLayoutChildClassChanged_UINotRefreshe
true);

ui.assertNotRefreshed();
Mockito.verify(liveReload).reload();
Mockito.verify(liveReload).refresh(anyBoolean());
}

@Test
Expand All @@ -352,6 +352,7 @@ public void onHotswap_pushDisabled_changedClassNotInUITree_skipLiveReloadAndUIRe

ui.assertNotRefreshed();
Mockito.verify(liveReload, never()).reload();
Mockito.verify(liveReload, never()).refresh(anyBoolean());
}

@Test
Expand All @@ -367,6 +368,7 @@ public void onHotswap_pushEnabled_routeClassChanged_routeRefreshed()

ui.assertRouteRefreshed();
Mockito.verify(liveReload, never()).reload();
Mockito.verify(liveReload, never()).refresh(anyBoolean());
}

@Test
Expand All @@ -383,6 +385,7 @@ public void onHotswap_pushEnabled_routeLayoutClassChanged_activeChainRefreshed()

ui.assertChainRefreshed();
Mockito.verify(liveReload, never()).reload();
Mockito.verify(liveReload, never()).refresh(anyBoolean());
}

@Test
Expand All @@ -400,6 +403,7 @@ public void onHotswap_pushEnabled_routeChildrenClassChanged_routeRefreshed()

ui.assertRouteRefreshed();
Mockito.verify(liveReload, never()).reload();
Mockito.verify(liveReload, never()).refresh(anyBoolean());
}

@Test
Expand All @@ -417,6 +421,7 @@ public void onHotswap_pushEnabled_layoutChildrenClassChanged_activeChainRefreshe

ui.assertChainRefreshed();
Mockito.verify(liveReload, never()).reload();
Mockito.verify(liveReload, never()).refresh(anyBoolean());
}

@Test
Expand All @@ -434,6 +439,7 @@ public void onHotswap_pushEnabled_routeAndLayoutClassesChanged_activeChainRefres

ui.assertChainRefreshed();
Mockito.verify(liveReload, never()).reload();
Mockito.verify(liveReload, never()).refresh(anyBoolean());
}

@Test
Expand All @@ -451,6 +457,7 @@ public void onHotswap_pushEnabled_routeAndLayoutChildClassChanged_activeChainRef

ui.assertChainRefreshed();
Mockito.verify(liveReload, never()).reload();
Mockito.verify(liveReload, never()).refresh(anyBoolean());
}

@Test
Expand All @@ -467,6 +474,7 @@ public void onHotswap_pushEnabled_changedClassNotInUITree_skipRefresh()

ui.assertNotRefreshed();
Mockito.verify(liveReload, never()).reload();
Mockito.verify(liveReload, never()).refresh(anyBoolean());
}

@Test
Expand All @@ -488,7 +496,7 @@ public void onHotswap_mixedPushState_classInUITreeChanged_liveReloadTriggered()

pushUI.assertNotRefreshed();
notPushUI.assertNotRefreshed();
Mockito.verify(liveReload).reload();
Mockito.verify(liveReload).refresh(anyBoolean());
}

@Test
Expand Down
5 changes: 3 additions & 2 deletions vaadin-dev-server/src/main/frontend/live-reload-connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export class LiveReloadConnection extends Connection {
}, Connection.HEARTBEAT_INTERVAL);
}

onReload() {
onReload(_strategy: string) {
// Intentionally empty
}

Expand All @@ -40,7 +40,8 @@ export class LiveReloadConnection extends Connection {
this.onHandshake();
} else if (json.command === 'reload') {
if (this.status === ConnectionStatus.ACTIVE) {
this.onReload();
const strategy = json.strategy || 'reload'
this.onReload(strategy);
}
} else {
this.handleError(`Unknown message from the livereload server: ${msg}`);
Expand Down
Loading

0 comments on commit dc12630

Please sign in to comment.