Skip to content

Commit 5a0a95e

Browse files
committed
feat: eagerly load browser details during UI initialization
Browser details (ExtendedClientDetails) are now automatically fetched and available during UI initialization instead of requiring an asynchronous callback after the UI is created. Changes: - Client-side (Flow.ts): Collect browser details and include them as JSON in the v-r=init request parameter (v-browserDetails) - Server-side (JavaScriptBootstrapHandler): Parse browser details from init request and store in UIInternals before UI constructor runs - ExtendedClientDetails: Add fromJson() static factory method for parsing browser details from JSON (used by both init and refresh) - Page.getExtendedClientDetails(): Now never returns null - creates placeholder with default values and triggers async fetch if needed - Consistent JSON format used for both init request and refresh Benefits: - Browser details available immediately in UI constructor - No null checks needed when calling getExtendedClientDetails() - Cleaner API with shared JSON parsing logic - Maintains backward compatibility with deprecated API
1 parent d66dc47 commit 5a0a95e

File tree

6 files changed

+129
-67
lines changed

6 files changed

+129
-67
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: 57 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -397,11 +397,63 @@ public boolean isIOS() {
397397
&& navigatorPlatform.startsWith("iPod"));
398398
}
399399

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+
400452
/**
401453
* Refreshes the browser details by fetching updated values from the
402454
* browser. The refresh happens asynchronously. The cached values in this
403-
* instance will be updated when the browser responds, and then the
404-
* provided callback will be invoked with the updated details.
455+
* instance will be updated when the browser responds, and then the provided
456+
* callback will be invoked with the updated details.
405457
*
406458
* @param callback
407459
* a callback that will be invoked with the updated
@@ -423,45 +475,8 @@ public void refresh(Consumer<ExtendedClientDetails> callback) {
423475
}
424476

425477
private void handleExtendedClientDetailsResponse(JsonNode json) {
426-
ExtendedClientDetails cachedDetails = ui.getInternals()
427-
.getExtendedClientDetails();
428-
if (cachedDetails != null) {
429-
return;
430-
}
431-
if (!(json instanceof ObjectNode)) {
432-
throw new RuntimeException("Expected a JSON object");
433-
}
434-
final ObjectNode jsonObj = (ObjectNode) json;
435-
436-
// Note that JSON returned is a plain string -> string map, the actual
437-
// parsing of the fields happens in ExtendedClient's constructor. If a
438-
// field is missing or the wrong type, pass on null for default.
439-
final Function<String, String> getStringElseNull = key -> {
440-
final JsonNode jsValue = jsonObj.get(key);
441-
if (jsValue != null
442-
&& JsonNodeType.STRING.equals(jsValue.getNodeType())) {
443-
return jsValue.asString();
444-
} else {
445-
return null;
446-
}
447-
};
448-
ui.getInternals()
449-
.setExtendedClientDetails(new ExtendedClientDetails(ui,
450-
getStringElseNull.apply("v-sw"),
451-
getStringElseNull.apply("v-sh"),
452-
getStringElseNull.apply("v-ww"),
453-
getStringElseNull.apply("v-wh"),
454-
getStringElseNull.apply("v-bw"),
455-
getStringElseNull.apply("v-bh"),
456-
getStringElseNull.apply("v-tzo"),
457-
getStringElseNull.apply("v-rtzo"),
458-
getStringElseNull.apply("v-dstd"),
459-
getStringElseNull.apply("v-dston"),
460-
getStringElseNull.apply("v-tzid"),
461-
getStringElseNull.apply("v-curdate"),
462-
getStringElseNull.apply("v-td"),
463-
getStringElseNull.apply("v-pr"),
464-
getStringElseNull.apply("v-wn"),
465-
getStringElseNull.apply("v-np")));
478+
// Always update with fresh details from the response
479+
ExtendedClientDetails details = fromJson(ui, json);
480+
ui.getInternals().setExtendedClientDetails(details);
466481
}
467482
}

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

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,6 @@
2424
import java.util.Objects;
2525
import java.util.UUID;
2626
import java.util.function.Consumer;
27-
import java.util.function.Function;
28-
29-
import tools.jackson.databind.JsonNode;
30-
import tools.jackson.databind.node.JsonNodeType;
31-
import tools.jackson.databind.node.ObjectNode;
3227

3328
import com.vaadin.flow.component.Direction;
3429
import com.vaadin.flow.component.UI;
@@ -484,33 +479,38 @@ public interface ExtendedClientDetailsReceiver extends Serializable {
484479

485480
/**
486481
* Gets the extended client details, such as screen resolution and time zone
487-
* information. The details are automatically fetched on the first call to
488-
* this method and cached for the lifetime of the UI.
482+
* information.
489483
* <p>
490-
* Browser details are fetched asynchronously. On the first call, this
491-
* method triggers the fetch and returns {@code null}. Subsequent calls
492-
* return the cached details once the async fetch completes. To refresh the
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
493490
* cached values with updated data from the browser, use
494491
* {@link ExtendedClientDetails#refresh(Consumer)}.
495492
* <p>
496-
* If you need to perform an action when the details become available, use
497-
* {@link #retrieveExtendedClientDetails(ExtendedClientDetailsReceiver)} or
498-
* call {@code getExtendedClientDetails()} and if not null, use
499-
* {@code refresh(callback)} to be notified of updates.
493+
* If you need to perform an action when the details are updated, use
494+
* {@link ExtendedClientDetails#refresh(Consumer)} with a callback.
500495
*
501-
* @return the extended client details, or {@code null} if not yet fetched
496+
* @return the extended client details (never {@code null})
502497
*/
503498
public ExtendedClientDetails getExtendedClientDetails() {
504499
ExtendedClientDetails details = ui.getInternals()
505500
.getExtendedClientDetails();
506501
if (details == null) {
507-
// Trigger fetch on first call - create a placeholder instance
502+
// Trigger fetch on first call - create a placeholder instance with
503+
// default values
508504
ExtendedClientDetails placeholder = new ExtendedClientDetails(ui,
509505
null, null, null, null, null, null, null, null, null, null,
510506
null, null, null, null, null, null);
507+
// Store placeholder immediately so we don't return null
508+
ui.getInternals().setExtendedClientDetails(placeholder);
509+
// Trigger async fetch to populate with real values
511510
placeholder.refresh(null);
511+
return placeholder;
512512
}
513-
return ui.getInternals().getExtendedClientDetails();
513+
return details;
514514
}
515515

516516
/**
@@ -521,8 +521,8 @@ public ExtendedClientDetails getExtendedClientDetails() {
521521
* @param receiver
522522
* the callback to which the details are provided
523523
* @deprecated Use {@link #getExtendedClientDetails()} to get the cached
524-
* details, or {@link ExtendedClientDetails#refresh(Consumer)} to
525-
* refresh the cached values.
524+
* details, or {@link ExtendedClientDetails#refresh(Consumer)}
525+
* to refresh the cached values.
526526
*/
527527
@Deprecated
528528
public void retrieveExtendedClientDetails(

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-tests/test-root-context/src/main/java/com/vaadin/flow/InitialExtendedClientDetailsView.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ public class InitialExtendedClientDetailsView extends Div {
2828
public InitialExtendedClientDetailsView() {
2929
Page page = UI.getCurrent().getPage();
3030

31-
// Use the deprecated API to ensure details are available when callback is invoked
31+
// Use the deprecated API to ensure details are available when callback
32+
// is invoked
3233
page.retrieveExtendedClientDetails(details -> {
3334
if (details == null) {
3435
throw new IllegalStateException(

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,10 @@ protected void onShow() {
7575
add(setValuesButton, fetchDetailsButton);
7676
}
7777

78-
private void displayDetails(ExtendedClientDetails details,
79-
Div screenWidth, Div screenHeight, Div windowInnerWidth,
80-
Div windowInnerHeight, Div bodyElementWidth,
81-
Div bodyElementHeight, Div devicePixelRatio, Div touchDevice) {
78+
private void displayDetails(ExtendedClientDetails details, Div screenWidth,
79+
Div screenHeight, Div windowInnerWidth, Div windowInnerHeight,
80+
Div bodyElementWidth, Div bodyElementHeight, Div devicePixelRatio,
81+
Div touchDevice) {
8282
screenWidth.setText("" + details.getScreenWidth());
8383
screenHeight.setText("" + details.getScreenHeight());
8484
windowInnerWidth.setText("" + details.getWindowInnerWidth());

0 commit comments

Comments
 (0)