-
Notifications
You must be signed in to change notification settings - Fork 1.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add call back when renderUI
is done
#3348
Comments
Regarding 1: I'm not associated with the shiny team so this isn't official, but from my understanding it's not possible to have shiny <--> javascript code be synchronous. It would be great if that was technically possible, I myself also would have many uses for that. For eample I have packages that communicate with javascript, but it's impossible to ask javascript for a value and retrieve it in the next line as a blocking request, the only solution AFAIK is to tell javascript to calculate a value and it will send it back to shiny as an input that I need to listen to, which is essentially a callback. |
Thanks Dean, I understand this part. You see in my second point, I am not asking the js to be synchronous, but telling me all the events that will happen and I can write my own code to watch and wait for these events. There are already callbacks like "shiny:value" or "shiny:bound". I would like to know how many "shiny:bound" will be triggered and their IDs by a "shiny:value" event, more specifically, in this case, triggered by |
You can add output$ui_out <- renderUI({
lapply(1:1000, function(i) {
freezeReactiveValue(input, paste0("n", i))
numericInput(paste0("n", i), paste0("n", i), value = 0)
})
}) This will cause any attempt to read |
Thanks for the suggestion @jcheng5 . Imagine we are designing an app loads UI and server from users as modules dynamically (by clicking the button), so I have no control what code is in the UI or server module that the user provides. I can't predict what I know there are many ways to prevent the error. That's just an example to illustrate that we have the need to wait for the UI to finish loading. Please do not struggle on how to fix the error above. The point is adding a callback when Here is how far I go: when |
Oh, I understand what you're doing now. OK, that's a good question. This is the first thing that came to mind, it's not easy but it does work--as long as the UI that's being loaded actually contains an input. Basically this code schedules execute_at_next_input <- function(expr, session = getDefaultReactiveDomain()) {
observeEvent(once = TRUE, reactiveValuesToList(session$input), {
force(expr)
}, ignoreInit = TRUE)
} |
Actually... if you're already writing JavaScript, you could use In terms of potential changes to Shiny, it does seem like there's something missing here; we have the notion of server busy/idle state, but not on the client. |
@jcheng5 Thanks a lot. The first method worked! execute_at_next_input <- function(expr, session = getDefaultReactiveDomain()) {
observeEvent(once = TRUE, reactiveValuesToList(session$input), {
print(reactiveValuesToList(session$input))
force(expr)
}, ignoreInit = TRUE)
}
ui <- fluidPage(
actionButton("a", "add UI"),
uiOutput("ui_out")
)
loadserver <- function(input, output, session){
if(input$n10 >= 0) print(1)
#### many other things
}
server <- function(input, output, session) {
observeEvent(input$a, {
output$ui_out <- renderUI({
lapply(1:10, function(i) {
numericInput(paste0("n", i), paste0("n", i), value = 0)
})
})
execute_at_next_input(loadserver(input, output, session))
})
}
shinyApp(ui, server) It actually waits for not only a single one but all the inputs in the next group to be set. $n9
[1] 0
$n4
[1] 0
$n3
[1] 0
$n7
[1] 0
$n8
[1] 0
$n5
[1] 0
$n1
[1] 0
$n6
[1] 0
$a
[1] 1
attr(,"class")
[1] "integer" "shinyActionButtonValue"
$n2
[1] 0
$n10
[1] 0 I tested with a much bigger dynamically loaded app and it worked fine. And I can use Could this be officially supported? Like I proposed, maybe add a Not so much luck with the second method: ui <- fluidPage(
actionButton("a", "add UI"),
uiOutput("ui_out")
)
loadserver <- function(input, output, session){
if(input$n10 >= 0) print(1)
#### many other things
}
server <- function(input, output, session) {
observeEvent(input$a, {
output$ui_out <- renderUI({
lapply(1:10, function(i) {
numericInput(paste0("n", i), paste0("n", i), value = 0)
})
})
session$onFlushed( function() {
message("End flush")
observeEvent(1, once = TRUE, {
print(reactiveValuesToList(session$input))
loadserver(input, output, session)
})
}, once = TRUE)
}, ignoreInit = TRUE)
}
shinyApp(ui, server) $a
[1] 1
attr(,"class")
[1] "integer" "shinyActionButtonValue"
Warning: Error in if: argument is of length zero
[No stack trace available] It immediately flushed. It doesn't wait for everything to finish inside |
We can't do something exactly like that--it's not possible to add a Although I would maybe find it somewhat compelling to have something beyond |
I see, anything like so will be nice. Feel free to close the issue. I can just use this hack for now. |
+1 to this being useful. I used it to detect when a renderUI had finished generating an input, and set the focus on that element. |
I came up with an alternative solution that might be helpful. So you could introduce a library(glue)
#' Execute JS code once rendered in DOM
rendered_js_callback_ui <- function(input_id, input_value = "Date.now().toString()") {
tags$script(
glue_safe("Shiny.setInputValue(\"{input_id}\", {input_value})")
)
} See your modified example: library(shiny)
library(glue)
#' Execute JS code once rendered in DOM
rendered_js_callback_ui <- function(input_id, input_value = "Date.now().toString()") {
tags$script(
glue_safe("Shiny.setInputValue(\"{input_id}\", {input_value})")
)
}
ui <- fluidPage(
actionButton("a", "add UI"),
uiOutput("ui_out")
)
loadserver <- function(input, output, session){
if(input$n1000 >= 0) print(1)
#### many other things
}
server <- function(input, output, session) {
observeEvent(input$a, {
output$ui_out <- renderUI({
#' Wrap whole output with `tagList`
tagList(
lapply(1:1000, function(i) {
numericInput(paste0("n", i), paste0("n", i), value = 0)
}),
#' Inform server that the UI has been already rendered
rendered_js_callback_ui(input_id = "my_input_for_catching_render")
)
})
})
observeEvent(input$my_input_for_catching_render, {
loadserver(input, output, session)
}, ignoreInit = TRUE)
}
shinyApp(ui, server) Also, it is important to note that the callback function is not going to work with library(shiny)
library(glue)
#' Execute JS code once rendered in DOM
rendered_js_callback_ui <- function(input_id, input_value = "Date.now().toString()") {
tags$script(
glue_safe("Shiny.setInputValue(\"{input_id}\", {input_value})")
)
}
ui <- fluidPage(
actionButton("a", "add UI"),
uiOutput("ui_out")
)
loadserver <- function(input, output, session){
if(input$n1000 >= 0) print(1)
#### many other things
}
server <- function(input, output, session) {
observeEvent(input$a, {
#' `insertUI` instead of `renderUI`
insertUI(
session = session,
selector = "#ui_out",
ui = tagList(
lapply(1:1000, function(i) {
numericInput(paste0("n", i), paste0("n", i), value = 0)
}),
#' Inform server that the UI has been already rendered - it won't work here
rendered_js_callback_ui(input_id = "my_input_for_catching_render")
)
)
})
observeEvent(input$my_input_for_catching_render, {
loadserver(input, output, session)
}, ignoreInit = TRUE)
}
shinyApp(ui, server) |
good angle of thoughts. In Joe's solution users only needs to call the function and define the expression inside, but it's observing all filpro's solution only needs to take care of one input, but users need to know advanced knowledge of JS-Shiny-R communication and need to set a new input for every render event. It doesn't feel very user-friendly. @jcheng5 Since this has drawn quite some attention, do we have any official plan for a clean and easy way to support this feature? |
Since I have seen these issue quite a few times on StackOverflow 1, 2 myself, I can imagine there are more similar issues that I didn't see. Instead of waiting for the official fix, I wrap this temp fix it into a function in my package spsComps. The function is called onNextInput. Not on CRAN yet, so you need to install the dev version It works as an library(spsComps)
# Simple example
ui <- fluidPage(
uiOutput("someui")
)
server <- function(input, output, session) {
output$someui <- renderUI({
# we update the text of new rendered text input to 3 random letters
# after `textInput` is displayed, and it only works for one time.
onNextInput({
updateTextInput(inputId = "mytext", value = paste0(sample(letters, 3), collapse = ""))
})
textInput("mytext", "some text")
})
# if you directly have update event like following line, it won't work
# updateTextInput(inputId = "mytext", value = paste0(sample(letters, 3), collapse = ""))
}
shinyApp(ui, server)
# complex example with modules
modUI <- function(id) {
ns <- NS(id)
textInput(ns("mytext"), "some text")
}
modServer = function(id) {
moduleServer(
id,
function(input, output, session) {
updateTextInput(inputId = "mytext", value = paste0(sample(letters, 3), collapse = ""))
}
)
}
ui = fluidPage(
actionButton("a", "load module UI"),
uiOutput("mod_container")
)
server = function(input, output, session) {
# everytime you click, render a new module UI and update the text value
# immediately
observeEvent(input$a, {
output$mod_container <- renderUI({
onNextInput(modServer("mod"))
modUI("mod")
})
})
# Without `onNextInput`, module server call will not work
# uncomment below and, comment `onNextInput` line to see the difference
# modServer("mod")
}
shinyApp(ui, server) |
I am having this problem also. I add a number of selectInputs in a renderUI. I get a double render of a datatable, One of which can be quite time consuming when all the inputs default to null. One thing I have tried, is to use req on input$my_select_input. Unfortunately, the truthiness is a problem the selectinput defaults to null when nothing is selected. Another solution I have tried is to add a selectInput with a default value, then hide it, However, renderUI doesn't seem to respect any hidden() functionality or hide(). Right now I am just leaving a disabled action button on the UI actionButton("shiny sucks>") not exactly, but you get the idea. Why is renderUI so difficult to work with? Other things I have tried is to take an isolated look at input[[]] eg req(my_select_input %in% isolate(names(input))) to see if the variable is in the list of variables. This seems like it should work, but it causes double rendering anyway. Why does renderUI have so many rough edges? |
Hello, Additionally, I found a pretty easy solution without any JS code using a global variable that indicates when something should be run after flushing. It should be doing the same stuff described above but with less code :) server <- function(input, output, session) {
redrawing <- FALSE
output$show<- renderUI({
redrawing <<- TRUE
# render stuff...
})
session$onFlushed(function() {
if (redrawing) {
# do stuff after UI has been rendered
redrawing <<- FALSE
}
}, once = F)
} |
This worked flawlessly for my simple case (use some jQuery to add If you're unfortunate enough to find yourself here, you're in good fortune now. This worked easily. |
The same can be done using
|
If we use
renderUI
to load some large UI and call the rendered UI associated server functions after therenderUI
, it will likely cause problems because the UI is just sent to the client and input-binding registration is still ongoing.Simple example:
I know I can add
req
to prevent the error, but please don't struggle with this. Just imagineloadserver
comes from a package and you can't do anything in the function. The normal preloading has no problem (load the required UI and server on start), but only causes problems with this dynamic loading. So, let's just say I want to wait until the UI loading and input-binding is complete and do something in the server.Can we have some options:
renderUI
sync ("blocking") before executing the next line. Now it is more like async, it only sends out the UI as a message and goes on to the next line. It doesn't know if the input-binding is complete or the expected inputs are registered in the current shiny session. Add an option likerenderUI(wait = TRUE)
The text was updated successfully, but these errors were encountered: