In a large Shiny App, I have a lot of modules within other modules. These nested modules also sometimes have input controls, e.g. textInput() or actionButton, which trigger certain events also in the parent module.
The following MWE shows the problem.
The module summaryServer prints a summary of a value, but waits for the reactive from rangeServer, which is triggered by a button. I want a Testing specific for summaryServer with testServer() function from Shiny, but how can I "click" the Button in the contained rangeServer module to continue? Is that something about the Mock Shiny Session?
### TESTING ###
x <- reactiveVal(1:10)
testServer(summaryServer, args = list(var = x), {
cat("var active?", d_act(),"\n")
# -----------------------------
# How to click "go" here?
# -----------------------------
cat("var active?", d_act(), "\n")
})
### The app ###
summaryUI <- function(id) {
tagList(
textOutput(NS(id, "min")),
textOutput(NS(id, "mean")),
textOutput(NS(id, "max")),
rangeUI(NS(id, "range"))
)
}
summaryServer <- function(id, var) {
stopifnot(is.reactive(var))
moduleServer(id, function(input, output, session) {
d_act = reactiveVal("Haha nope")
range_val = rangeServer("range", var = var)
# waits to range_val
observeEvent(range_val(),{
d_act("TRUE")
message(range_val())
output$min <- renderText(range_val()[[1]])
output$max <- renderText(range_val()[[2]])
output$mean <- renderText(mean(var()))
})
})
}
rangeUI = function(id) {
textInput(inputId = NS(id, "go"), label = "Go")
}
rangeServer = function(id, var){
moduleServer(id, function(input, output, session){
# when button gets clicked
eventReactive(input$go,{
range(var(), na.rm = TRUE)
}, ignoreInit = TRUE, ignoreNULL = TRUE)
})
}
library(shiny)
ui <- fluidPage(
summaryUI("sum")
)
server <- function(input, output, session) {
x = reactiveVal(1:10)
summaryServer("sum", x)
}
# shinyApp(ui, server)
That is a tricky one. It works if you set both ignoreInit and ignoreNULL to FALSE but just because then you are not initially dependent on a change of go anymore, which is undesirable.
I do not think it is possible to change go inside of rangeServer when running testServer with summaryServer. You can however use {shinytest} to achieve this. Note that here you invoke and test the entire app. Therefore, when using modules, you have to call elements by their complete id, including namespaces.
(I changed go to an actionButton, everything else stays the same)
rangeUI <- function(id) {
actionButton(inputId = NS(id, "go"),label = "Go")
}
test_that("output updates when reactive input changes", {
# invoke app
app <- shinytest::ShinyDriver$new("app.R")
# initially, the button has`nt been clicked and the outputs are empty
testthat::expect_equal(app$getValue("summary-range-go"), 0)
testthat::expect_equal(app$getValue("summary-min"), "")
# click the button
app$click("summary-range-go")
testthat::expect_equal(app$getValue("summary-range-go"), 1)
# testthat::expect_equal(app$getValue("summary-min"), "1")
# for some reason, the button value increased, hence is clicked,
# but the outputs have not been triggered yet.
# another click fixes that
app$click("summary-range-go")
testthat::expect_equal(app$getValue("summary-min"), "1")
})
Related
I have a Shiny app (please see end for a minimum working example) with a "parent" reactable table and a drilldown table that pops up when a user clicks on a row of the parent table. The information on which row is selected in the parent is obtained via reactable::getReactableState(). However, when the user switches to a different "parent" table, the function returns the row selection for the outdated table, not the updated one.
This occurs event though the output for the new parent table has completed it's calculations and is fully updated by the time the drilldown table starts it's calculations. After the whole systems finished and the app is idle, something (and I'm not sure what) triggers the input to reactable::getReactableState() to be invalidated, and the reactives fire again, but this time using the updated (or "correct" from my perspective) tables, and returns the expected result, which is that now row is selected.
Referring to the reactive graph below, what I want to do is have input$tables-table_parent__reactable__selected set not NULL every time input$tables-data_set changes.
I have tried to do this via the session$sendCustomMessage() and Shiny.addCustomMessageHandler approach found here: Change the input value in shiny from server, but I find that, although I can change input$tables-table_parent__reactable__selected value it doesn't seem to send send the info to the browser until after all the outputs are done caculating when input$tables-data_set is changed.
A minimum working example:
UI module:
drilldownUI <- function(id) {
ns <- NS(id)
tagList(
tags$script("
Shiny.addCustomMessageHandler('tables-table_parent__reactable__selected', function(value) {
Shiny.setInputValue('tables-table_parent__reactable__selected', value);
});
"),
shiny::selectizeInput(
inputId = ns("data_set"),
label = "Data set",
choices = c("iris", "cars"),
selected = "iris"
),
reactable::reactableOutput(outputId = ns("table_parent"),
width = "100%"),
reactable::reactableOutput(
outputId = NS(id, "drilldown_table"),
width = "100%"
)
)
}
Server module:
drilldownServer <- function(id, dat) {
moduleServer(id, function(input, output, session) {
dataset <- reactive({
data_list <-
list(iris = as.data.table(iris), cars = as.data.table(MASS::Cars93))
data_list[[input$data_set]]
})
data_grouped <- reactive({
dataset()[, .N, by = c(grouping_var())]
})
grouping_var <- reactive({
if (input$data_set == "iris") {
return("Species")
}
"Origin"
})
output$table_parent <- reactable::renderReactable({
req(input$data_set)
reactable::reactable(
data_grouped(),
selection = "single",
onClick = "select"
)
})
selected <- reactive({
out <- reactable::getReactableState("table_parent", "selected")
if(is.null(out)||out=="NULL") return(NULL)
out
})
output$drilldown_table <- reactable::renderReactable({
req(selected())
# This should only fire after a new parent table is generated and the row selection is
# reset to NULL, but it fires once the new table is generated and BEFORE the row selection
# is reset to NULL
selected_group <- data_grouped()[selected(), ][[grouping_var()]]
drilldown_data <- dataset()[get(grouping_var()) == selected_group]
reactable::reactable(drilldown_data)
})
observeEvent(input$data_set, {
session$sendCustomMessage("tables-table_parent__reactable__selected", 'NULL')
})
})
App:
library(shiny)
library(reactable)
library(data.table)
# Define UI for application that draws a histogram
ui <- fluidPage(
drilldownUI("tables")
)
# Define server logic required to draw a histogram
server <- function(input, output) {
drilldownServer("tables")
}
# Run the application
shinyApp(ui = ui, server = server)
I found the solution thanks in part to this SO answer https://stackoverflow.com/a/39440482/9474704.
The key was to consider the row selection a state, rather than just reacting to input changes. Then, by using reactiveValues() instead of reactive(), I could update the state in multiple places using observeEvent().
An important additonal piece of information was that observe functions are eager, and you can set a priority, so when the user changes the input$data_set, I could reset the row selection to 0 before the drilldown reactable::renderReactable() section was evaluated.
The updates to the server module below for an example of the working solution:
drilldownServer <- function(id, dat) {
moduleServer(id, function(input, output, session) {
dataset <- reactive({
data_list <-
list(iris = as.data.table(iris), cars = as.data.table(MASS::Cars93))
data_list[[input$data_set]]
})
data_grouped <- reactive({
dataset()[, .N, by = c(grouping_var())]
})
grouping_var <- reactive({
if (input$data_set == "iris") {
return("Species")
}
"Origin"
})
# Create output for parent table
output$table_parent <- reactable::renderReactable({
req(input$data_set)
reactable::reactable(data_grouped(),
selection = "single",
onClick = "select")
})
# Create state variable
selected <- reactiveValues(n = 0)
currentSelected <- reactive({
reactable::getReactableState("table_parent", "selected")
})
observeEvent(currentSelected(), priority = 0, {
selected$n <- currentSelected()
})
# When data set input changes, set the selected number of rows to 0e
observeEvent(input$data_set,
label = "reset_selection",
priority = 9999, {
selected$n <- 0
})
# Create output for drilldown table
output$drilldown_table <- reactable::renderReactable({
req(selected$n > 0)
selected_group <-
data_grouped()[selected$n, ][[grouping_var()]]
drilldown_data <-
dataset()[get(grouping_var()) == selected_group]
reactable::reactable(drilldown_data)
})
})
}
So far I made a Shiny app that has three inputs connected to the database and a final download button. Everything works well except the download button. The actual data downloading part works but I want to add one last logic that hides the download button if myvars$input3 is empty:
observe({
if (is.null(myvars$var3)) {shinyjs::hide("???")}
else {shinyjs::show("???")}
})
server_tab2.R:
Function1 dropdownTab2Server:
Defined the date range logic with id daterange_tab2
Defined the last input dropdown logic with id var_list_tab2
Function2 downloadTab2Server:
Defined the logic for download button
server.R: (This part is not working)
Want to only show the download button if the third input (myvars$input3) is not empty
ui_tab2.R: Defined the three inputs explained in ui.R:
var_lab_tab2: A static dropdown input with only two choices Choice1 and Choice2
daterange_tab2_ui: A date range
subid_dropdown_tab2_ui: The last dropdown input that depends on the first two
##### server_tab2.R
#### Function 1 - A dropdown input dependent on the date range
dropdownTab2Server <- function(id) {
moduleServer(id, function(input, output, session) {
ns <- session$ns
rv <- reactiveValues()
output$daterange_tab2_ui <- renderUI({
req(input$var_lab_tab2)
dateRangeInput(ns("daterange_tab2"), "Date Range:", start = min_max_date_df$min_date, end = min_max_date_df$max_date) # Retrieved from "global.R"
})
unique_lists_tab2 <- reactive({
sql <- glue_sql("
SELECT
DISTINCT list AS unique_list
FROM table1
WHERE date BETWEEN date ({dateid1_tab2*}) AND date ({dateid2_tab2*})
",
dateid1_tab2 = input$daterange_tab2[1],
dateid2_tab2 = input$daterange_tab2[2],
.con = pool
)
dbGetQuery(pool, sql)
})
output$subid_dropdown_tab2_ui <- renderUI({
req(input$daterange_tab2[1], input$daterange_tab2[2])
shinyWidgets::pickerInput(
ns("var_list_tab2"),
"Stations:",
choices = unique_lists_tab2(),
multiple = T
)
})
observe({
rv$var1 <- input$daterange_tab2[1]
rv$var2 <- input$daterange_tab2[2]
rv$var3 <- input$var_list_tab2
})
return(rv)
}
)
}
#### Function 2 - download button
downloadTab2Server <- function(id, df, filename) {
moduleServer(id, function(input, output, session) {
output$downloadbttn_tab2 <- downloadHandler(
filename = function() {
paste0(filename, ".xlsx")
},
content = function(file) {
WriteXLS::WriteXLS(df, file)
}
)
}
)
}
##### server.R => Struggling with this part
function(input, output, session) {
dropdownTab2Server("dropdown_ui_tab2")
myvars <- dropdownTab2Server("dropdown_ui_tab2")
### download button layout => Struggling with this part
observe({
if (is.null(myvars$var3)) {shinyjs::hide("???")}
else {shinyjs::show("???")}
})
downloadTab2Server(
id = "download_ui_tab2",
df = fake_data(), # reactive
filename = "data"
)
}
##### ui_tab2.R
downloadTab2UI <- function(id) {
ns <- NS(id)
tagList(
shinyWidgets::pickerInput(
ns("var_lab_tab2"),
"ID:",
choices = c("Choice1", "Choice2"), multiple = T
),
uiOutput(ns("daterange_tab2_ui")),
uiOutput(ns("subid_dropdown_tab2_ui")),
downloadButton(ns("downloadbttn_tab2"), "Download Data")
)
}
##### ui.R
downloadTab2UI("download_ui_tab2")
You could the following in the main server part (I've changed it to an observeEvent because I think it's easier to reason what exactly it listens to):
observeEvent(myvars$var3, {
if (is.null(myvars$var3)) {shinyjs::hide("download_ui_tab2-downloadbttn_tab2")}
else {shinyjs::show("download_ui_tab2-downloadbttn_tab2")}
}, ignoreNULL = FALSE)
You need to prefix the download button id with the correct namespace, in your case "download_ui_tab2".
However, this is not great style as you need to manually handle the namespace. A cleaner solution would be to pass myvars to the downloadTab2Server module as an argument and then have the observeEvent in the module code. Then you can directly use downloadbttn_tab2 and don't need to manually prefix the namespace.
I am working on a shiny app that can generate a determined number of UI outputs in form of inputs based on a value defined by the user. Thanks to the help of #YBS I was able to get the app working. But now I face a new issue. Although I could define min and max value for the inputs generated, I would like to add a warning in the inputs when a value is greater than 100, I found shinyfeedback package can do this but I do not where to put properly the code or what to do in the case of dynamic inputs like the ones generated here.
This is the working app:
library(shiny)
library(shinydashboard)
library(DT)
library(shinyFeedback)
#Function
compute <- function(firstitem,seconditem)
{
Sum <- firstitem+seconditem
Difference <- firstitem+seconditem
Product <- firstitem*seconditem
Ratio <- firstitem/seconditem
Res <- data.frame(C1=Sum,C2=Difference,C3=Product,C4=Ratio)
return(Res)
}
#App
ui = shinyUI(fluidPage(
titlePanel("Compare"),
sidebarLayout(
sidebarPanel(
numericInput("numitems", label = "Number of items to compare?",
min = 1, max = 100, value = 1),
uiOutput("period_cutpoints"),
uiOutput("period_cutpoints2"),
actionButton("submit", "Submit")
),
mainPanel(
uiOutput("t1")
)
)
))
server = shinyServer(function(input, output, session) {
output$period_cutpoints<-renderUI({
req(input$numitems)
lapply(1:(input$numitems), function(i) {
numericInput(inputId=paste0("firstitem",i),
label=paste0("Enter the value of first item ", i, ":"),value = i)
})
})
output$period_cutpoints2<-renderUI({
req(input$numitems)
lapply(1:(input$numitems), function(i) {
numericInput(inputId=paste0("seconditem",i),
label=paste0("Enter the value of second item ", i, ":"),value = i+i)
})
})
seldates <- reactiveValues(x=NULL)
observeEvent(input$submit, {
seldates$x <- list()
lapply(1:(input$numitems), function(i) {
seldates$x[[i]] <- compute(firstitem = input[[paste0("firstitem", i)]],seconditem = input[[paste0("seconditem", i)]])
})
})
observeEvent(input$submit, {
lapply(1:(input$numitems), function(i) {
output[[paste0("table",i)]] <- renderDT(seldates$x[[i]])
})
output$t1 <- renderUI({
tagList(
lapply(1:(input$numitems), function(i) {
DTOutput(paste0("table",i))
})
)
})
})
})
shinyApp(ui = ui , server = server)
I tried to add some code inside the dynamic inputs in this way:
#Code demo
output$period_cutpoints<-renderUI({
req(input$numitems)
lapply(1:(input$numitems), function(i) {
numericInput(inputId=paste0("firstitem",i),
label=paste0("Enter the value of first item ", i, ":"),value = i)
})
lapply(1:(input$numitems), function(i) {
observeEvent(input[[paste0('firstitem',i)]], {
shinyFeedback::feedbackWarning(
inputId = paste0('firstitem',i),
show = input[[paste0('firstitem',i)]] > 100,
text = "Number less than 100 required.",
color="red"
)
})
})
})
Unfortunately, this action broke down the app:
And the first input was not generated as you can see.
How can I solve this issue so that I can have warnings when the value is greater than 100? Moreover, this leads to an additional fact, in the action button if working with multiple inputs generated dynamically, how could I do something like this:
#How to extend the if condition so that it can consider the number of inputs defined by the user
observeEvent(input$submit,
{
if(input$firstitem1 < 0 && input$seconditem1 < 0 && input$firstitem2<0 && input$seconditem1<0)
{
showModal(modalDialog(title ="Warning!!!", "Check fields!!!",easyClose = T))
}
else
{
showModal(modalDialog(title ="Congratulations!!!", "Computing Done!!!",easyClose = T))
}
})
How could I change the if so that it considers all the inputs that can be generated.
Many thanks!
I think you have a couple of problems here.
First, you have forgotten to add useShinyFeedback() to your UI definition.
ui = shinyUI(
fluidPage(
useShinyFeedback(),
titlePanel("Compare"),
...
Second, you've put the observeEvents that monitor your first item values inside your renderUI. That's not going to work: R's standard scoping means that these observeEvents won't be available to monitor changes in the corresponding input widgets. The solution is to create a separate observeEvent to create your observers on the inputs:
observeEvent(input$numitems, {
lapply(1:(input$numitems), function(i) {
observeEvent(input[[paste0('firstitem',i)]], {
shinyFeedback::feedbackWarning(
inputId = paste0('firstitem',i),
show = input[[paste0('firstitem',i)]] > 100,
text = "Number less than 100 required.",
color="red"
)
})
})
})
Making these changes gives me, for example,
With regard to your final question about the Submit actionButton, and as a general observation, I think your life will be much easier if you use Shiny modules to solve this problem. This will allow you to delegate the error checking to the indivudual modules and remove the need to continually loop through the indices of the dynamic inputs. This will lead to shorter, simpler, and more understandable code.
One thing to bear in mind if you do this: make sure you put a call to useShinyFeedback in the definition of the module UI.
I have two DTedit tables which are functionally related
I do not want users to get the Insert/New button in DT#2 when no row is selected in DT#1
I have Table1_Results$rows_selected to test if selection exists (length>0)
I also identified the id of the 'New button' in DT#2 as being Table2_add
But do not succeed to make the length of Table1_Results$rows_selected trigger the shinyjs show() or hide() action for DT#2
Could anyone please share some reactivity command to do this!
the following code is not working but illustrates my aim
observe(Table1_Results$rows_selected,{
if (length(Table1_Results$rows_selected)) {
shinyjs::show('Table2_add')
} else {
shinyjs::hide('Table2_add')
}
})
Error in .getReactiveEnvironment()$currentContext() : Operation not
allowed without an active reactive context. (You tried to do something
that can only be done from inside a reactive expression or observer.)
This manual test using a button works
observeEvent(input$showhide, {
toggle('Table2_add')
})
So it is really the reactive testing of the Table1_Results$rows_selected which is lacking
Thanks in advance
In the code below:
I cannot clear the selected row in the observed textoutput
I do not succeed to hide the New button
Note: I use DTedit because it allows other features not shown here
AIMs:
1) when no drink is selected, hide the New button for containers
2) manage <table>$rows_selected so that it reflects the current status
library("shiny")
library("shinyjs")
library("DT")
library("DTedit")
server <- function(input, output) {
Drink_Results <- dtedit(
input, output,
name = 'Drink',
thedata = data.frame(
ID = c(1:3),
drink = c('Tea', 'Coffea', 'Water'),
stringsAsFactors = FALSE
)
)
# create proxy to clear row selection (found 'Drinkdt' by looking in the source)
Drink_proxy <- DT::dataTableProxy('Drinkdt')
Container_Results <- dtedit(
input, output,
name = 'Container',
thedata = data.frame(
ID = c(1:3),
Container = c('Cup', 'Glass', 'Pint'),
stringsAsFactors = FALSE
)
)
# create proxy to clear row selection
Container_proxy <- DT::dataTableProxy('Container')
# manually toggle visibility for New button
observeEvent(input$showhide, {
shinyjs::toggle('Container_add')
})
# clear Drink row selection
observeEvent(input$clearrows, {
Drink_proxy %>% selectRows(NULL)
})
# when no drink is selected, hide the New button for containers
observeEvent(Drink_Results$rows_selected, {
if ( length(Drink_Results$rows_selected) ) {
shinyjs::show('Container_add')
} else {
shinyjs::hide('Container_add')
}
})
# attempt to react on clearing the row-selection
choice <- reactive({
paste0(Drink_Results$rows_selected, " - ", Container_Results$rows_selected)
})
# output current combination
output$choice <- renderText({ as.character(choice()) })
}
ui <- tagList(useShinyjs(),
fluidPage(
shinyFeedback::useShinyFeedback(),
h3('What will you drink?'),
uiOutput('Drink'),
# manually clear row selections
actionButton(inputId="clearrows", label="clear selected drink", icon=icon('trash')),
hr(),
h3("What container do you prefer?"),
uiOutput('Container'),
hr(),
# manually hide the New button
actionButton(inputId="showhide", label="toggle New buttons", icon=icon('refresh')),
hr(),
# show current user choices
textOutput('choice'),
)
)
shinyApp(ui = ui, server = server)
The reactive for selected row is input$Drinkdt_rows_selected in your case, based on the source code. If you use that, your code works fine. Try this
server <- function(input, output) {
## could not install DTedit. So, made a copy of the function
source("C:\\RStuff\\GWS\\dtedit.R", local=TRUE)
Drink_Results <- dtedit(
input, output,
name = 'Drink',
thedata = data.frame(
ID = c(1:3),
drink = c('Tea', 'Coffea', 'Water'),
stringsAsFactors = FALSE
)
)
name <- "Drink"
# create proxy to clear row selection (found Drinkdt by looking in the source)
Drink_proxy <- DT::dataTableProxy('Drinkdt')
Container_Results <- dtedit(
input, output,
name = 'Container',
thedata = data.frame(
ID = c(1:3),
Container = c('Cup', 'Glass', 'Pint'),
stringsAsFactors = FALSE
)
)
# create proxy to clear row selection
Container_proxy <- DT::dataTableProxy('Container')
# clear Drink row selection
observeEvent(input$clearrows, {
Drink_proxy %>% selectRows(NULL)
shinyjs::hide('Container_add')
})
sel <- reactive({!is.null(input[[paste0(name, 'dt_rows_selected')]])})
observe({
print(sel())
print(input$Drinkdt_rows_selected)
})
# when no drink is selected, hide the New button for containers
observe({
#observeEvent(input[[paste0(name, 'dt_rows_selected')]], {
if ( length(input[[paste0(name, 'dt_rows_selected')]])>0 ) {
shinyjs::show('Container_add')
}else {
shinyjs::hide('Container_add')
}
})
observeEvent(Drink_Results$thedata, {
message(Drink_Results$thedata)
})
observeEvent(input[[paste0(name, 'dt_rows_selected')]], ignoreNULL = FALSE, {
# 'no' (NULL) row will be 'selected' after each edit of the data
message(paste("Selected row:", input[[paste0(name, 'dt_rows_selected')]]))
})
# attempt to react on clearing the row-selection
choice <- reactive({
if (is.null(input[[paste0(name, 'dt_rows_selected')]])) {
paste0("Drink not selected")
}else {
paste0(input[[paste0(name, 'dt_rows_selected')]], " - ", input$Containerdt_rows_selected)
}
})
observeEvent(input$showhide, {
toggle('Container_add')
})
# output current combination
output$choice <- renderText({ choice() })
}
ui <- fluidPage(
shinyFeedback::useShinyFeedback(),
useShinyjs(),
h3('What will you drink?'),
uiOutput('Drink'),
# manually clear row selections
actionButton(inputId="clearrows", label="clear selected drink", icon=icon('trash')),
hr(),
h3("What container do you prefer?"),
uiOutput('Container'),
hr(),
# manually hide the New button
actionButton(inputId="showhide", label="toggle New buttons", icon=icon('refresh')),
hr(),
# show current user choices
textOutput('choice'),
)
shinyApp(ui = ui, server = server)
btw - it should be mentioned that the original code was not using the jbryer version of DTedit (v1.0.0) , which does not return $rows_selected. The modified DavidPatShuiFong version of DTedit (v 2.2.3+) does return $rows_selected.
The original code presented above used an observeEvent which, by default, has ignoreNULL = TRUE. That doesn't work, because if no row is selected, then $rows_selected will return NULL.
One option is to set ignoreNULL = FALSE. Unfortunately, this still leaves the problem that shinyjs::hide does not work on first execute, perhaps because 'Container_add' does not yet exist on first pass. Adding an invalidateLater which only executes a few times fixes that problem.
library("shiny")
library("shinyjs")
library("DT")
library("DTedit")
server <- function(input, output, session) {
Drink_Results <- dtedit(
input, output,
name = 'Drink',
thedata = data.frame(
ID = c(1:3),
drink = c('Tea', 'Coffea', 'Water'),
stringsAsFactors = FALSE
)
)
# create proxy to clear row selection (found 'Drinkdt' by looking in the source)
Drink_proxy <- DT::dataTableProxy('Drinkdt')
Container_Results <- dtedit(
input, output,
name = 'Container',
thedata = data.frame(
ID = c(1:3),
Container = c('Cup', 'Glass', 'Pint'),
stringsAsFactors = FALSE
)
)
# create proxy to clear row selection
Container_proxy <- DT::dataTableProxy('Container')
# manually toggle visibility for New button
observeEvent(input$showhide, {
shinyjs::toggle('Container_add')
})
# clear Drink row selection
observeEvent(input$clearrows, ignoreNULL = FALSE, {
Drink_proxy %>% selectRows(NULL)
shinyjs::hide('Container_add')
})
# when no drink is selected, hide the New button for containers
invalidateCount <- reactiveVal(0)
observe({
# need to execute this observe more than once
# (?because 'Container_add' does not actually exist first time?)
if (isolate(invalidateCount()) < 1) {
shiny::invalidateLater(200, session) # 200ms delay
}
isolate(invalidateCount(invalidateCount() + 1))
print(paste0("row selected:", Drink_Results$rows_selected))
if (!is.null(Drink_Results$rows_selected)) {
shinyjs::show('Container_add')
} else {
shinyjs::hide('Container_add')
}
})
}
ui <- tagList(useShinyjs(),
fluidPage(
h3('What will you drink?'),
uiOutput('Drink'),
# manually clear row selections
actionButton(inputId="clearrows", label="clear selected drink", icon=icon('trash')),
hr(),
h3("What container do you prefer?"),
uiOutput('Container'),
hr(),
# manually hide the New button
actionButton(inputId="showhide", label="toggle New buttons", icon=icon('refresh')),
hr(),
# show current user choices
textOutput('choice'),
)
)
shinyApp(ui = ui, server = server)
I am trying to create a table using Shiny, where the user can click on a row in order to see further information about that row. I thought I understood how to do this (see code attached).
However, right now as soon as the user clicks the "getQueue" action button, the observeEvent(input$fileList_cell_clicked, {}) seems to get called. Why would this be called before the user even has the chance to click on a row? Is it also called when the table is generated? Is there any way around this?
I need to replace "output$devel <- renderText("cell_clicked_called")" with code that will have all sorts of errors if there isn't an actual cell to refer to.
Thank you for any advice!
ui <- fluidPage(
actionButton("getQueue", "Get list of queued files"),
verbatimTextOutput("devel"),
DT::dataTableOutput("fileList")
)
shinyServer <- function(input, output) {
observeEvent(input$getQueue, {
#get list of excel files
toTable <<- data.frame("queueFiles" = list.files("queue/", pattern = "*.xlsx")) #need to catch if there are no files in queue
output$fileList <- DT::renderDataTable({
toTable
}, selection = 'single') #, selection = list(mode = 'single', selected = as.character(1))
})
observeEvent(input$fileList_cell_clicked, {
output$devel <- renderText("cell_clicked_called")
})}
shinyApp(ui = ui, server = shinyServer)
minimal error code
DT initializes input$tableId_cell_clicked as an empty list, which causes observeEvent to trigger since observeEvent only ignores NULL values by default. You can stop the reactive expression when this list is empty by inserting something like req(length(input$tableId_cell_clicked) > 0).
Here's a slightly modified version of your example that demonstrates this.
library(shiny)
ui <- fluidPage(
actionButton("getQueue", "Get list of queued files"),
verbatimTextOutput("devel"),
DT::dataTableOutput("fileList")
)
shinyServer <- function(input, output) {
tbl <- eventReactive(input$getQueue, {
mtcars
})
output$fileList <- DT::renderDataTable({
tbl()
}, selection = 'single')
output$devel <- renderPrint({
req(length(input$fileList_cell_clicked) > 0)
input$fileList_cell_clicked
})
}
shinyApp(ui = ui, server = shinyServer)