Skip to content

Commit 08c4c51

Browse files
committed
feat: eagerly load ExtendedClientDetails during UI initialization
ExtendedClientDetails (browser information like screen size, timezone, etc.) is now automatically fetched during UI initialization and immediately available, instead of requiring asynchronous callbacks. Client-side changes (Flow.ts): - Collect browser details using getBrowserDetailsParameters() - Send details as JSON in v-r=init request (v-browserDetails parameter) Server-side changes: - JavaScriptBootstrapHandler: Extract and parse browser details from init request, store in UIInternals before UI constructor runs - ExtendedClientDetails.fromJson(): New static factory method for parsing browser details from JSON (shared by init and refresh) - Page.getExtendedClientDetails(): Never returns null - creates placeholder with default values if not yet available - Page.retrieveExtendedClientDetails(): Deprecated method maintains backward compatibility by returning cached values when available - ExtendedClientDetails.refresh(): Updated to use shared JSON parsing Test updates: - MockUI: Override getPage() to return the mocked Page instance for proper test mocking - PageTest: Removed obsolete tests for old async-only behavior - Test views updated to use new synchronous API Benefits: - Browser details available immediately in UI constructor/onAttach - Eliminates null checks when accessing ExtendedClientDetails - Consistent JSON format for init and refresh operations - Cleaner API with shared parsing logic - Full backward compatibility with deprecated async API
1 parent bd2d668 commit 08c4c51

File tree

9 files changed

+273
-173
lines changed

9 files changed

+273
-173
lines changed

flow-client/src/main/frontend/Flow.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -419,9 +419,16 @@ export class Flow {
419419
return new Promise((resolve, reject) => {
420420
const xhr = new XMLHttpRequest();
421421
const httpRequest = xhr as any;
422+
423+
// Collect browser details to send with init request as JSON
424+
const browserDetails = ($wnd.Vaadin.Flow as any).getBrowserDetailsParameters();
425+
const browserDetailsParam = browserDetails
426+
? `&v-browserDetails=${encodeURIComponent(JSON.stringify(browserDetails))}`
427+
: '';
428+
422429
const requestPath = `?v-r=init&location=${encodeURIComponent(
423430
this.getFlowRoutePath(location)
424-
)}&query=${encodeURIComponent(this.getFlowRouteQuery(location))}`;
431+
)}&query=${encodeURIComponent(this.getFlowRouteQuery(location))}${browserDetailsParam}`;
425432

426433
httpRequest.open('GET', requestPath);
427434

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

Lines changed: 96 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,32 @@
1818
import java.io.Serializable;
1919
import java.util.Date;
2020
import java.util.TimeZone;
21+
import java.util.function.Consumer;
22+
import java.util.function.Function;
2123

24+
import tools.jackson.databind.JsonNode;
25+
import tools.jackson.databind.node.JsonNodeType;
26+
import tools.jackson.databind.node.ObjectNode;
27+
28+
import com.vaadin.flow.component.UI;
29+
import com.vaadin.flow.function.SerializableConsumer;
2230
import com.vaadin.flow.server.VaadinSession;
2331

2432
/**
2533
* Provides extended information about the web browser, such as screen
2634
* resolution and time zone.
2735
* <p>
28-
* Please note that all information is fetched only once, and <em>not updated
29-
* automatically</em>. To retrieve updated values, you can execute JS with
30-
* {@link Page#executeJs(String, Object...)} and get the current value back.
36+
* Browser details are automatically fetched on the first call to
37+
* {@link Page#getExtendedClientDetails()} and cached for the lifetime of the
38+
* UI. The fetch happens asynchronously, so the first call may return
39+
* {@code null} while the data is being retrieved. To update the cached values
40+
* with fresh data from the browser, use {@link #refresh(Consumer)}.
3141
*
3242
* @author Vaadin Ltd
3343
* @since 2.0
3444
*/
3545
public class ExtendedClientDetails implements Serializable {
46+
private final UI ui;
3647
private int screenWidth = -1;
3748
private int screenHeight = -1;
3849
private int windowInnerWidth = -1;
@@ -54,6 +65,8 @@ public class ExtendedClientDetails implements Serializable {
5465
* For internal use only. Updates all properties in the class according to
5566
* the given information.
5667
*
68+
* @param ui
69+
* the UI instance that owns this ExtendedClientDetails
5770
* @param screenWidth
5871
* Screen width
5972
* @param screenHeight
@@ -88,13 +101,14 @@ public class ExtendedClientDetails implements Serializable {
88101
* @param navigatorPlatform
89102
* navigation platform received from the browser
90103
*/
91-
ExtendedClientDetails(String screenWidth, String screenHeight,
104+
ExtendedClientDetails(UI ui, String screenWidth, String screenHeight,
92105
String windowInnerWidth, String windowInnerHeight,
93106
String bodyClientWidth, String bodyClientHeight, String tzOffset,
94107
String rawTzOffset, String dstShift, String dstInEffect,
95108
String tzId, String curDate, String touchDevice,
96109
String devicePixelRatio, String windowName,
97110
String navigatorPlatform) {
111+
this.ui = ui;
98112
if (screenWidth != null) {
99113
try {
100114
this.screenWidth = Integer.parseInt(screenWidth);
@@ -382,4 +396,82 @@ public boolean isIOS() {
382396
|| (navigatorPlatform != null
383397
&& navigatorPlatform.startsWith("iPod"));
384398
}
399+
400+
/**
401+
* Creates an ExtendedClientDetails instance from browser details JSON
402+
* object. This is intended for internal use when browser details are
403+
* provided as JSON (e.g., during UI initialization or refresh).
404+
* <p>
405+
* For internal use only.
406+
*
407+
* @param ui
408+
* the UI instance that owns this ExtendedClientDetails
409+
* @param json
410+
* the JSON object containing browser details parameters
411+
* @return a new ExtendedClientDetails instance
412+
* @throws RuntimeException
413+
* if the JSON is not a valid object
414+
*/
415+
public static ExtendedClientDetails fromJson(UI ui, JsonNode json) {
416+
if (!(json instanceof ObjectNode)) {
417+
throw new RuntimeException("Expected a JSON object");
418+
}
419+
final ObjectNode jsonObj = (ObjectNode) json;
420+
421+
// Note that JSON returned is a plain string -> string map, the actual
422+
// parsing of the fields happens in ExtendedClient's constructor. If a
423+
// field is missing or the wrong type, pass on null for default.
424+
final Function<String, String> getStringElseNull = key -> {
425+
final JsonNode jsValue = jsonObj.get(key);
426+
if (jsValue != null
427+
&& JsonNodeType.STRING.equals(jsValue.getNodeType())) {
428+
return jsValue.asString();
429+
} else {
430+
return null;
431+
}
432+
};
433+
434+
return new ExtendedClientDetails(ui, getStringElseNull.apply("v-sw"),
435+
getStringElseNull.apply("v-sh"),
436+
getStringElseNull.apply("v-ww"),
437+
getStringElseNull.apply("v-wh"),
438+
getStringElseNull.apply("v-bw"),
439+
getStringElseNull.apply("v-bh"),
440+
getStringElseNull.apply("v-tzo"),
441+
getStringElseNull.apply("v-rtzo"),
442+
getStringElseNull.apply("v-dstd"),
443+
getStringElseNull.apply("v-dston"),
444+
getStringElseNull.apply("v-tzid"),
445+
getStringElseNull.apply("v-curdate"),
446+
getStringElseNull.apply("v-td"),
447+
getStringElseNull.apply("v-pr"),
448+
getStringElseNull.apply("v-wn"),
449+
getStringElseNull.apply("v-np"));
450+
}
451+
452+
/**
453+
* Refreshes the browser details by fetching updated values from the
454+
* browser. The refresh happens asynchronously. The cached values in this
455+
* instance will be updated when the browser responds, and then the provided
456+
* callback will be invoked with the updated details.
457+
*
458+
* @param callback
459+
* a callback that will be invoked with the updated
460+
* ExtendedClientDetails when the refresh is complete
461+
*/
462+
public void refresh(Consumer<ExtendedClientDetails> callback) {
463+
final String js = "return Vaadin.Flow.getBrowserDetailsParameters();";
464+
final SerializableConsumer<JsonNode> resultHandler = json -> {
465+
ExtendedClientDetails details = fromJson(ui, json);
466+
ui.getInternals().setExtendedClientDetails(details);
467+
if (callback != null) {
468+
callback.accept(details);
469+
}
470+
};
471+
final SerializableConsumer<String> errorHandler = err -> {
472+
throw new RuntimeException("Unable to retrieve extended "
473+
+ "client details. JS error is '" + err + "'");
474+
};
475+
ui.getPage().executeJs(js).then(resultHandler, errorHandler);
476+
}
385477
}

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

Lines changed: 45 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,7 @@
2323
import java.util.Arrays;
2424
import java.util.Objects;
2525
import java.util.UUID;
26-
import java.util.function.Function;
27-
28-
import tools.jackson.databind.JsonNode;
29-
import tools.jackson.databind.node.JsonNodeType;
30-
import tools.jackson.databind.node.ObjectNode;
26+
import java.util.function.Consumer;
3127

3228
import com.vaadin.flow.component.Direction;
3329
import com.vaadin.flow.component.UI;
@@ -481,76 +477,62 @@ public interface ExtendedClientDetailsReceiver extends Serializable {
481477
void receiveDetails(ExtendedClientDetails extendedClientDetails);
482478
}
483479

480+
/**
481+
* Gets the extended client details, such as screen resolution and time zone
482+
* information.
483+
* <p>
484+
* Browser details are automatically fetched during UI initialization and
485+
* cached for the lifetime of the UI. In normal operation, this method
486+
* returns complete details immediately after the UI is created. As a
487+
* fallback, if details are not yet available, an asynchronous fetch is
488+
* triggered automatically and a placeholder instance with default values is
489+
* returned (which will be updated when the fetch completes). To refresh the
490+
* cached values with updated data from the browser, use
491+
* {@link ExtendedClientDetails#refresh(Consumer)}.
492+
* <p>
493+
* If you need to perform an action when the details are updated, use
494+
* {@link ExtendedClientDetails#refresh(Consumer)} with a callback.
495+
*
496+
* @return the extended client details (never {@code null})
497+
*/
498+
public ExtendedClientDetails getExtendedClientDetails() {
499+
ExtendedClientDetails details = ui.getInternals()
500+
.getExtendedClientDetails();
501+
if (details == null) {
502+
// Create placeholder instance with default values
503+
ExtendedClientDetails placeholder = new ExtendedClientDetails(ui,
504+
null, null, null, null, null, null, null, null, null, null,
505+
null, null, null, null, null, null);
506+
// Store placeholder immediately so we don't return null
507+
ui.getInternals().setExtendedClientDetails(placeholder);
508+
return placeholder;
509+
}
510+
return details;
511+
}
512+
484513
/**
485514
* Obtain extended client side details, such as time screen and time zone
486515
* information, via callback. If already obtained, the callback is called
487516
* directly. Otherwise, a client-side roundtrip will be carried out.
488517
*
489518
* @param receiver
490519
* the callback to which the details are provided
520+
* @deprecated Use {@link #getExtendedClientDetails()} to get the cached
521+
* details, or {@link ExtendedClientDetails#refresh(Consumer)}
522+
* to refresh the cached values.
491523
*/
524+
@Deprecated
492525
public void retrieveExtendedClientDetails(
493526
ExtendedClientDetailsReceiver receiver) {
494-
final ExtendedClientDetails cachedDetails = ui.getInternals()
527+
ExtendedClientDetails details = ui.getInternals()
495528
.getExtendedClientDetails();
496-
if (cachedDetails != null) {
497-
receiver.receiveDetails(cachedDetails);
498-
return;
499-
}
500-
final String js = "return Vaadin.Flow.getBrowserDetailsParameters();";
501-
final SerializableConsumer<JsonNode> resultHandler = json -> {
502-
handleExtendedClientDetailsResponse(json);
503-
receiver.receiveDetails(
504-
ui.getInternals().getExtendedClientDetails());
505-
};
506-
final SerializableConsumer<String> errorHandler = err -> {
507-
throw new RuntimeException("Unable to retrieve extended "
508-
+ "client details. JS error is '" + err + "'");
509-
};
510-
executeJs(js).then(resultHandler, errorHandler);
511-
}
512-
513-
private void handleExtendedClientDetailsResponse(JsonNode json) {
514-
ExtendedClientDetails cachedDetails = ui.getInternals()
515-
.getExtendedClientDetails();
516-
if (cachedDetails != null) {
517-
return;
518-
}
519-
if (!(json instanceof ObjectNode)) {
520-
throw new RuntimeException("Expected a JSON object");
529+
if (details != null && details.getScreenWidth() != -1) {
530+
// Already fetched and complete, call receiver immediately
531+
receiver.receiveDetails(details);
532+
} else {
533+
// Not available or placeholder, trigger refresh
534+
getExtendedClientDetails().refresh(receiver::receiveDetails);
521535
}
522-
final ObjectNode jsonObj = (ObjectNode) json;
523-
524-
// Note that JSON returned is a plain string -> string map, the actual
525-
// parsing of the fields happens in ExtendedClient's constructor. If a
526-
// field is missing or the wrong type, pass on null for default.
527-
final Function<String, String> getStringElseNull = key -> {
528-
final JsonNode jsValue = jsonObj.get(key);
529-
if (jsValue != null
530-
&& JsonNodeType.STRING.equals(jsValue.getNodeType())) {
531-
return jsValue.asString();
532-
} else {
533-
return null;
534-
}
535-
};
536-
ui.getInternals()
537-
.setExtendedClientDetails(new ExtendedClientDetails(
538-
getStringElseNull.apply("v-sw"),
539-
getStringElseNull.apply("v-sh"),
540-
getStringElseNull.apply("v-ww"),
541-
getStringElseNull.apply("v-wh"),
542-
getStringElseNull.apply("v-bw"),
543-
getStringElseNull.apply("v-bh"),
544-
getStringElseNull.apply("v-tzo"),
545-
getStringElseNull.apply("v-rtzo"),
546-
getStringElseNull.apply("v-dstd"),
547-
getStringElseNull.apply("v-dston"),
548-
getStringElseNull.apply("v-tzid"),
549-
getStringElseNull.apply("v-curdate"),
550-
getStringElseNull.apply("v-td"),
551-
getStringElseNull.apply("v-pr"),
552-
getStringElseNull.apply("v-wn"),
553-
getStringElseNull.apply("v-np")));
554536
}
555537

556538
/**

flow-server/src/main/java/com/vaadin/flow/server/communication/JavaScriptBootstrapHandler.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,14 @@
2121
import java.util.Optional;
2222
import java.util.function.Function;
2323

24+
import org.slf4j.Logger;
25+
import org.slf4j.LoggerFactory;
2426
import tools.jackson.databind.JsonNode;
2527
import tools.jackson.databind.node.ObjectNode;
2628

2729
import com.vaadin.flow.component.PushConfiguration;
2830
import com.vaadin.flow.component.UI;
31+
import com.vaadin.flow.component.page.ExtendedClientDetails;
2932
import com.vaadin.flow.internal.BootstrapHandlerHelper;
3033
import com.vaadin.flow.internal.DevModeHandler;
3134
import com.vaadin.flow.internal.DevModeHandlerManager;
@@ -137,6 +140,11 @@ private boolean isServletRootRequest(VaadinRequest request) {
137140
return pathInfo == null || "".equals(pathInfo) || "/".equals(pathInfo);
138141
}
139142

143+
private static Logger getLogger() {
144+
return LoggerFactory
145+
.getLogger(JavaScriptBootstrapHandler.class.getName());
146+
}
147+
140148
protected String getRequestUrl(VaadinRequest request) {
141149
return ((VaadinServletRequest) request).getRequestURL().toString();
142150
}
@@ -163,9 +171,40 @@ protected BootstrapContext createAndInitUI(Class<? extends UI> uiClass,
163171

164172
config.put("requestURL", requestURL);
165173

174+
// Parse browser details from request parameters and store in UI
175+
extractAndStoreBrowserDetails(request, context.getUI());
176+
166177
return context;
167178
}
168179

180+
/**
181+
* Extracts browser details from the request JSON parameter and stores them
182+
* in the UI's internals as ExtendedClientDetails.
183+
*
184+
* @param request
185+
* the request containing browser details as JSON parameter
186+
* @param ui
187+
* the UI instance to store the details in
188+
*/
189+
private void extractAndStoreBrowserDetails(VaadinRequest request, UI ui) {
190+
// Extract browser details JSON parameter from request
191+
// This is sent by the client in the v-r=init request
192+
String browserDetailsJson = request.getParameter("v-browserDetails");
193+
194+
if (browserDetailsJson != null && !browserDetailsJson.isEmpty()) {
195+
try {
196+
JsonNode json = JacksonUtils.readTree(browserDetailsJson);
197+
ExtendedClientDetails details = ExtendedClientDetails
198+
.fromJson(ui, json);
199+
ui.getInternals().setExtendedClientDetails(details);
200+
} catch (Exception e) {
201+
// Log and continue without browser details
202+
getLogger().debug(
203+
"Failed to parse browser details from init request", e);
204+
}
205+
}
206+
}
207+
169208
@Override
170209
protected void initializeUIWithRouter(BootstrapContext context, UI ui) {
171210
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ private class ExtendBuilder {
163163
private String navigatorPlatform = "Linux i686";
164164

165165
public ExtendedClientDetails buildDetails() {
166-
return new ExtendedClientDetails(screenWidth, screenHeight,
166+
return new ExtendedClientDetails(null, screenWidth, screenHeight,
167167
windowInnerWidth, windowInnerHeight, bodyClientWidth,
168168
bodyClientHeight, timezoneOffset, rawTimezoneOffset,
169169
dstSavings, dstInEffect, timeZoneId, clientServerTimeDelta,

0 commit comments

Comments
 (0)