Introduction
I recently came across this stackoverflow post from 6 years ago and I was intrigued.
The OP wanted to render a dynamic number of selectInput
s. The number of the selectInput
s would be dependent on the value of a numericInput
.
If the numericInput
had a value of 3 then there would be 3 selectInput
s on the UI.
The OP correctly made this observation:
- Say the default value of the
numericInput
is 3. If you change it to 2, the UI correctly updates and there are only 2selectInputs
. But when you print theinput
object, it still contains the id and value of the 3rdselectInput
even though it is not currently rendered. - Generally, the
input
slot does not correspond with the current number of elements, but with the largest number chosen during the session.
Here is the reprex the OP provided:
reprex-from-op.R
library(shiny)
library(plyr)
<- function(id) {
testUI <- NS(id)
ns uiOutput(ns("container"))
}
<- function(input, output, session, numElems) {
test $container <- renderUI(do.call(tagList, llply(1:numElems(), function(i)
outputselectInput(session$ns(paste0("elem", i)),
label = i, choices = LETTERS[sample(26, 3)]))))
<- reactive(reactiveValuesToList(input))
getNames list(getNames = getNames)
}
<- fluidPage(numericInput("n", "Number of Elems", value = 3),
ui testUI("column1"),
verbatimTextOutput("debug"))
<- function(input, output, session) {
server <- reactive(input$n)
getN <- callModule(test, "column1", getN)
handler $debug <- renderPrint(handler$getNames())
output
}
shinyApp(ui, server)
My manager at work always says “Trust but verify”. So please run the reprex and ascertain that all the above observations are indeed true.
My $0.02
The OP had 2 questions:
- Is this behaviour intentional?
- If so, how can I update the
input
to assure that it only contains valid slots?
TL;DR
Is this behaviour intentional?
I don’t know. But it is consistent
. The OP used renderUI
in the reprex, I will use insertUI
/removeUI
in my reprex later and you will see that the behaviour is the same.
How can I update the
input
to assure that it only contains valid slots?
You can set the “invalid” slots to NULL
, then use req()
or isTruthy()
to check for validity in your server.R
.
Explanation
We finally get to the juicy part.
Here’s what I know about input
:
input
is immutable from the app’s server, unless you useupdate*Input()
. eg. If you try this:server.R
$random_id <- "trial" input
you will get an error: “Can’t modify read-only reactive value ‘random_id’”.
Once added, you can’t remove an element (an input id) from
input
, but you can change its value (usingupdate*Input()
or JavaScript).Changing the value of an element to
NULL
will not remove it frominput
.By that I mean
input
will not behave like a regular list where setting the value of an element toNULL
removes it from the list:regular-list.R
<- list(a = 1, b = 2, c = 3) x $a <- NULL x x # $b # [1] 2 # # $c # [1] 3
Also, you can’t use
update*Input()
to set the value of an input id toNULL
. From?updateSelectInput
:Any arguments with
NULL
values will be ignored; they will not result in any changes to the input object on the client.So to set the value of an input element to
NULL
you have to use JavaScript and provide the optionpriority: "event"
. Reference.script.js
.setInputValue(input_id, new_input_value, {priority: "event"}); Shiny
We can use
3
above to our advantage: Set the unwanted input id values (the ones whose UI has been removed/deleted) toNULL
.This is what will allow us to use
req()
orisTruthy()
if need be.
Reprex
In the reprex below, I show how you can set the input id values to NULL
.
Also, I use insertUI
/removeUI
as stated earlier.
global.R
library(shiny)
ui.R
<- fluidPage(
ui numericInput("n", "Number of Elems", value = 3),
mod_test_ui("column1"),
verbatimTextOutput("debug"),
$script(src = "script.js")
tags )
server.R
<- function(input, output, session) {
server <- mod_test_server(
handler id = "column1",
numElems = reactive({ input$n })
)$debug <- renderPrint({ handler$getNames() })
output }
R/mod_test_ui.R
<- function(id) {
mod_test_ui <- NS(id)
ns $div(id = ns("container"))
tags }
R/mod_test_ui.R
R/mod_test_server.R
<- \(id, numElems) {
mod_test_server moduleServer(
id = id,
module = \(input, output, session) {
<- session$ns
ns # reactive to track added UI ids:
<- reactiveValues(ids = NULL)
rv_added_ids observeEvent(numElems(), {
# do nothing if `numElems()` is less than zero:
<- numElems()
n if (n < 0) return()
# remove previously rendered UIs:
removeUI(
selector = sprintf("#%s > *", ns("container")),
multiple = TRUE,
immediate = TRUE
)# inform JS to set the removed input id values to NULL:
lapply(rv_added_ids$ids, \(id) {
$sendCustomMessage(
sessiontype = "set_to_null",
list(id = id, value = NULL)
)
})# reset tracker:
$ids <- NULL
rv_added_ids# add new UIs:
lapply(seq_len(n), \(i) {
<- ns(paste0("elem", i))
id # track new id:
$ids <- c(rv_added_ids$ids, id)
rv_added_idsinsertUI(
selector = paste0("#", ns("container")),
where = "beforeEnd",
ui = selectInput(
inputId = id,
label = i,
choices = LETTERS[sample(26, 3)]
)
)
}
)
})<- reactive(reactiveValuesToList(input))
getNames list(getNames = getNames)
}
) }
www/script.js
$(document).ready(function() {
.addCustomMessageHandler("set_to_null", (message) => {
Shiny.setInputValue(message.id, message.value, {priority: "event"});
Shiny;
}); })
Conclusion
$(this)
has been the input
in function(input, output, session)
.
Me: At this point I can confidently say that I like JavaScript.
JS: