Skip to content

feat: trigger/action framework#24361

Merged
mshabarov merged 13 commits into
mainfrom
feature/trigger-step1
May 21, 2026
Merged

feat: trigger/action framework#24361
mshabarov merged 13 commits into
mainfrom
feature/trigger-step1

Conversation

@Artur-
Copy link
Copy Markdown
Member

@Artur- Artur- commented May 18, 2026

Adds a server-side API for wiring client-side triggers (DOM events) to
client-side actions, reading values from arguments, without a server
round-trip when the action doesn't ask for one.

The whole surface is kept internal at com.vaadin.flow.component.trigger.internal while the framework matures;
nothing here is part of the public API yet.

Java side (com.vaadin.flow.component.trigger.internal):

  • Sealed interfaces: Trigger, Action, Argument. Implementations must
    extend the corresponding abstract base.
  • Abstract bases: AbstractTrigger, AbstractAction, AbstractArgument,
    AbstractCallbackAction. Each carries a namespaced type id
    (flow:event, myapp:double-tap). Subclasses override
    buildClientConfig(ConfigContext) to ship JSON config; callback
    actions additionally declare a payload type and an
    applyServerSideEffect(T) hook the framework calls after Jackson
    deserialises the JSON the client reports back over the per-host
    return channel.
  • Built-ins: DomEventTrigger (generic DOM event by name),
    PropertyArgument (read a JS property at fire time),
    SetPropertyAction (assign a JS property when the trigger fires).
  • TriggerSupport extends ServerSideFeature: per-host store of
    triggers/actions/arguments + bindings. Emits a single JSON snapshot
    per host through Element.executeJs, coalescing mutations via
    StateTree.beforeClientResponse. Lazily registers a
    ReturnChannelRegistration for callback actions.
  • ConfigContext: lets subclasses encode argument/element references by
    stable id during buildClientConfig.

Client (flow-client/src/main/frontend/Triggers.ts):

  • Self-registers window.Vaadin.Flow.triggers with public
    registerTrigger / registerAction / registerArgument / bind / unbind
    so add-on @jsmodules can plug in their own factories.
  • bind is idempotent — second bind disposes the previous installation.
  • Built-in factories for flow:event, flow:property, flow:set-property.

Framework wiring: NodeFeatures.TRIGGER_SUPPORT = 28, slot in
NodeFeatureRegistry and BasicElementStateProvider, NodeFeatureTest
expectations updated.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 18, 2026

Test Results

 1 413 files  + 1   1 413 suites  +1   1h 20m 59s ⏱️ + 3m 24s
 9 982 tests +10   9 913 ✅ +10  69 💤 ±0  0 ❌ ±0 
10 457 runs  +10  10 386 ✅ +10  71 💤 ±0  0 ❌ ±0 

Results for commit 1d4565b. ± Comparison against base commit 50bcfc5.

♻️ This comment has been updated with latest results.

@mshabarov mshabarov self-requested a review May 18, 2026 08:30
@Artur- Artur- force-pushed the feature/trigger-step1 branch from 2f54e28 to 462630b Compare May 18, 2026 12:15
Copy link
Copy Markdown
Contributor

@mshabarov mshabarov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First round of review, looked at the top most interfaces/classes.

Comment thread flow-server/src/main/java/com/vaadin/flow/component/trigger/internal/Trigger.java Outdated
Comment thread flow-server/src/main/java/com/vaadin/flow/component/trigger/internal/Action.java Outdated
Comment thread flow-server/src/main/java/com/vaadin/flow/component/trigger/internal/Trigger.java Outdated
Comment thread flow-server/src/main/java/com/vaadin/flow/component/trigger/internal/Trigger.java Outdated
Comment thread flow-server/src/main/java/com/vaadin/flow/component/trigger/internal/Trigger.java Outdated
@Artur- Artur- force-pushed the feature/trigger-step1 branch 2 times, most recently from dbd02aa to abb0dae Compare May 20, 2026 07:33
mshabarov
mshabarov previously approved these changes May 20, 2026
Copy link
Copy Markdown
Contributor

