feat: let trigger actions be fired from client-rendered templates - #25366
Draft
totally-not-ai[bot] wants to merge 1 commit into
Draft
feat: let trigger actions be fired from client-rendered templates#25366totally-not-ai[bot] wants to merge 1 commit into
totally-not-ai[bot] wants to merge 1 commit into
Conversation
Trigger bindings so far name the component that fires them, which means one
server-side component, one trigger and one addJsInitializer registration per
affordance. A copy button in every row of a grid therefore costs a binding per
rendered row, rebuilt every time a row scrolls out of the buffer and back,
while a LitRenderer — the tool that exists to avoid per-row components —
cannot help: its withFunction callbacks round-trip to the server, and the
browser's transient activation is gone by the time the server responds, so
navigator.clipboard.write and friends reject.
Add a seam for actions that are not bound to a component:
- ClientAction, an unbound action handle produced by the feature facades
(Clipboard.write() to begin with), which whatever renders the affordance
binds once and fires per rendered element.
- ClientActionSink, implemented by the renderer that accepts the action; it
receives the rendered JsFunction and gets it invoked on the client.
- ClientValue, a value the action reads on the client when it runs rather than
one captured on the server when it is bound. ClientValue.itemProperty reads
it from the item the action fired for, which is what lets a single binding
serve a whole column.
- SinkTrigger, a trigger that installs no listener of its own and hands its
rendered action to the sink instead.
Rendered handlers now take (event, context) rather than (event): the context
describes what the trigger fired for — {item, index, key} for a renderer — and
actions forward it to their inputs. A handler that declares only event keeps
working, since the extra argument is simply ignored on the client, so the
convention is adoptable one action at a time.
An input that reads the context rejects a trigger that supplies none, so
binding an item-scoped value to a plain click trigger fails on the server
instead of evaluating to undefined in the browser.
Contributor
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



What changed
A seam for trigger actions that are not bound to a server-side component, so
an affordance rendered on the client — a
LitRendererrow template, for example— can fire one while the server holds a single binding.
Today every trigger binding names the component that fires it. A copy button in
every row of a grid therefore costs, per rendered row, a
Button, aClickTrigger, anaddJsInitializerregistration and a client-side listener,all torn down and rebuilt each time the row scrolls out of the buffer and back.
LitRenderer, the tool that exists precisely to avoid per-row components, cannothelp: its
withFunctioncallbacks round-trip to the server, and the browser'stransient activation is gone by the time the server responds, so
navigator.clipboard.write*rejects.New public API in
com.vaadin.flow.component.trigger:ClientAction— an unbound action handle. Produced by the feature facadesand handed to whatever renders the affordance, which binds it once via
bindTo(Element, ClientActionSink)and fires it per rendered element.ClientActionSink— implemented by whatever accepts client actions; itreceives the rendered
JsFunctionand arranges for it to be invoked on theclient.
ClientValue— a value read on the client when the action runs rather thancaptured on the server when it is bound.
ClientValue.itemProperty("email")reads it from the item the action fired for;
ClientValue.of(…)is a literal.Clipboard.write()/ClipboardWrite— the first facade entry point thatproduces a
ClientAction, mirroringClipboard.onClick(component).Internal:
SinkTrigger(a trigger that installs no listener of its own),ContextInput(reads the trigger context),ClientActions(bridges thepublic handle to the internal
Action), and aTrigger(Element)constructor forhosts that have no component of their own.
With the companion flow-components branch
(
LitRenderer.withClientAction, branch only, no PR yet) the grid use case reads:One binding for the whole column, no per-row component, no scroll churn.
Why the handler contract changed
Rendered handlers now take
(event, context)rather than(event). The contextdescribes what the trigger fired for —
{item, index, key}for a renderer,undefinedfor the plain event triggers — and actions forward it to theirinputs, which is what lets one action resolve a per-row value.
Extra arguments are ignored on the client, so a handler that declares only
eventkeeps working and the convention is adoptable one action at a time; theseven built-in actions were updated together for consistency, which is most of
the diff in the existing files.
An input that reads the context rejects a trigger that supplies none
(
Trigger.suppliesContext()), so binding an item-scoped value to a plain clicktrigger fails on the server with a clear message instead of evaluating to
undefinedin the browser.Testing
ClientActionTest— one binding per renderer, the value resolved from theitem the action fired for, the registration detaching the binding, the outcome
channel, and the context guard.
rendered-JS shape; flow-server: 5140 tests green.
the action functions reified exactly as
ClientJsonCodecdoes it: each row'sbutton copied its own value, a root rebound to another item (grid recycling a
row) copied the new one,
navigator.userActivation.isActivewastrueat thenavigator.clipboard.writecall, the write resolved, and no server round tripfired.
Open for discussion — deliberately not settled here
This is a draft to review the shape, not a finished API.
over a return channel on the renderer's node; it does not say which row. The
context knows (
key) — threading it into the outcome payload is a designdecision, not done here.
ClientValue.itemProperty("emial")silently resolves to
undefinedand the clipboard write still resolves.Validating action value sources against the renderer's declared properties
has to happen at render time, since properties may be declared after the
action.
Clipboard.write().text(…)exists.html,image,WebShare,FullscreenandDownloadeach need an unbound entrypoint, which roughly doubles their API surface — worth deciding whether one
generic entry point beats one per facade before going wide.
withClientActionnext towithFunctionreads ambiguously, andClientValue.itemPropertyis more verbose than the plaintext("email")theoriginal gap report sketched.
ClientValueis also the natural home for a signal-backed value source(
ClientValue.signal(…)over the existingSignalInput), which is the otherhalf of the reported clipboard gap: a shared affordance whose value is chosen
at click time.
LitRenderer+ clipboard IT belongs inflow-components alongside the companion branch.
Background: the gaps reported in vaadin/use-cases#323
(
clipboard/API-GAPS.md), from https://vaadin.com/forum/t/clipboard-copy/164697.