Skip to content
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

observeEvent stay registred after destroy() and removed the observed item #1486

Closed
sinacek opened this issue Nov 24, 2016 · 15 comments
Closed

Comments

@sinacek
Copy link

sinacek commented Nov 24, 2016

Hi,
I want dynamically add and remove new buttons and I encountered a problem when I want create button with same name which was already used (it also has been removed).

How replicate trouble:

  1. Create new button
  2. Remove the button
  3. Create button again
    In the third step was new button created and also removed. When you click on the insert button again, the new button will be create.
library(shiny)

ui <- fluidPage(actionButton('insertBtn', 'Create My Button'), tags$div(id = 'placeholder'))

server <- function(input, output, session) {
  storage <- reactiveValues(isCreated = FALSE, observer = NULL)
  
  observeEvent(input$insertBtn, {
    if(!storage$isCreated) {
      print(paste0(format(Sys.time(), "%H:%M:%S"), " Insert"))
      insertUI(
        selector = "#placeholder",
        ui = tags$div(id = "my-button", actionButton("btnRemove", "Remove this button"))
      )
      storage$isCreated <- TRUE
      storage$observer <- observeEvent(input$btnRemove, {
        print(paste0(format(Sys.time(), "%H:%M:%S"), " Remove"))
        removeUI(selector = "#my-button")
        storage$isCreated <- FALSE
        storage$observer$destroy()
        storage$observer <- NULL
      })
    }
  })
}

shinyApp(ui = ui, server = server)

My workaround was used add random suffix of button name.

@bborgesr
Copy link
Contributor

Hmmm, I might not be understanding your issue very well. But it seems to me like you are making it more complicated than it needs to be (nesting an observer inside of another observer is generally not a good idea). Does the modified/refactored code below not do exactly what you want?

library(shiny)

ui <- fluidPage(
  actionButton('insertBtn', 'Create My Button'), 
  tags$div(id = 'placeholder')
)

server <- function(input, output, session) {
  storage <- reactiveValues(isCreated = FALSE)
  
  observeEvent(input$insertBtn, {
    if (!storage$isCreated) {
      print(paste0(format(Sys.time(), "%H:%M:%S"), " Insert"))
      
      insertUI(
        selector = "#placeholder",
        ui = tags$div(id = "my-button", 
                      actionButton("btnRemove", "Remove this button"))
      )
      storage$isCreated <- TRUE
    } else {
      message("The button has already been created!")
    }
  })
  
  
  observeEvent(input$btnRemove, {
    print(paste0(format(Sys.time(), "%H:%M:%S"), " Remove"))
    
    removeUI(selector = "#my-button")
    storage$isCreated <- FALSE
  })
}

shinyApp(ui = ui, server = server)

Let me know!

@sinacek
Copy link
Author

sinacek commented Nov 24, 2016

I create simple example, where I missed note, that the name of new button have to be set by user.

I want create new section of configuration for example. (Each section have name defined by user and can be removed.)
I will click on button "Create My Button" which will create the new section and in other input text field is defined name od new button (I remove this part from example - is not necessary for description of problem).
So I have to create observer for remove button in same time as the remove button, because the name of the button is defined by user.

UPDATE:
I create newer version of example where it is more clear.

library(shiny)

ui <- fluidPage(
  textInput("btnName", "Button name", value = ""),
  actionButton('insertBtn', 'Create button'),
  tags$div(id = 'placeholder')
  )

server <- function(input, output, session) {
  storage <- reactiveValues(observers = list())
  
  myPrint <- function(...) {
    print(paste0(c(format(Sys.time(), "%H:%M:%S "), c(...)),collapse = ""))
  }
  
  observeEvent(input$insertBtn, {
    btnName <- input$btnName
    if(btnName == "") {
      myPrint("Button name can not be empty.")
      return()
    }
    if(btnName %in% names(storage$observers)) {
      myPrint("Button '", btnName, "' already exists.")
      return()
    }
    myPrint("Insert button: ", btnName)
    insertUI(
      selector = "#placeholder",
      ui = tags$div(id = "my-button", actionButton(btnName, paste0("Remove button: ", btnName)))
    )
    observer <- observeEvent(input[[btnName]], {
      myPrint("Remove ", btnName)
      removeUI(selector = "#my-button")
      storage$observers[[btnName]]$destroy()
      storage$observers <- storage$observers[-which(names(storage$observers) == btnName)]
    })
    observer <- list(observer)
    names(observer) <- btnName
    storage$observers <- c(storage$observers, observer)
      
  })
}