@mshabarov mshabarov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me from the API shape perspective. Haven't yet checked carefully the implementation and test coverage, but giving a stamp now to not block other PRs, will double check later.

Artur- added 11 commits May 20, 2026 16:30
Adds a server-side API for wiring client-side triggers (DOM events) to
client-side actions, reading values from arguments, without a server
round-trip when the action doesn't ask for one.

Public package `com.vaadin.flow.component.trigger`:

- Interfaces: `Trigger`, `Action`, `Argument<T>`.
- Abstract bases: `AbstractTrigger`, `AbstractAction`, `AbstractArgument`.
  Each carries a namespaced type id (`flow:event`, `myapp:double-tap`).
  Subclasses override `buildClientConfig(ConfigContext)` to ship JSON
  config and `applyServerSideEffect(ArrayNode)` for any server-side
  reaction to client-reported outcomes (delivered through a per-host
  return channel).
- Built-ins: `DomEventTrigger` (generic DOM event by name),
  `PropertyArgument<T>` (read a JS property at fire time),
  `SetPropertyAction<T>` (assign a JS property when the trigger fires).

Internal package `com.vaadin.flow.component.trigger.internal`:

- `TriggerSupport extends ServerSideFeature`: per-host store of
  triggers/actions/arguments + bindings. Emits a single JSON snapshot
  per host through `Element.executeJs`, coalescing mutations via
  `StateTree.beforeClientResponse`. Lazily registers a
  `ReturnChannelRegistration` so actions that report back to the server
  (clipboard outcome, etc.) get there without a separate protocol.
- `ConfigContext`: lets subclasses encode argument/element references
  by stable id during `buildClientConfig`.

Client (`flow-client/src/main/frontend/Triggers.ts`):

- Self-registers `window.Vaadin.Flow.triggers` with public
  `registerTrigger / registerAction / registerArgument / bind / unbind`
  so add-on `@JsModule`s can plug in their own factories.
- `bind` is idempotent — second bind disposes the previous installation.
- Built-in factories for `flow:event`, `flow:property`,
  `flow:set-property`.

Framework wiring: `NodeFeatures.TRIGGER_SUPPORT = 28`, slot in
`NodeFeatureRegistry` and `BasicElementStateProvider`,
`NodeFeatureTest` expectations updated.
Seal Action/Argument interfaces, consolidate buildSnapshot loops, drop
unused test/helper methods, and remove obvious narration comments. No
behavior change; all trigger tests pass.
The trigger framework is not yet stable; keep the whole surface internal
so revisions don't break consumers.
Triggers can now expose their own output as Arguments that downstream
Actions consume on the same footing as element-derived Arguments. The
JS expression for a trigger-output Argument refers to a variable in the
trigger's handler scope (e.g. event["screenX"]); a scope guard refuses
to render an Argument from trigger A into trigger B's handler.

- HandlerExprArg: package-private Argument whose JS is a handler-scoped
  expression, carrying its owning trigger.
- JsBuilder: now carries the current trigger so handler-scoped Arguments
  can assert they're rendered into their own handler.
- DomEventTrigger.property(name): generic accessor returning
  Argument<T> for event[name].
- ClickTrigger: typed sugar over DomEventTrigger("click") with
  screenX/screenY/clientX/clientY/shiftKey/ctrlKey/altKey/metaKey.
- SetPropertyAction: new constructor accepting Argument<? extends T>;
  literal-value path delegates through LiteralArg so rendering is
  uniform.
…s only

Trigger, Action, and Argument added nothing once they were sealed against
their corresponding abstract base classes. Folding them into the bases
removes one layer of indirection, drops the runtime/sealed plumbing, and
lets AbstractTrigger.triggers(AbstractAction...) replace the previous
runtime cast.

