Skip to content

feat: let trigger actions be fired from client-rendered templates - #25366

Draft
totally-not-ai[bot] wants to merge 1 commit into
mainfrom
feat/client-actions-for-client-rendered-templates
Draft

feat: let trigger actions be fired from client-rendered templates#25366
totally-not-ai[bot] wants to merge 1 commit into
mainfrom
feat/client-actions-for-client-rendered-templates

Conversation

@totally-not-ai

Copy link
Copy Markdown
Contributor

What changed

A seam for trigger actions that are not bound to a server-side component, so
an affordance rendered on the client — a LitRenderer row 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, a
ClickTrigger, an addJsInitializer registration 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, 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* rejects.

New public API in com.vaadin.flow.component.trigger:

  • ClientAction — an unbound action handle. Produced by the feature facades
    and 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; it
    receives the rendered JsFunction and arranges for it to be invoked on the
    client.
  • ClientValue — a value read on the client when the action runs rather than
    captured 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 that
    produces a ClientAction, mirroring Clipboard.onClick(component).

Internal: SinkTrigger (a trigger that installs no listener of its own),
ContextInput (reads the trigger context), ClientActions (bridges the
public handle to the internal Action), and a Trigger(Element) constructor for
hosts 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:

grid.addColumn(LitRenderer.<Customer> of(
        "<span>${item.email}</span><button @click=${copy}>Copy</button>")
        .withProperty("email", Customer::email)
        .withClientAction("copy",
                Clipboard.write().text(ClientValue.itemProperty("email"))));

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 context
describes what the trigger fired for — {item, index, key} for a renderer,
undefined for the plain event triggers — and actions forward it to their
inputs, which is what lets one action resolve a per-row value.

Extra arguments are ignored on the client, so a handler that declares only
event keeps working and the convention is adoptable one action at a time; the
seven 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 click
trigger fails on the server with a clear message instead of evaluating to
undefined in the browser.

Testing

  • ClientActionTest — one binding per renderer, the value resolved from the
    item the action fired for, the registration detaching the binding, the outcome
    channel, and the context guard.
  • Existing trigger, clipboard, fullscreen and share tests updated to the new
    rendered-JS shape; flow-server: 5140 tests green.
  • The client contract was exercised in headless Chromium against real Lit with
    the action functions reified exactly as ClientJsonCodec does it: each row's
    button copied its own value, a root rebound to another item (grid recycling a
    row) copied the new one, navigator.userActivation.isActive was true at the
    navigator.clipboard.write call, the write resolved, and no server round trip
    fired.

Open for discussion — deliberately not settled here

This is a draft to review the shape, not a finished API.

  1. Row identity in callbacks. The observed overload reports the copied string
    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 design
    decision, not done here.
  2. No validation of the value source. ClientValue.itemProperty("emial")
    silently resolves to undefined and 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.
  3. Rollout across facades. Only Clipboard.write().text(…) exists. html,
    image, WebShare, Fullscreen and Download each need an unbound entry
    point, which roughly doubles their API surface — worth deciding whether one
    generic entry point beats one per facade before going wide.
  4. Naming. withClientAction next to withFunction reads ambiguously, and
    ClientValue.itemProperty is more verbose than the plain text("email") the
    original gap report sketched.
  5. ClientValue is also the natural home for a signal-backed value source
    (ClientValue.signal(…) over the existing SignalInput), which is the other
    half of the reported clipboard gap: a shared affordance whose value is chosen
    at click time.
  6. Integration test. A real Grid + LitRenderer + clipboard IT belongs in
    flow-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.

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.
@github-actions

Copy link
Copy Markdown
Contributor

Test Results

 1 388 files  +1   1 389 suites  +1   1h 32m 4s ⏱️ + 6m 44s
10 622 tests +4  10 555 ✅ +4  67 💤 ±0  0 ❌ ±0 
10 941 runs  +4  10 873 ✅ +4  68 💤 ±0  0 ❌ ±0 

Results for commit 86f0f12. ± Comparison against base commit b1f9674.

@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants