1

I have been trying to implement a R Shiny app based on DT. More specifically, my idea is to use an external numericInput to filter numerical fields and the search boxes embedded in the datatable for the other fields. What is critical in my exercise is to optionally perform textual search using regex expression for exact match. Also, for such exact search the user is not supposed to see the special expressions. For instance, the user simply types "abc" and in the background the string is handled as "^abc$".

Everything seems to work fine if I first use the external filters and then the embedded one. Vice versa, the regex expression are not activated and I cannot perform the exact match.

This works just fine, with the regex expression correctly handled in the background. enter image description here

This doesn't work instead, as the 'DT::updateSearch' updates the filter based on the numericInput and the JS script is not execute as that is triggered only when we type on the search box. enter image description here

Is there a way to trigger a drawCallback, for instance, that can update the behaviour I implemented in the JS function below?

library(shiny)
library(DT)

ui <- fluidPage(
  fluidRow(column(3, strong('num'), style='margin-bottom:0px;')),
  fluidRow(column(3, numericInput("missing_min", "", NULL, min = 1, max = 100)), column(3, numericInput("missing_max", "", NULL, min = 1, max = 100)), style='margin-top:0px;'),
  checkboxInput("regex_check_num", "activate regex search", FALSE),
  fluidRow(
    dataTableOutput("dataTable")
  )
)


# default global search value
if (!exists("default_search")) default_search <- ""
# default column search values
if (!exists("default_search_columns")) default_search_columns <- NULL


server <- function(input, output) {
  
  proxy <- DT::dataTableProxy('dataTable')
  
  observeEvent(c(input$missing_min, input$missing_max),{
    isolate({
      # update global search and column search strings
      default_search <- input$dataTable_search
      default_search_columns <- c(input$dataTable_search_columns)
      
      # update integer value 
      if (is.na(input$missing_min))
        default_search_columns[4] <- paste0('...', input$missing_max)
      else if (is.na(input$missing_max)){
        print('here')
        default_search_columns[4] <- paste0(input$missing_min, '...')
      }
      else
        default_search_columns[4] <- paste0(input$missing_min, '...', input$missing_max)
      
      # update the search terms on the proxy table (see below)
      proxy %>% DT::updateSearch(keywords =
                               list(global = default_search, columns = default_search_columns))
      
    })
  }, ignoreInit = T)
  
  output$dataTable <- DT::renderDataTable({
    
    dat <- data.frame(
      car = c("Mazda", "Mazda RX4", "Mazda RX4 Wag", "Ford", "Mercedes"),
      day = Sys.Date() + 0:4,
      num = as.integer(c(1,2,3,4,5)),
      hidden_num = as.integer(c(1,2,3,4,5))
    )
    
    js <- c(
      "function(settings){",
      "  $.fn.dataTable.ext.errMode = 'none';",
      "  var instance = settings.oInstance;",
      "  var table = instance.api();",
      "  var $inputs = instance.parent().find('.form-group input');  console.log('ready')",
      "  $inputs.off('keyup search input').on('keyup', function(){",
      "    var index = $inputs.index(this);", # add 1 if rownames column included
      "    var title = table.column(index).header();",
      "    title = $(title).html()",
      "    var val = $(this).val(); console.log(val);",
      "    if (val!== '' & document.getElementById('regex_check_num').checked){",
      "     var keyword = '^' + val + '$';", # For exact match
      "     table.column(index).search(keyword, true, false).draw();",
      " }",
      "else {table.column(index).search(val).draw(); console.log('done');}});",
      "}"
    )
    
    datatable(
      dat, 
      filter = list(position="top", plain=F, clear = F),
      rownames = FALSE,
      
      options = list(
        # drawCallback = JS(js_update), # TODO
        searchCols = default_search_columns
        , initComplete = JS(js)
        , columnDefs = list(
            list(targets = c(2), searchable = F),
            list(targets = c(3), visible = F)
            )
        
        ), style = 'bootstrap4', class = 'table-bordered'
    )
  }, server = T)
  
}

shinyApp(ui = ui, server = server)
xander85
  • 11
  • 2
  • The AND operator in JavaScript is `&&`, not `&`. – Stéphane Laurent Mar 20 '23 at 17:43
  • @StéphaneLaurent thank you for the feedback. For some reason both & and && led to the same behaviour. – xander85 Mar 20 '23 at 18:53
  • Why don't you use the [search builder](https://laustep.github.io/stlahblog/posts/DT_SearchBuilder.html)? – Stéphane Laurent Mar 20 '23 at 20:47
  • related: https://stackoverflow.com/a/56541562/1100107 – Stéphane Laurent Mar 21 '23 at 02:11
  • @StéphaneLaurent this is indeed very similar to what I implemented. – xander85 Mar 21 '23 at 08:54
  • But I don't understand why you want to use the `drawCallback`. It is refreshed each time the table is redrawn, do you need a refresh? I don't get your question yet. – Stéphane Laurent Mar 21 '23 at 09:04
  • I tried your code and it seems to work. What is the problem? I don't understand. – Stéphane Laurent Mar 21 '23 at 09:20
  • Indeed I am not even sure drawCallback is the proper way to solve my issue. The limitation of my code is that when I run 'DT::updateSearch' to update the filter based on the numericInput then the JS script is not execute as that is triggered only when we type on the search box. I have added some animations in my questions to show the correct behaviour. In the animation you can see that the initial filtering on the 'car' field is not working as expected when we filter on the 'num' field later. Sorry in advance if my question sounds a bit cryptic :) – xander85 Mar 21 '23 at 10:20
  • But the num field is not in `$inputs`. Is it? – Stéphane Laurent Mar 21 '23 at 10:24
  • I copied indeed the num field into an hidden 'hidden_num' (to hide the "x...y" string in the filter box). The '$input' filters then on hidden_num :) – xander85 Mar 21 '23 at 10:34
  • Maybe I still misunderstand :-( I meant the numeric inputs min and max are not in the `$inputs` of the JavaScript code. – Stéphane Laurent Mar 21 '23 at 10:56
  • if you do `console.log($inputs)` you just get the three visible filtering fields. – Stéphane Laurent Mar 21 '23 at 10:58
  • The min and max inputs are actually shiny objects. I don't think we can actually observe those in JS directly. My understanding is that they are passed to dt search box via a special function "updateSearch". Maybe my JS could be modified in some way to override the default behaviour of dt when searches are performed (i.e. not only when keyup)? – xander85 Mar 21 '23 at 12:28
  • Of course you can observe them in JS. But maybe you should try the [search event](https://datatables.net/reference/event/search). – Stéphane Laurent Mar 21 '23 at 12:51
  • Hmm... not sure how to fit this in :( The search event is used to trigger a global search. What am I missing? :( – xander85 Mar 21 '23 at 15:05
  • The search event does not trigger anything: this is an *event*. The search event *is triggered* when a search is performed. But indeed, I don't see how to use it finally. I would observe the numeric inputs instead. – Stéphane Laurent Mar 21 '23 at 15:28
  • I get the feeling we are going on the right direction though :) Do you have any idea how to modify the JS function to be triggered whenever there is a change in missing_min or missing_max? – xander85 Mar 21 '23 at 17:04

0 Answers0