Skip to content

Add @js_action decorator for client-side open/close/toggle optimization#5843

Draft
evnchn wants to merge 6 commits intozauberzeug:mainfrom
evnchn:jsaction
Draft

Add @js_action decorator for client-side open/close/toggle optimization#5843
evnchn wants to merge 6 commits intozauberzeug:mainfrom
evnchn:jsaction

Conversation

@evnchn
Copy link
Copy Markdown
Collaborator

@evnchn evnchn commented Feb 28, 2026

Motivation

When using on_click=dialog.open or similar patterns, the action currently requires a full server round-trip before the UI updates. This introduces noticeable latency, especially on slower connections. By running a JavaScript equivalent on the client side in addition to the server-side handler, the UI responds instantly while server state stays in sync.

Implementation

Introduces a @js_action decorator (nicegui/js_action.py) that marks element methods (like open, close, toggle) as having a client-side JavaScript equivalent:

  • JsAction wrapper: When a decorated method is accessed on an element instance, it returns a JsAction object that carries both the Python callable and a JS handler string.
  • Element.on() integration: When a JsAction is passed as a handler, Element.on() automatically installs the JS handler for instant client-side feedback and suppresses the redundant server→client update (loopback) since the client already applied the change.
  • js_action.value(v) / js_action.toggle(): Convenience constructors for the common patterns of setting or toggling a model-value prop.
  • has_js_action(): Utility to detect JsAction handlers, used by elements with custom on_click wrappers (Button, Chip, Item, FabAction, DropdownButton) to pass the handler directly to on() instead of wrapping it in a lambda.
  • Backward compatible: Direct calls like dialog.open() still work identically. Subclasses that override decorated methods lose the JS action (by design), so custom logic always runs server-side.

Applied @js_action to: Dialog.open/close, Menu.open/close/toggle, DropdownButton.open/close/toggle, Fab.open/close/toggle.

Progress

  • I chose a meaningful title that completes the sentence: "If applied, this PR will..."
  • The implementation is complete.
  • If this PR addresses a security issue, it has been coordinated via the security advisory process.
  • Pytests have been added (or are not necessary).
  • Documentation has been added (or is not necessary).
  • Fix mypy/pylint
  • Clicking elsewhere in the ui.dialog to close it still causes server involvement
    • Afterwards, the js_action ones are broken.

@evnchn evnchn marked this pull request as draft February 28, 2026 07:02
@evnchn
Copy link
Copy Markdown
Collaborator Author

evnchn commented Feb 28, 2026

Human note: This is either 3.10 or 3.11, so this will take a while. Let's slow-cook this. Meanwhile, it is a sneak peak as to what could be done.

I would have went for evnchn/nicegui treatment, but I need to show this for the SEO context: This PR would supersede the hack in #5801

@evnchn evnchn added feature Type/scope: New or intentionally changed behavior 🌳 advanced Difficulty: Requires deep knowledge of the topic labels Feb 28, 2026
evnchn and others added 3 commits February 28, 2026 09:34
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The js_action JS handlers set element props and invalidate the vnode
cache, but Vue's reactivity doesn't trigger a re-render because the
cached render path short-circuits before reading element props, so the
dependency is never tracked. Adding $forceUpdate() ensures Vue picks up
the prop change after cache invalidation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Set LOOPBACK = False so backdrop/outside clicks update model-value
directly on the client without a server round-trip. Also fix the
client-side loopback handler: unwrap single-element args arrays and
call $forceUpdate() to ensure Vue re-renders after cache invalidation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add type: ignore for update_wrapper on _JsActionDescriptor
- Add type: ignore for JsAction callbacks passed to on() in fab/dropdown
- Replace ValueElement import with hasattr check to break cyclic import
  (nicegui.element -> nicegui.js_action -> nicegui.elements.mixins.value_element)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
evnchn and others added 2 commits February 28, 2026 10:26
Add type: ignore for JsAction callbacks passed to on() in button,
chip, and item elements. The JsAction handler type doesn't match
GenericEventArguments but works correctly at runtime since it
ignores event arguments entirely.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use proper typing instead of suppressing mypy errors:
- Add JsAction to Element.on() handler type since it already handles it at runtime
- Use TypeGuard for has_js_action() to enable mypy type narrowing
- Replace functools.update_wrapper with manual attribute copying to avoid arg-type mismatch
- Use explicit _JsActionDescriptor return types instead of TypeVar cast

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@evnchn
Copy link
Copy Markdown
Collaborator Author

evnchn commented Feb 28, 2026

image

Server does not need to intervene when opening-closing dialogs 🎉

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

Labels

🌳 advanced Difficulty: Requires deep knowledge of the topic feature Type/scope: New or intentionally changed behavior

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants