0

I am new to shiny and am currently trying to develop my first shinyapp.

This apps contains multiple actionButtons and nested observeEvents statements, which I think are the cause of my problem.

The app should allow the user to add observations of species by clicking on a add button, that updates the UI. Within each observation, more details can be asked, but I only showed the species name in the REPREX below (textinput).

Each observation can be deleted individually via a delete button.

Until here, it works! However, I also want a modal dialog to confirm the deletion when the delete button is clicked. To do this, I used a nested observeEvent and it doesn't seem to work (or maybe only for the first time). What am I doing wrong ?

Thanks in advance to anyone who tries to help me.

library(shiny)
library(random)

ui <- fluidPage(

  fluidRow(br(), br(), actionButton("adder", 
                                    label = "Add an observation"),
           align="center")
  )



server <- function(input, output,session) {
  rv <- reactiveValues()
  
  rv$GridId_list <- c()

  observeEvent(input$adder,{
    
    # create random ID for each added species
    GridId <- as.character(randomStrings(1, 10))
    
    # store the new ID 
    rv$GridId_list <- c(rv$GridId_list,GridId)
    
    # ID for the textinput
    SpId <- paste(GridId, "sp", sep="_")
    
    # ID of the button used to remove this species
    removeSpeciesId <- paste(GridId,'remover', sep="_")
    
    #Update of the UI
    insertUI(
      selector = '#adder',
      where = "beforeBegin",
      ui = tags$div(
        id = GridId,
        fluidRow(
          column(6,
                 h5("Species name : "),
                 textInput(SpId,label = NULL)
                 ),

          column(6, align = "center",
                 br(),br(), 
                 actionButton(removeSpeciesId,
                              label = "Delete")
                 )
          )
        )
      )

    # Remove an observation when the "delete" button is clicked (and after confirmation)
    observeEvent(input[[removeSpeciesId]], {
      #Confirmation modal
      showModal(
        modalDialog(
          "Are you sure ?",
          title = "Delete",
          footer = tagList(
            actionButton("cancel", "Cancel"),
            actionButton("confirm", "Confirm", class = "btn btn-danger")
            )
          )
        )
      
      # Delete observation if user confirms
      observeEvent(input$confirm, {
        id_to_remove <- substring(removeSpeciesId,1, nchar(removeSpeciesId)-8)
        rv$GridId_list <- rv$GridId_list[rv$GridId_list!=id_to_remove]
        removeUI(selector = paste("#", id_to_remove, sep = ""))
        showNotification("Observation deleted !")
        removeModal()
      })
      
      # Just remove the modal if user cancels
      observeEvent(input$cancel, {
        removeModal()
      })
    })


  })
}

shinyApp(ui = ui, server = server, options = list(launch.browser = T))
shafee
  • 15,566
  • 3
  • 19
  • 47

1 Answers1

0

Referencing dynamic input id's is a pain. I find it best to add a last clicked input identifier to reference. You can add a class to those inputs to just listen to them and not others in your app:

tags$head(tags$script(HTML("$(document).on('click', '.needed', function () {
                                Shiny.onInputChange('last_btn',this.id);
                             });")))

That little piece of code will allow you to get an input$last_btn id, that you can use for your event listeners. In this case you don't need to nest your event listeners; it is better to think about the events in sequence and program those reactions. So, with some tweakings in your code, your app now looks like this:

library(shiny)
library(random)

ui <- fluidPage(
  tags$head(tags$script(HTML("$(document).on('click', '.needed', function () {
                                Shiny.onInputChange('last_btn',this.id);
                             });"))),
  fluidRow(br(), br(), actionButton("adder", 
                                    label = "Add an observation"),
           align="center")
)



server <- function(input, output,session) {
  
  rv <- reactiveValues()
  
  rv$GridId_list <- c()
  
  observeEvent(input$adder,{
    
    # create random ID for each added species
    GridId <- as.character(randomStrings(1, 10))
    
    # store the new ID 
    rv$GridId_list <- c(rv$GridId_list,GridId)
    
    # ID for the textinput
    SpId <- paste(GridId, "sp", sep="_")
    
    # ID of the button used to remove this species
    removeSpeciesId <- paste(GridId,'remover', sep="_")
    
    #Update of the UI
    insertUI(
      selector = '#adder',
      where = "beforeBegin",
      ui = tags$div(
        id = GridId,
        fluidRow(
          column(6,
                 h5("Species name : "),
                 textInput(SpId,label = NULL)
          ),
          
          column(6, align = "center",
                 br(),br(), 
                 actionButton(removeSpeciesId, 
                              label = "Delete", class="needed")
          )
        )
      )
    )
  })
  
  # Remove an observation when the "delete" button is clicked (and after confirmation)
  observeEvent(input$last_btn, {
    observeEvent(input[[input$last_btn]] > 0,{#We want the modal to show when any "remover" id is clicked
      
      #Confirmation modal
      showModal(
        modalDialog(
          "Are you sure ?",
          title = "Delete",
          footer = tagList(
            actionButton("cancel", "Cancel"),
            actionButton("confirm", "Confirm", class = "btn btn-danger")
          )
        )
      )
    })
  }, ignoreNULL = TRUE, ignoreInit = TRUE)
  
  # Delete observation if user confirms
  observeEvent(input$confirm, {
    #The following selector is for the parent id of the parent id of the last_btn id
    removeUI(selector = paste0("div:has(>div:has(>#", input$last_btn, "))"))
    showNotification("Observation deleted !")
    removeModal()
  })
  
  # Just remove the modal if user cancels
  observeEvent(input$cancel, {
    removeModal()
  })
  
}

shinyApp(ui = ui, server = server, options = list(launch.browser = T))
David Jorquera
  • 2,046
  • 12
  • 35
  • Thank you for your answer, it seems to work ! I finally made my code work using the "ignoreNULL = TRUE, ignoreInit = TRUE" arguments in the listeners but your solution is much more elegant ! Thank you again ! – Hugo Counoy Jan 07 '23 at 08:28
  • If you find the answer useful please accept as solution – David Jorquera Jan 07 '23 at 12:05
  • While testing my final app, I noticed that there is still a small problem. When you click on the "delete" button and then on the "cancel", the delete button doesn't respond anymore. Maybe this bug comes from the function detecting the last clicked button. As the button id remains the same, the observEvent is not triggered ? Could you please help me solve this @David Jorquera ? – Hugo Counoy Jan 11 '23 at 10:07
  • Edited code to fix that issue! check it out – David Jorquera Jan 12 '23 at 16:09
  • Finally your solution still uses nested listeners, but at least it works perfectly ! Thanks ! – Hugo Counoy Jan 18 '23 at 15:44