- Delete Trigger.java, Action.java, Argument.java.
- AbstractTrigger now declares triggers(AbstractAction...) / remove()
  directly and implements Serializable.
- AbstractAction / AbstractArgument drop the `non-sealed` modifier and
  the implements clause; they only need to be Serializable.
- DomEventTrigger.property and ClickTrigger accessors return
  AbstractArgument<T>.
- SetPropertyAction's Argument constructor now takes
  AbstractArgument<? extends T>, removing the cast.
- TriggerTest updated to inspect the new JsFunction-based init wrapper
  (the user expression is now passed as a JsFunction parameter to a
  fixed framework wrapper).
The action handler is now built as a JsFunction with the element
references as captures and `event` as a runtime argument. The install
JS is reduced to framework boilerplate that references the handler at
$0 — no user-supplied content leaks into the install string.

- AbstractTrigger.triggers constructs JsFunction.of(handlerBody,
  captures).withArguments("event") and passes it as the single
  addJsInitializer parameter.
- AbstractTrigger.installJs() now takes no arguments. Subclasses just
  produce the install JS that references the handler at $0.
- DomEventTrigger.installJs returns the literal addEventListener +
  removeEventListener pair with $0 as the handler reference.
- JsBuilder.params() renamed to captures() to match what the value is
  now used for.
- Tests inspect the install JsFunction's body + the captured handler
  JsFunction; the install body for DomEventTrigger is now stable enough
  to assert verbatim.
With the interfaces gone, the abstract base classes own the names. Now
DomEventTrigger extends Trigger, SetPropertyAction extends Action, and
PropertyArgument extends Argument.
Addresses PR review:

- Argument is overloaded with "method argument" — renamed Argument<T> to
  Input<T>, PropertyArgument to PropertyInput, HandlerExprArg to
  HandlerInput, LiteralArg to LiteralInput. Reads as "an input that an
  Action consumes from a Trigger."
- Trigger javadoc now spells out that the synchronous-dispatch claim
  doesn't extend to the asynchronous browser APIs that motivate the
  framework — server-observable effects of an action (e.g. a callback
  for navigator.clipboard.writeText) may reach the server arbitrarily
  later than the gesture itself.
Making Input a nested type of Action encodes the consumer direction in
the name: a Trigger that hands out an Action.Input isn't claiming to
"produce an input", it's offering a value typed for what an Action
consumes. The Map.Entry-style nesting also keeps the abstract base
discoverable from Action without bloating the package surface.

- Action.java now hosts a nested `public abstract static class Input<T>`.
- Top-level Input.java deleted.
- Concrete subclasses extend Action.Input<T> (PropertyInput,
  LiteralInput, HandlerInput).
- DomEventTrigger.property, ClickTrigger accessors, SetPropertyAction
  constructor, and the test all reference Action.Input<T> explicitly.
- Updated Action's javadoc to explain the input/trigger relationship.
Reviewer wants varargs OR chaining, not both. Pick varargs — each call
wires one or more actions in one shot, and no return is needed.
Multiple wirings come from multiple triggers() calls on the same
trigger.
Artur- added 2 commits May 20, 2026 13:58
main reordered the addJsInitializer wrapper from [element, userFn,
initId] to [element, initId, userFn]. The test helper that inspects
the trigger's install JsFunction now reads it from index 2 instead of
index 1.
@sonarqubecloud
Copy link
Copy Markdown

@mshabarov mshabarov added this pull request to the merge queue May 21, 2026
Merged via the queue into main with commit 4a3e05b May 21, 2026
31 checks passed
@mshabarov mshabarov deleted the feature/trigger-step1 branch May 21, 2026 06:27
@github-project-automation github-project-automation Bot moved this from 🔎Iteration reviews to Done in Vaadin Flow | Hilla | Kits ongoing work May 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Development

Successfully merging this pull request may close these issues.

2 participants