Skip to content

fix: fire shiny:outputinvalidated for outputs bound after invalidation#4373

Open
cpsievert wants to merge 4 commits intomainfrom
fix/outputinvalidated-modal-event
Open

fix: fire shiny:outputinvalidated for outputs bound after invalidation#4373
cpsievert wants to merge 4 commits intomainfrom
fix/outputinvalidated-modal-event

Conversation

@cpsievert
Copy link
Copy Markdown
Collaborator

@cpsievert cpsievert commented Apr 28, 2026

Closes #2797

Summary

  • When an output is invalidated before it's in the DOM (e.g., inside a closed modal), the shiny:outputinvalidated jQuery event was never fired. PR Improve/fix output progress reporting #4039 introduced a state machine that correctly applies showProgress(true) when such outputs are later bound, but the corresponding event dispatch was missing. This adds it.
  • Adds isInvalidated() to OutputProgressReporter and uses it in bindOutputs() to fire the event only when an output in the Invalidated state is first bound. This is intentionally a separate check from isRecalculating() (which drives showProgress), because isRecalculating() includes the Initial state — firing shiny:outputinvalidated for every output on first page load would be incorrect.

Context

Issue #2797 reported three problems with outputs inside modals:

  1. shiny:outputinvalidated never fires
  2. shiny:recalculating / shiny:recalculated fire at the wrong time
  3. .recalculating CSS class is never added

Problems 2 and 3 were fixed by #4039. This PR fixes problem 1, completing the fix for #2797.

Manual testing app
library(shiny)

ui <- basicPage(
  tags$script(HTML("
    $(document).on('shiny:outputinvalidated', function(event) {
      var el = document.getElementById('log');
      el.textContent += new Date().toLocaleTimeString() +
        ' outputinvalidated: ' + event.target.id + '\\n';
    });
  ")),
  textOutput("mainOutput"),
  actionButton("open", "Open modal"),
  actionButton("increment", "Increment"),
  tags$h4("Event log:"),
  tags$pre(id = "log", style = "background: #f0f0f0; padding: 10px; min-height: 200px;")
)

server <- function(input, output, session) {
  x <- reactiveVal(0)

  observeEvent(input$open, {
    showModal(modalDialog(
      textOutput("insideModal"),
      title = "Modal output"
    ))
  })

  observeEvent(input$increment, {
    x(x() + 1)
  })

  output$mainOutput <- renderText({
    paste("Main page value:", x())
  })

  output$insideModal <- renderText({
    Sys.sleep(2)
    paste("Modal value:", x())
  })
}

shinyApp(ui, server)

Steps:

  1. Open the modal — wait for "Modal value: 0" to render, then dismiss it
  2. Click "Increment" — this changes x() while the modal is closed
  3. Open the modal again

Before fix: The event log shows outputinvalidated: mainOutput (step 2) but never outputinvalidated: insideModal.

After fix: The event log shows outputinvalidated: insideModal when the modal reopens (step 3).

Note: use pkgload::load_all(".") to test the development version rather than the installed package.

Test plan

  • Unit tests for OutputProgressReporter state machine (33 tests covering isRecalculating, isInvalidated, state transitions, takeChanges, and invalid transitions)
  • Verified with the manual testing app above (FAIL on main, PASS on branch)
  • Verify that shiny:outputinvalidated does NOT fire for outputs on initial page load (only showProgress / .recalculating class should be applied in that case)

@cpsievert cpsievert marked this pull request as draft April 28, 2026 16:51
@cpsievert cpsievert requested a review from Copilot April 28, 2026 23:37

This comment was marked as outdated.

@cpsievert cpsievert force-pushed the fix/outputinvalidated-modal-event branch from 70090ef to a1c4bb9 Compare April 29, 2026 00:05
@cpsievert cpsievert requested a review from Copilot April 29, 2026 00:09
@cpsievert cpsievert force-pushed the fix/outputinvalidated-modal-event branch from a1c4bb9 to 75a6fe1 Compare April 29, 2026 00:10

This comment was marked as outdated.

This comment was marked as resolved.

#2797)

When an output is invalidated before it's in the DOM (e.g., inside a
closed modal), the shiny:outputinvalidated event was never fired. The
progress state machine already tracks this — the Invalidated state means
"this output was invalidated while not bound."

Adds isInvalidated() to OutputProgressReporter and uses it in
bindOutputs() to fire the event when an output in the Invalidated state
is first bound. This is intentionally separate from the isRecalculating()
check that drives showProgress(), since isRecalculating() also includes
the Initial state (every output on first page load) where firing the
invalidated event would be incorrect.
Matches the event payload shape from progressHandlers.binding in
shinyapp.ts, where event.binding is an OutputBindingAdapter.
…ordering

Matches the ordering in progressHandlers.binding (shinyapp.ts), where
the event fires before _updateProgress() applies showProgress.
Covers isRecalculating, isInvalidated, takeChanges, full lifecycle
transitions (value, error, cancel, persistent paths), and invalid
transition errors. 33 tests total.
@cpsievert cpsievert force-pushed the fix/outputinvalidated-modal-event branch from 52d91ad to f626f2c Compare April 29, 2026 14:29
@cpsievert cpsievert marked this pull request as ready for review April 29, 2026 16:07
@cpsievert cpsievert requested a review from Copilot April 29, 2026 16:07
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes missing dispatch of the shiny:outputinvalidated event for outputs that were invalidated while not yet present in the DOM (e.g., inside a closed modal), complementing the progress state machine introduced in #4039 and closing #2797.

Changes:

  • Add OutputProgressReporter.isInvalidated() and expose it through the bind context.
  • Fire shiny:outputinvalidated during output binding when the output is already in the Invalidated state.
  • Add unit tests covering OutputProgressReporter state transitions and new isInvalidated() behavior.

Reviewed changes

Copilot reviewed 5 out of 10 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
srcts/src/shiny/outputProgress.ts Adds isInvalidated() to the output progress state machine.
srcts/src/shiny/bind.ts Uses outputIsInvalidated() during output binding to trigger shiny:outputinvalidated.
srcts/src/shiny/index.ts Threads outputIsInvalidated() into the bind context.
srcts/src/shiny/tests/outputProgress.test.ts Adds state machine unit tests, including isInvalidated().
srcts/types/src/shiny/outputProgress.d.ts Updates typings for the new isInvalidated() API.
srcts/types/src/shiny/bind.d.ts Updates typings for the new bind context callback.
inst/www/shared/shiny.js Built output reflecting the TS changes.
inst/www/shared/shiny.min.js Minified built output reflecting the TS changes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +313 to +317
void test("recalculating from Value throws", () => {
const r = new OutputProgressReporter();
runFullCycle(r, "x", "value");
assert.throws(() => r.updateStateFromMessage(recalculated("x")));
});
Comment thread srcts/src/shiny/bind.ts
Comment on lines +359 to +366
if (outputIsInvalidated(id)) {
$el.trigger({
type: "shiny:outputinvalidated",
// @ts-expect-error; Can not remove info on a established, malformed Event object
binding: bindingAdapter,
name: id,
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Shiny events for outputs inside modalDialogs not working correctly

2 participants