shinyApp(ui = ui, server = server)

@bborgesr
Copy link
Contributor

Hmm, this took me some time, but I still think the best solution (and more conceptually correct) is to stick to an observer for each button. You have to be a bit sneaky with the observer for the remove button, but it's nothing from another world. I modified the app quite a bit while trying to figure out the best approach. Here it is, again let me know if I missed something:

library(shiny)

ui <- fluidPage(
  textInput("divID", "Enter div ID:", ""),
  actionButton("isrt", "Create My Button"), 
  tags$div(id = "placeholder")
)

server <- function(input, output, session) {
  storage <- reactiveValues(divID = NULL, btnID = NULL)
  
  # take a dependency on `isrt` button
  observeEvent(input$isrt, {
    
    # handle the case when user does not provide ID
    divID <- if (input$divID == "") "id" else input$divID
    
    # only create button if there is none
    if (is.null(storage$divID)) {
      insertUI(
        selector = "#placeholder",
        ui = tags$div(id = divID, 
               actionButton(paste0(divID, "rmv"), "Remove this button"))
      )
      
      # store the ids of the containing div and the 'remove' button
      storage$divID <- divID
      storage$btnID <- paste0(divID, "rmv")
      
      # otherwise, print a message to the console
    } else {
      message("The button has already been created!")
    }
  })
  
  
  observeEvent({
    
    # take a dependency on `rmv` button *if* it exists
    id <- isolate(storage$btnID)
    if (!is.null(id)) input[[id]]
    
    # necessary for the first time around (otherwise 
    # this observer would *never* get run)
    else storage$btnID
  },
  {
    # fail silently if the `rmv` button does not exist (again, 
    # this is only necessary for the first time around)
    req(storage$btnID)
    
    # if the `rmv` button *does* exist, then make sure it has
    # been clicked
    req(input[[storage$btnID]])
    
    removeUI(selector = paste0("#", storage$divID))
    
    # reset state back to original setting
    storage$divID <- NULL
    storage$btnID <- NULL
  })
  
}

shinyApp(ui = ui, server = server)

@bborgesr
Copy link
Contributor

[Aside to @jcheng5: do you think this pattern is important enough for us to make some user-friendly wrappers? It's really not trivial to get it to work in this edge case, but it is possible. I've been looking at this for a while, so I'm having trouble judging whether this is a very particular situation or if it something more and more people are likely to run into...]

@sinacek
Copy link
Author

sinacek commented Nov 25, 2016

Your app works just for one button. I'm not able create the second one. Variable storage$btnID is common for all, isn't it?

@bborgesr
Copy link
Contributor

storage$btnID is reset each time the button is removed. Isn't that what you wanted?

I want dynamically add and remove new buttons and I encountered a problem when I want create button with same name which was already used (it also has been removed).

@sinacek
Copy link
Author

sinacek commented Nov 28, 2016

I need add more buttons with dynamic names and then can remove some of them.

@bborgesr
Copy link
Contributor

bborgesr commented Dec 5, 2016

Alright, I think I finally understood what you wanted and the best way to achieve it. We needed to add a new argument to observeEvent() in order to accommodate your request. This change is not yet merged into master, so to try it out, please restart your R session and do:

devtools::install_github("rstudio/shiny", ref="barbara/observe")
library(shiny)

Then, run this app:

ui <- fluidPage(
  textInput("divID", "Enter an ID for the custom area:", ""),
  helpText("Leave the text input blank for automatically unique IDs."),
  actionButton("isrt", "Add a button"), 
  tags$div(id = "placeholder")
)

server <- function(input, output, session) {
  rv <- reactiveValues()
  
  # take a dependency on `isrt` button
  observeEvent(input$isrt, {
    
    # handle the case when user does not provide ID
    divID <- if (input$divID == "") gsub("\\.", "", format(Sys.time(), "%H%M%OS3")) 
             else input$divID
    btnID <- paste0(divID, "rmv")
    
    # only create button if there is none
    if (is.null(rv[[divID]])) {
      
      insertUI(
        selector = "#placeholder",
        ui = tags$div(id = divID, 
                      paste0("Welcome, ", divID, "!"),
                      actionButton(btnID, "Remove this area", 
                                   class = "pull-right btn btn-danger"),
                      style = "background-color: #e0cda7;
                               height: 50px;
                               margin: 10px;
                               padding: 5px;
                               display: block;
                               border-radius: 5px;
                               border: 2px solid #2a334f;"
        )
      )
      
      # make a note of the ID of this section, so that it is not repeated accidentally
      rv[[divID]] <- TRUE
      print("created")
      
      # create a listener on the newly-created button that will
      # remove it from the app when clicked
      obs <-  observeEvent(input[[btnID]], {
        removeUI(selector = paste0("#", divID))
        
        rv[[divID]] <- NULL
        print("destroyed")
        
        obs$destroy()
        
      }, skipFirst = TRUE)
      
      # otherwise, print a message to the console
    } else {
      message("The button has already been created!")
    }
  })
}

shinyApp(ui = ui, server = server)

As you can see, the second observer() must have the new argument (skipFirst) set to TRUE, and then everything works as expected. Does this do what you wanted?

I also must say that I was wrong: for your particular use case, it's a great idea to nest the observers. I did not realize at first that you wanted to have multiple dynamically created buttons at the same time in the app.

Let me know if you have any feedback.

Thanks for the great catch!

@sinacek
Copy link
Author

sinacek commented Dec 13, 2016

I'm out of my PC, I'll look on it after New year. Thanks and happy christmas

@sinacek
Copy link
Author

sinacek commented Jan 4, 2017

I tried install your branch, but it doesn't exists.

devtools::install_github("rstudio/shiny", ref="barbara/observe")
Downloading GitHub repo rstudio/shiny@barbara/observe
from URL https://api.github.com/repos/rstudio/shiny/zipball/barbara/observe
Error in stop(github_error(request)) : 404: Not Found
(404)

@bborgesr
Copy link
Contributor

bborgesr commented Jan 4, 2017

The branch has already been merged into master, so just enter:

devtools::install_github("rstudio/shiny")
library(shiny)

And then the app:

ui <- fluidPage(
  textInput("divID", "Enter an ID for the custom area:", ""),
  helpText("Leave the text input blank for automatically unique IDs."),
  actionButton("isrt", "Add a button"), 
  tags$div(id = "placeholder")
)

server <- function(input, output, session) {
  rv <- reactiveValues()
  
  # take a dependency on `isrt` button
  observeEvent(input$isrt, {
    
    # handle the case when user does not provide ID
    divID <- if (input$divID == "") gsub("\\.", "", format(Sys.time(), "%H%M%OS3")) 
             else input$divID
    btnID <- paste0(divID, "rmv")
    
    # only create button if there is none
    if (is.null(rv[[divID]])) {
      
      insertUI(
        selector = "#placeholder",
        ui = tags$div(id = divID, 
                      paste0("Welcome, ", divID, "!"),
                      actionButton(btnID, "Remove this area", 
                                   class = "pull-right btn btn-danger"),
                      style = "background-color: #e0cda7;
                               height: 50px;
                               margin: 10px;
                               padding: 5px;
                               display: block;
                               border-radius: 5px;
                               border: 2px solid #2a334f;"
        )
      )
      
      # make a note of the ID of this section, so that it is not repeated accidentally
      rv[[divID]] <- TRUE
      print("created")
      
      # create a listener on the newly-created button that will
      # remove it from the app when clicked
      observeEvent(input[[btnID]], {
        removeUI(selector = paste0("#", divID))
        
        rv[[divID]] <- NULL
        print("destroyed")
        
      }, ignoreInit = TRUE, once = TRUE)
      
      # otherwise, print a message to the console
    } else {
      message("The button has already been created!")
    }
  })
}

shinyApp(ui = ui, server = server)

@sinacek
Copy link
Author

sinacek commented Jan 4, 2017

Great! It's what I need.

The main different is that you used new arguments ignoreInit = TRUE, once = TRUE ?

@bborgesr
Copy link
Contributor

bborgesr commented Jan 4, 2017

Yep, that's right. See the documentation in observeEvent to learn about the new arguments and let me know if anything is unclear. Otherwise, I think we can close this issue. Thanks for reaching out and helping us make this a better function!

@sinacek
Copy link
Author

sinacek commented Jan 4, 2017

I looked in to documentation, but I missed it there. https://shiny.rstudio.com/reference/shiny/latest/observeEvent.html
Thanks a lot for your attitude and your help!

@sinacek sinacek closed this as completed Jan 4, 2017
@bborgesr
Copy link
Contributor

bborgesr commented Jan 4, 2017

Yeah, the documentation on the website is for the latest released version, not for the latest development version, which is why it is not up-to-date. But we're rolling out a new Shiny release soon, so you can count on this being out on CRAN within a few weeks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants