How to dynamically generate and capture user input from modalDialog within a module?

Background: I am creating a Donor/Sample registration application. The workflow is as such:

  1. Users select which Vendor the Donors/Samples are from.

  2. Users upload a file (using datamods::import_file_server()).

  3. Run some validation against that file (ie. x number of rows, y number of columns, etc., using datamods::validation_server())

  4. Select pre-existing or Create a new Field Mapping.

    4a) a Field Mapping is a mechanism for users to map the uploaded file columns to our database table columns.

    4b) If Create New, a modalDialog window should pop up, showing a 2 column datatable, one column for File column names, one column of selectInput()‘s that are populated with our database table fields (columns).

I have this set up in such a way that a registration_module handles 1-3, and then within that, I have a nested fieldmapping_module which takes as input:

  1. the (validated) file data
  2. the vendor selection
  3. and the database columns

Problem: I cannot seem to make the dynamically generated selectInput()‘s “visible” to Shiny. Below is the fieldmapping_module code.

### FieldMapping module ####

fieldmappingUI <- function(id) {
  
  tagList(
    div(
      column(8,
             selectInput(
               inputId = NS(id, "fieldmapping_selection"),
               label = "Select Field Mapping",
               choices = c("Choice 1", "Choice 2", "Choice 3") 
             ),
      ),
      column(4,
             shinyWidgets::actionBttn(
               inputId = NS(id, "create_new_fieldmapping_btn"),
               label = "Create New",
               icon = icon("file-alt"),
               size = "sm"
             )
      ),
      style = "display:inline-block",
      class = "form-group shiny-input-container")
  )
}


fieldmappingServer <- function(id, file_data, vendor_selection, db_cols) {
  stopifnot(is.reactive(file_data))
  stopifnot(is.reactive(vendor_selection))
  
  moduleServer(id, function(input, output, session) {
    
    ns <- session$ns
  
    #observe for creation of new FieldMappings
    observeEvent(input$create_new_fieldmapping_btn, {
      
      fieldmapping_table <- data.frame(
        "File Columns" = colnames(file_data()),
        "DB Field Mapping" = rep("", ncol(file_data()))
      )
      
      #browser()
      
      for(i in seq_len(nrow(fieldmapping_table))) {
        fieldmapping_table[i,"DB.Field.Mapping"] <- as.character(selectInput(
          inputId = glue::glue("fieldmap_select_{fieldmapping_table$File.Column[i]}"),
          label = NULL,
          choices = db_cols
        ))
      }
      #browser()

      #display the table
      showModal(modalDialog(
        
        renderDataTable({
          DT::datatable(fieldmapping_table,
                        escape = 2,
                        selection = "none",
                        filter = 'none',
                        options = list(
                          dom = 't' 
                          ),
                        callback = JS("table.rows().every(function(i, tab, row) {
                                                       var $this = $(this.node());
                                                       $this.attr('id', this.data()[0]);
                                                       $this.addClass('shiny-input-slider-input');
                                                         });
                                                       Shiny.unbindAll(table.table().node());
                                                       Shiny.bindAll(table.table().node());")
          )

            
          }),
        title = "New Field Mapping",
        footer = tagList(
          actionButton(ns("submit_fieldmapping"), label = "Submit", icon = icon("paper-plane")),
          modalButton(label = "Close", icon = icon("window-close"))
          )
      ))
      
    })
   
    observeEvent(input$submit_fieldmapping, {
      browser()
    })
    

  })
  
}

Excepted behavior: For example, say I have uploaded a file with 3 columns: Subject_ID, Col_A, and Col_B, and it has passed validation (done in the registration_module, not shown).

When I hit the submit button of the modalDialog, and the app is paused due to the browser() call, I am excepting to have access to input$fieldmap_select_[column name](ex: input$fieldmap_select_Subject_ID), but I don’t. I thought the custom JS callback would achieve this, as it seems to have worked here (and other code/apps I’ve come across while Googling).

On the browser() pause, if I enter input into the console to see the list of inputs, I indeed do see the dynamically generated inputs I created (the first three), but they are all NULL, despite having “set” them in the modalDialog window. enter image description here

I am not too well versed in Javascript/Shiny interaction, but would appreciate any help I could get with this! What am I doing wrong?

(cross-posted on RStudio community forum)

Answer

Figured it out. I needed to include the server-side ns function in the dynamic generation of the input fields. ie:

for(i in seq_len(nrow(fieldmapping_table))) {
        fieldmapping_table[i,"DB.Field.Mapping"] <- as.character(selectInput(
          inputId = ns(glue::glue("fieldmap_select_{fieldmapping_table$File.Column[i]}")),
          label = NULL,
          choices = db_cols
        ))
      }

notice the ns() call around the inputId.