From ab22ae732511c46aeecf0efab84f7cb5b5d2b1b3 Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Tue, 7 Jul 2026 14:35:03 +0300 Subject: [PATCH] docs: document arm-don't-handle model for click-bound browser APIs Explain that Clipboard, Fullscreen, and Web Share bindings arm an action up front rather than registering a click listener, and that a binding left unarmed now fails loudly with an IllegalStateException (flow#24716) instead of silently doing nothing. Adds a canonical "How the Click Binding Works" section to the Clipboard article plus a Toggling Fullscreen pattern. Anti-patterns are described in prose only; all code samples show the correct form. --- articles/flow/browser-apis/clipboard-api.adoc | 25 ++++++++++++++++ .../flow/browser-apis/fullscreen-api.adoc | 29 +++++++++++++++++++ articles/flow/browser-apis/web-share-api.adoc | 3 ++ 3 files changed, 57 insertions(+) diff --git a/articles/flow/browser-apis/clipboard-api.adoc b/articles/flow/browser-apis/clipboard-api.adoc index cd516d395d..446377c318 100644 --- a/articles/flow/browser-apis/clipboard-api.adoc +++ b/articles/flow/browser-apis/clipboard-api.adoc @@ -16,6 +16,24 @@ The [classname]`Clipboard` API lets server-side Java code copy text, HTML, and i The Clipboard API requires a secure context (HTTPS), except on `localhost` during development. Browsers also require the [code]`write` call to happen inside a short-lived user gesture (click, key press, …) -- writes triggered outside of a gesture, for example from a background thread or a timer, will fail with a [code]`NotAllowedError`. +[#how-the-binding-works] +== How the Click Binding Works + +[methodname]`Clipboard.onClick(component)` does not register a click listener. It _arms_ a clipboard action that the browser runs synchronously inside the click event, while the user gesture is active -- a browser requirement for clipboard writes. The action must therefore be assigned during the same server request that creates the binding. + +Chain the action method directly onto [methodname]`onClick(...)`. The [methodname]`write*` and [methodname]`read*` methods return [code]`void`, so there is nothing to keep: + +[source,java] +---- +Button copy = new Button("Copy"); +Clipboard.onClick(copy).writeText("Hello, world!"); +---- + +Assigning the action later -- for example storing the [classname]`ClipboardBinding` and calling [methodname]`writeText()` from a separate [methodname]`addClickListener` callback -- arms the binding one request too late, after the gesture window has closed. A binding left with no action does nothing on the client, so the framework throws an [classname]`IllegalStateException` when the response is built. + +The same arming model applies to the <<{articles}/flow/browser-apis/fullscreen-api#,Fullscreen>> and <<{articles}/flow/browser-apis/web-share-api#,Web Share>> APIs, which share the user-gesture requirement. + + == Copying Text [methodname]`Clipboard.onClick(component)` returns a [classname]`ClipboardBinding` -- a fluent surface that attaches a clipboard action to the component's [code]`click` event. The typical "Copy to clipboard" button is one line: @@ -28,6 +46,13 @@ Clipboard.onClick(copy).writeText("Hello, world!"); Every subsequent click of the button copies the literal string to the clipboard. The call returns immediately and does not subscribe to the result -- see <<#observing-the-outcome,Observing the Outcome>> if you need to know whether the write succeeded. +[IMPORTANT] +==== +A literal string is captured *when the action is armed*, not when the click fires. Building it from a value that changes -- for example concatenating [methodname]`LocalTime.now()` into the text -- therefore copies a stale, build-time snapshot on every click, frozen at the moment the action was armed. + +To copy a value that changes, drive it through a component and pass the component (not a string) to [methodname]`writeText()` -- the value is then read on the client at click time, as shown next. Pre-arming has no equivalent for content that must be computed at the exact instant of the click; route such values through a field your server keeps up to date. +==== + == Copying a Field Value diff --git a/articles/flow/browser-apis/fullscreen-api.adoc b/articles/flow/browser-apis/fullscreen-api.adoc index d2453ae6a8..f22b8c93e9 100644 --- a/articles/flow/browser-apis/fullscreen-api.adoc +++ b/articles/flow/browser-apis/fullscreen-api.adoc @@ -26,6 +26,15 @@ Button fullscreenButton = new Button("Fullscreen"); Fullscreen.onClick(fullscreenButton).enter(); ---- +[IMPORTANT] +==== +[methodname]`onClick(...)` _arms_ the request; it does not register a click listener you fill in later. The request must run inside the browser's gesture window, so it has to be armed up front -- calling [methodname]`enter()` from an [methodname]`addClickListener` callback runs one round-trip too late and the browser rejects it. + +Forgetting [methodname]`enter()` altogether -- a binding created with no action -- fails loudly instead: the framework throws an [classname]`IllegalStateException` when the view renders, naming the unarmed binding. + +[methodname]`exit()` is different: it needs no gesture and so _is_ called from a normal handler. See <<{articles}/flow/browser-apis/clipboard-api#how-the-binding-works,How the Click Binding Works>> in the Clipboard article for the full explanation of this arm-don't-handle model. +==== + To fullscreen a single component instead of the whole page, pass it to [methodname]`enter(Component)`: [source,java] @@ -51,6 +60,26 @@ Button closeButton = new Button("Close", e -> Fullscreen.exit()); The call is a no-op when the page isn't currently fullscreen. The user can also press kbd:[Esc] -- the browser handles that itself, and the state signal updates accordingly. +== Toggling Fullscreen + +"Click to enter, click to exit" is a natural request, but a single button can't both _arm_ an enter request for the gesture window and decide at click time to exit instead -- the two needs pull in opposite directions. The canonical pattern keeps them separate: always arm [methodname]`enter()`, let the user leave with kbd:[Esc] or a control wired to [methodname]`Fullscreen.exit()`, and drive the visible affordance from [methodname]`stateSignal()` so the right control shows in each state: + +[source,java] +---- +Button enter = new Button("Fullscreen"); +Fullscreen.onClick(enter).enter(videoPanel); + +Button exit = new Button("Exit fullscreen", e -> Fullscreen.exit()); + +Signal isFullscreen = Fullscreen.stateSignal() + .map(state -> state == FullscreenState.FULLSCREEN); +enter.bindVisible(isFullscreen.map(on -> !on)); +exit.bindVisible(isFullscreen); +---- + +Because [methodname]`exit()` needs no gesture, the exit control is an ordinary click handler -- only the enter side has to be armed. Pressing kbd:[Esc] reaches the same state through [methodname]`stateSignal()`, so the buttons stay in sync however the user leaves fullscreen. + + == Observing Fullscreen State [methodname]`Fullscreen.stateSignal()` returns a read-only signal of the current fullscreen state. Use it to drive UI that depends on whether the page is fullscreen, or to hide controls in browsers that don't support fullscreen at all: diff --git a/articles/flow/browser-apis/web-share-api.adoc b/articles/flow/browser-apis/web-share-api.adoc index b878bc735f..b2cf90ccde 100644 --- a/articles/flow/browser-apis/web-share-api.adoc +++ b/articles/flow/browser-apis/web-share-api.adoc @@ -31,6 +31,9 @@ WebShare.onClick(shareButton).share( The trigger component must implement [interfacename]`ClickNotifier`, which covers buttons and most other clickable components. +[IMPORTANT] +[methodname]`onClick(...)` _arms_ the share action; it does not register a click listener you fill in later. The share sheet must open inside the browser's gesture window, so the action has to be armed up front -- calling [methodname]`share()` from an [methodname]`addClickListener` callback runs one round-trip too late and the browser rejects it. Forgetting [methodname]`share()` altogether -- a binding created with no action -- fails loudly instead: the framework throws an [classname]`IllegalStateException` when the view renders. See <<{articles}/flow/browser-apis/clipboard-api#how-the-binding-works,How the Click Binding Works>> in the Clipboard article for the full explanation of this arm-don't-handle model. + == Building Share Content