shiny icon indicating copy to clipboard operation
shiny copied to clipboard

FR: add ability to distinguish programmatic updates to UI elements from user updates

Open ismirsehregal opened this issue 4 years ago • 7 comments

I just come across this idea here:

@hadley once asked for the ability to know whether a given input value was last updated by the user, or programmatically (i.e. updateXXXInput). Then your first two observeEvents could know to execute only on user actions.

This encouraged me to post a FR (I couldn't find an existing one regarding this), as I was working on similar use cases earlier where a feature like this would be really helpful: https://stackoverflow.com/questions/56331426/avoid-double-execution-of-computation-that-depends-on-updated-slider-value/56332245#56332245 Please see the comments: In the end it comes down to the question if it's posible to distinguish a programmatic update to the slider from a manual update. [...]

ismirsehregal avatar Jun 02 '20 08:06 ismirsehregal

I was having a similar issues to this. I wanted to stop triggering things on the server side until my programmatic updates had flushed through the system. My work around solution was to programmatically call the update function, then send a random value to the client. I would then detect on the server when the client reported that the updated value matched the server side one. You could use this same strategy.

RightChain avatar Oct 14 '21 19:10 RightChain

Thanks for bringing my attention to this StackOverflow post, @RightChain. I posted an answer with my take on how to solve this issue.

jcheng5 avatar Oct 15 '21 20:10 jcheng5

@jcheng5 there was a well presented, minimal question on StackOverflow today about a problem that I think would benefit from this proposed feature. It would be great to hear your thoughts on that.

mikmart avatar Mar 10 '22 16:03 mikmart

Here is something related. Include this JavaScript in your app:

    tags$script(
      HTML("$(document)
             .on('shiny:updateinput', function(e) {
                $(e.target).one('shiny:inputchanged', function(ev) {
                  ev.stopPropagation();
                });
              })
              .on('shiny:inputchanged', function(e) {
                Shiny.setInputValue(e.name + '_manualchange', true, {priority: 'event'});
              });")
    )

Then you can observe input$xxx_manualchange which is triggered when a manual change of input$xxx occurs.

stla avatar Apr 29 '22 20:04 stla

I'm trying to implement this. Here's a reprex where I have two sliders. See SO post here

The desired behaviour is for the value of input$slide2 to only change when the user interacts with slide2, but for the slider UI element to change when either slide1 or slide2 are interacted with.

library(shiny)

ui <- fluidPage(
    
    tags$head(
        tags$script(
            HTML("$(document)
                 .on('shiny:updateinput', function(e) {
                    $(e.target).one('shiny:inputchanged', function(ev) {
                      ev.stopPropagation();
                    });
                  })
                  .on('shiny:inputchanged', function(e) {
                    Shiny.setInputValue(e.name + '_manualchange', true, {priority: 'event'});
                  });")
        )
    ),

    sliderInput("slide1", "Slider", min = 0, max = 10, value = 5),
    sliderInput("slide2", "Slider2", min = 0, max = 10, value = 0),
    
    textOutput("slide2_val")
)

server <- function(input, output, session) {
    
    observe({
        updateSliderInput(session, "slide2", value = input$slide1)
    }) |> 
        bindEvent(input$slide1)
    
    output$slide2_val <- renderText({
        paste("Value of `slide2`:", input$slide2)
    }) |> 
        bindEvent(input$slide2_manualchange)
}

shinyApp(ui, server)

Unfortunately, the value of input$slide2 is still changing when slide1 is modified by the user.

whipson avatar May 03 '22 15:05 whipson

@whipson I got this working but in a way I don't recommend... I was just curious if it would work. I first got rid of updateSliderInput, instead updating slide2 in JavaScript; this ensures that the change to slide1 and slide2 happen "simultaneously" as far as the server is concerned (because input value changes that happen in the same JavaScript event loop tick are coalesced into a single reactive flush). Then, I created a helper function that determines if a given reactive expression invalidated during the current tick. Then, we can use that to filter out changes to slide2 that happen during the same tick that there were changes to slide1.

library(shiny)

# Given a reactive() or reactiveVal(), returns a function
# that returns TRUE if it changed during this reactive tick
changeTracker <- function(r) {
  changed <- NA
  
  observe({
    if (is.na(changed)) {
      changed <<- FALSE
    } else {
      changed <<- TRUE
    }
    try(silent = TRUE, r())
    
    getDefaultReactiveDomain()$onFlushed(function() {
      changed <<- FALSE
    }, once = TRUE)
  }, priority = 1000)
  
  return(function() {
    changed
  })
}

# Given a reactive or reactiveVal `r` and a filter function
# that takes no args and returns TRUE or FALSE, returns
# a reactive that relays changes from `r` but ignores any
# values that arrive while filter() returns FALSE.
filteredReactive <- function(r, filter) {
  lastKnownValue <- reactiveVal(isolate(r()))
  observeEvent(r(), {
    if (filter()) {
      lastKnownValue(r())
    }
  }, ignoreNULL = FALSE, ignoreInit = TRUE, priority = 500)
  
  return(lastKnownValue)
}

ui <- fluidPage(
  tags$script(HTML("$(document)
     .on('shiny:inputchanged', function(e) {
       if (e.name === 'slide1') {
         $('#slide2').data('shinyInputBinding').setValue($('#slide2')[0], e.value);
       }
     });"
  )),
  
  sliderInput("slide1", "Slider", min = 0, max = 10, value = 5),
  sliderInput("slide2", "Slider2", min = 0, max = 10, value = 0),
  
  textOutput("slide2_val")
)

server <- function(input, output, session) {
  # Tells us whether slide1 changed during this tick.
  slide1_changed <- changeTracker(reactive(input$slide1))
  # Returns the same as input$slide2 except ignores changes
  # that occur when slide1_changed() is TRUE.
  slide_2_manual_only <- filteredReactive(
    reactive(input$slide2),
    Negate(slide1_changed)
  )

  observe({
    updateSliderInput(session, "slide2", value = input$slide1)
  }) |> 
    bindEvent(input$slide1)
  
  output$slide2_val <- renderText({
    
    paste("Value of `slide2`:", slide_2_manual_only())
  }) |> 
    bindEvent(slide_2_manual_only())
}

shinyApp(ui, server)

I mostly don't advise this because of that $('#slide2').data('shinyInputBinding').setValue($('#slide2')[0], e.value); line which is pretty dubious; that data('shinyInputBinding') in particular is not documented. It would be nice though to have a supported way in shiny.js to invoke setValue on input controls, not just send input values to the server with no regard to the UI.

jcheng5 avatar May 05 '22 16:05 jcheng5

Here is how you can do the same thing using only server side values. This can get a bit messy when you are dealing with multiple interconnected/interdependent reactive chains and many inputs. The idea is once you have sent an update to the client, you keep track of the new value on the server side. When the value returns from the client side, you perform a comparison against the server side value to see if they match. If they match then it means it was an automatic update. Otherwise it was a manual update and your code can run. You then have to keep track of any new values as it updates to prevent some odd behavior.


ui <- fluidPage(
  
  sliderInput("slide1", "Slider", min = 0, max = 10, value = 5),
  sliderInput("slide2", "Slider2", min = 0, max = 10, value = 0),
  
  textOutput("slide2_val")
)

server <- function(input, output, session) {
  
  old_val <- NULL

  observe({
    updateSliderInput(session, "slide2", value = input$slide1)
    old_val <<- input$slide1
  })
  
  output$slide2_val <- renderText({
    req(old_val != input$slide2, cancelOutput=T)
    old_val <<- input$slide2
    paste("Value of `slide2`:", input$slide2)
  })
}

shinyApp(ui, server)

RightChain avatar May 05 '22 16:05 RightChain

What is the purpose of this input$ feedback on updates triggered from the server? Is it because input changes could come from client-side logic? Is there a way to opt-out?

I tried most of the workarounds mentionned about but none of them seemed generic enough for my use case. I resorted to a combination of https://github.com/rstudio/shiny/issues/2914#issuecomment-1118828991 and some server state diffing code that skips updateXInput calls when nothing has changed.

PS: Perhaps it was designed so as to save us from typing twice the number of lines?

inputX <- reactiveVal(NULL)
observe(inputX(input$x)) # Update from UI
...
inputX(newValue) # Update from server
updateInput(session, "x", value = newValue)

king-of-poppk avatar Aug 07 '23 08:08 king-of-poppk