diff --git a/articles/flow/advanced/browser-access.adoc b/articles/flow/advanced/browser-access.adoc index 2fa09e6437..94024ab6de 100644 --- a/articles/flow/advanced/browser-access.adoc +++ b/articles/flow/advanced/browser-access.adoc @@ -104,8 +104,45 @@ For that reason, the [methodname]`isIOS()` and [methodname]`isIPad()` methods ar == Executing JavaScript in the Browser You can use server-side Java to execute JavaScript snippets in the browser. -You can also pass parameters to the executed script as variables named `$0`, `$1`, and so on. +Declare the JavaScript in a Java interface and run it through [methodname]`Page.executeJs(Class)`: the build collects the declaration into the client bundle, so nothing is compiled from a string in the browser and the call works under a content security policy that doesn't allow `unsafe-eval`. + +For JavaScript that works on a particular element rather than on the page as a whole, use the Element API instead (see <>). + + +[role="since:com.vaadin:vaadin@V25.4"] +=== Declaring JavaScript in Java + +[methodname]`Page.executeJs(Class)` runs the JavaScript that an interface annotated with [annotationname]`@JsDefinition` declares, instead of a string expression. It returns an implementation of the interface, and calling a method of that implementation queues the JavaScript the method declares with [annotationname]`@JsExpression`, with the arguments of the call available as `$0`, `$1`, and so on. What such an interface may contain is described in <>. + +The JavaScript runs on nothing in particular, since page-level JavaScript works with global browser APIs, where the JavaScript of [methodname]`Element.executeJs(Class)` runs on the element it was obtained from. A method that declares a return value can use the returned [classname]`PendingJavaScriptResult` the same way [methodname]`executeJs(String, Object...)` does. + +.Opening the browser's print dialog +[example] +==== +[source,java] +---- +@JsDefinition +public interface PrintJs extends Serializable { + + @JsExpression("window.print()") + void print(); +} +---- + +[source,java] +---- +printButton.addClickListener(event -> UI.getCurrentOrThrow() + .getPage().executeJs(PrintJs.class).print()); +---- +==== + + +=== Running a JavaScript Expression + +[methodname]`Page.executeJs(String, Object...)` takes the JavaScript as a string. +You can pass parameters to the executed script as variables named `$0`, `$1`, and so on. Vaadin automatically serializes and escapes the parameter values. +The browser compiles the expression, which a content security policy that doesn't allow `unsafe-eval` blocks, so prefer a declaration for JavaScript that's known in advance. You can execute JavaScript in the browser and pass parameters as follows: @@ -130,6 +167,7 @@ The parameter value is `null` for a parameter of type [classname]`Element` that The script is executed asynchronously, so you can't directly pass values back to the server. Instead, you can use the returned [classname]`PendingJavaScriptResult` instance to add a callback that's called when the result is available. + == Scrolling a Component into View You can scroll any component into the visible area of the browser window using [methodname]`scrollIntoView()`. This calls the browser's native `scrollIntoView()` on the component's element. diff --git a/articles/flow/component-internals/element-api/calling-javascript.adoc b/articles/flow/component-internals/element-api/calling-javascript.adoc index c16c9941a9..4bc9cd79ff 100644 --- a/articles/flow/component-internals/element-api/calling-javascript.adoc +++ b/articles/flow/component-internals/element-api/calling-javascript.adoc @@ -12,101 +12,100 @@ order: 3 The Element API contains methods for executing JavaScript in the browser from the server side. +Declare the JavaScript in a Java interface and call it through [methodname]`Element.executeJs(Class)`. The build collects the declared JavaScript into the frontend bundle, and the client runs it from there. Nothing is compiled from a string in the browser, so the call works under a content security policy that doesn't allow `unsafe-eval`. -== `callJsFunction` Method +The other methods described on this page -- [methodname]`executeJs(String, Object...)`, [methodname]`callJsFunction()`, [classname]`JsFunction`, and [methodname]`addJsInitializer()` -- send the JavaScript to the browser as a string that's compiled there. Avoid them for anything a declaration can express. -The [methodname]`Element.callJsFunction()` method allows you to run a client-side component function from the server side. The method accepts two parameters: the name of the function to call; and the arguments to pass to the function. -The arguments passed to the function must be a type supported by the communication mechanism. The supported types are `String`, `Boolean`, `Integer`, `Double`, `JsonNode`, `Element`, `Component`, and `JsFunction` (see <<#js-function,Passing JavaScript Functions>>). +[[declaring-javascript]] +[role="since:com.vaadin:vaadin@V25.4"] +== Declaring JavaScript in Java -.Calling the `clearSelection()` JavaScript function on the root element from the server side -[example] -==== -[source,java] ----- -public void clearSelection() { - getElement().callJsFunction("clearSelection"); -} ----- -==== +An interface annotated with [annotationname]`@JsDefinition` declares the JavaScript that its methods run, and the build collects that JavaScript into the bundle. Call the declared JavaScript through [methodname]`Element.executeJs(Class)`, which hands out an implementation of the interface; calling a method of it schedules the JavaScript that the method declares. -.Calling the `expand(otherComponentElement)` JavaScript function on the root element from the server side +Each method of the interface is annotated with [annotationname]`@JsExpression`, whose value is the JavaScript expression to run. The arguments of the call are available inside the expression as `$0`, `$1`, and so on, and the element the implementation was obtained from is `this`. + +The arguments must be a type supported by the communication mechanism. The supported types are `String`, `Boolean`, `Integer`, `Double`, `JsonNode`, `Element`, `Component`, and `JsFunction`. + +Passing a [classname]`JsFunction` is allowed, but it brings the browser-side compilation back, since the client builds such a function from its body string. Declare a method for the JavaScript instead of passing a function into it. + +.Declaring and calling JavaScript through a `@JsDefinition` interface [example] ==== [source,java] ---- -public void setExpanded(Component otherComponent) { - getElement().callJsFunction("expand", - otherComponent.getElement()); +@JsDefinition +public interface GreeterJs extends Serializable { + + @JsExpression("window.alert($0)") + void showGreeting(String greeting); } ---- -==== -.Passing a JSON object to a JavaScript function -[example] -==== [source,java] ---- -public void configure(String label, int count) { - ObjectNode config = JacksonUtils.createObjectNode(); - config.put("label", label); - config.put("count", count); - config.put("enabled", true); - getElement().callJsFunction("configure", config); +public void greet(String message) { + getElement().executeJs(GreeterJs.class).showGreeting(message); } ---- ==== +A method returns either `void` or [classname]`PendingJavaScriptResult`, to retrieve a return value the same way an expression does (see <<#return-values,Return Values>>). -== `executeJs` Method - -You can also use the generic [methodname]`Element.executeJs()` method to run JavaScript asynchronously from the server side. This method can be used in addition to the [methodname]`Element.callJsFunction()` method when calling any JavaScript. - -The [methodname]`executeJs()` method accepts two parameters: the JavaScript expression to invoke; and the parameters to pass to the expression. The given parameters are available as variables named `$0`, `$1`, and so on. - -The arguments passed to the expression must be a type supported by the communication mechanism. The supported types are `String`, `Integer`, `Double`, `Boolean`, `JsonNode`, `Element`, `Component`, and `JsFunction` (see <<#js-function,Passing JavaScript Functions>>). - -.Calling `MyModule.complete(true)` on the client side +.Declaring JavaScript that returns a value [example] ==== [source,java] ---- -public void complete() { - getElement().executeJs("MyModule.complete($0)", true); +@JsDefinition +public interface OverflowJs extends Serializable { + + @JsExpression("return this.scrollWidth > this.clientWidth") + PendingJavaScriptResult isContentClipped(); } ---- -==== -.Passing a JSON array to a JavaScript expression -[example] -==== [source,java] ---- -public void setItems(List items) { - getElement().executeJs("this.items = $0", items); +public void updateTooltip() { + getElement().executeJs(OverflowJs.class) + .isContentClipped() + .then(Boolean.class, this::setTooltipEnabled); } ---- ==== -.Avoid Script Injection Vulnerabilities -[WARNING] -Always pass arguments using the `$0`, `$1`, ... notation to avoid script injection vulnerabilities. Never concatenate or interpolate strings to build JavaScript code to be executed. +Nothing about the JavaScript is decided at the call site: the build generates one function per declared expression into the bundle, and the client runs that function after looking it up by an identifier of the JavaScript. The expression itself is never sent to the browser, and a production bundle carries the generated functions only -- the Java names stay on the server. -If you need to run JavaScript without having access to an element, use the [methodname]`UI.getCurrentOrThrow().getPage().executeJs()` method. +[NOTE] +The interface is checked when [methodname]`executeJs(Class)` hands out the implementation. It must be annotated with [annotationname]`@JsDefinition`, and every method must be annotated with [annotationname]`@JsExpression` and return `void` or [classname]`PendingJavaScriptResult`. A `default` or `static` method is refused, since it's implemented in Java rather than declaring JavaScript to run in the browser. + +When the JavaScript isn't about a particular element, declare it the same way and run it through [methodname]`Page.executeJs(Class)`, which works with global browser APIs instead of with an element (see <>). +[[return-values]] == Return Values -The return value from the JavaScript function called using [methodname]`callJsFunction()`, or the value from a `return` statement in an `executeJs()` expression can be accessed by adding a listener to the [classname]`PendingJavaScriptResult` instance returned from either method. +Add a listener to the [classname]`PendingJavaScriptResult` instance that a call answers with to access the value from a `return` statement in the JavaScript. This works the same way for a declared expression, an <<#execute-js,[methodname]`executeJs()` expression>>, and a function called through <<#call-js-function,[methodname]`callJsFunction()`>>. A declared method has to declare [classname]`PendingJavaScriptResult` as its return type to give access to the result. .Checking for support of Constructable Stylesheets in the browser [example] ==== +[source,java] +---- +@JsDefinition +public interface FeatureDetectionJs extends Serializable { + + @JsExpression("return 'adoptedStyleSheets' in document") + PendingJavaScriptResult supportsConstructableStylesheets(); +} +---- + [source,java] ---- public void checkConstructableStylesheets() { - getElement().executeJs( - "return 'adoptedStyleSheets' in document") + getElement().executeJs(FeatureDetectionJs.class) + .supportsConstructableStylesheets() .then(Boolean.class, supported -> { if (supported) { System.out.println( @@ -133,7 +132,20 @@ The [methodname]`then()` method accepts a [classname]`Class` parameter for simpl ==== [source,java] ---- -getElement().executeJs("return this.getItems()") +@JsDefinition +public interface ItemsJs extends Serializable { + + @JsExpression("return this.getItems()") + PendingJavaScriptResult getItems(); + + @JsExpression("return this.getPersonMap()") + PendingJavaScriptResult getPersonMap(); +} +---- + +[source,java] +---- +getElement().executeJs(ItemsJs.class).getItems() .then(new TypeReference>() {}, items -> { // items is List @@ -147,7 +159,7 @@ An error handler can be provided as a second callback. The handler receives the [source,java] ---- -getElement().executeJs("return this.getItems()") +getElement().executeJs(ItemsJs.class).getItems() .then(new TypeReference>() {}, items -> processItems(items), errorMessage -> handleError(errorMessage)); @@ -158,18 +170,106 @@ You can also use [methodname]`toCompletableFuture(TypeReference)` to get the res [source,java] ---- CompletableFuture> future = getElement() - .executeJs("return this.getPersonMap()") + .executeJs(ItemsJs.class).getPersonMap() .toCompletableFuture( new TypeReference>() {}); ---- +[[execute-js]] +== `executeJs` Method + +The [methodname]`Element.executeJs()` method runs a JavaScript expression given as a string. The browser compiles the expression, which a content security policy that doesn't allow `unsafe-eval` blocks. Declare the JavaScript in Java instead (see <<#declaring-javascript,Declaring JavaScript in Java>>). Use this method only for JavaScript that can't be declared -- an expression that isn't known until runtime, for example. + +The [methodname]`executeJs()` method accepts two parameters: the JavaScript expression to invoke; and the parameters to pass to the expression. The given parameters are available as variables named `$0`, `$1`, and so on. + +The arguments passed to the expression must be a type supported by the communication mechanism. The supported types are `String`, `Integer`, `Double`, `Boolean`, `JsonNode`, `Element`, `Component`, and `JsFunction` (see <<#js-function,Passing JavaScript Functions>>). + +.Calling `MyModule.complete(true)` on the client side +[example] +==== +[source,java] +---- +public void complete() { + getElement().executeJs("MyModule.complete($0)", true); +} +---- +==== + +.Passing a JSON array to a JavaScript expression +[example] +==== +[source,java] +---- +public void setItems(List items) { + getElement().executeJs("this.items = $0", items); +} +---- +==== + +.Avoid Script Injection Vulnerabilities +[WARNING] +Always pass arguments using the `$0`, `$1`, ... notation to avoid script injection vulnerabilities. Never concatenate or interpolate strings to build JavaScript code to be executed. + +If you need to run JavaScript without having access to an element, use the [methodname]`UI.getCurrentOrThrow().getPage().executeJs()` method. + + +[[call-js-function]] +== `callJsFunction` Method + +The [methodname]`Element.callJsFunction()` method allows you to run a client-side component function from the server side. The method accepts two parameters: the name of the function to call; and the arguments to pass to the function. + +The call is sent to the browser as an expression and compiled there, the same way [methodname]`executeJs()` is, so it also needs a content security policy that allows `unsafe-eval`. A definition method that declares an expression such as `return this.clearSelection()` calls the same client-side function without one (see <<#declaring-javascript,Declaring JavaScript in Java>>). + +The arguments passed to the function must be a type supported by the communication mechanism. The supported types are `String`, `Boolean`, `Integer`, `Double`, `JsonNode`, `Element`, `Component`, and `JsFunction` (see <<#js-function,Passing JavaScript Functions>>). + +.Calling the `clearSelection()` JavaScript function on the root element from the server side +[example] +==== +[source,java] +---- +public void clearSelection() { + getElement().callJsFunction("clearSelection"); +} +---- +==== + +.Calling the `expand(otherComponentElement)` JavaScript function on the root element from the server side +[example] +==== +[source,java] +---- +public void setExpanded(Component otherComponent) { + getElement().callJsFunction("expand", + otherComponent.getElement()); +} +---- +==== + +.Passing a JSON object to a JavaScript function +[example] +==== +[source,java] +---- +public void configure(String label, int count) { + ObjectNode config = JacksonUtils.createObjectNode(); + config.put("label", label); + config.put("count", count); + config.put("enabled", true); + getElement().callJsFunction("configure", config); +} +---- +==== + + [[js-function]] [role="since:com.vaadin:vaadin@V25.2"] == Passing JavaScript Functions A [classname]`JsFunction` lets you build a reusable JavaScript function on the server and pass it as a parameter to [methodname]`executeJs()` or [methodname]`callJsFunction()`. The function arrives on the client as a real callable function with its captured values pre-bound, so you don't need to concatenate JavaScript fragments to embed server-side values. +The function body is compiled on the client, so it needs a content security policy that allows `unsafe-eval` -- also when the function is passed to a method of a JavaScript definition. Where the body is known in advance, declare the JavaScript in Java instead (see <<#declaring-javascript,Declaring JavaScript in Java>>). + The first argument to [methodname]`JsFunction.of()` is a JavaScript function body. The remaining arguments are captured values, referenced inside the body as `$0`, `$1`, … using the same naming convention as [methodname]`executeJs()` parameters. .Defining a function and invoking it @@ -250,6 +350,8 @@ The [methodname]`Element.addJsInitializer()` method registers a JavaScript expre Use this when you need to install something on the client-side DOM – an event listener, a third-party widget, an observer – and reliably tear it down. A one-shot [methodname]`executeJs()` call doesn't cover two cases: a real re-attach gives the element a brand-new DOM node that no longer has your listener, and cleanup from a server-side detach listener cannot be delivered because the element is leaving the tree. +The expression is compiled in the browser, so [methodname]`addJsInitializer()` needs a content security policy that allows `unsafe-eval`. No declaration-based counterpart exists; under a stricter policy, install and tear down from a client-side module of your own instead. + The expression syntax is the same as [methodname]`executeJs()`: `this` is the host element on the client, and parameters are referenced as `$0`, `$1`, …. If the expression returns a function, that function is invoked at teardown. .Installing a listener with cleanup @@ -272,7 +374,7 @@ Remove the registration on the server when the listener is no longer needed; the === Re-Attach Semantics -The initializer is re-run after a real re-attach – when the element is removed from the DOM in one round trip and re-added in a later one, and so the browser receives a fresh DOM node. It is *not* re-run when the element is detached and re-attached on the server inside a single round trip, because the client never discarded its DOM in that case. +The initializer is re-run after a real re-attach – when the element is removed from the DOM in one round trip and re-added in a later one, and so the browser receives a fresh DOM node. It is *not* re-run when the element is detached and re-attached on the server inside a single round trip, because the client never discarded its DOM. === Cleanup Constraints