javascriptrshinyshinyjs

How to replace isolate() with another reactivity management function that serves the same purpose in R Shiny?


The below example code has 3 linked user input matrixes: the first, base_input, allows the user to make static inputs and the next 2 user input matrixes, var_1_input and var_2_input (collectively, "var_x_input"), are reactively fed the values from base_input and the slider input for time horizon, and allow the user to alter the time dimension in their respective left-most user input columns labeled "X". The image below shows the data flows. How can I replace the one isolate() in this code with other reactivity-management code?

The problem I'm having is the isolate() conflicts with the js code for managing decimals. The js code ensures that a minimum of 2 decimal places are shown in the matrix input cells. When activating the js code (commented-out below), the reactivity flow between base_input and the var_x_input matrixes no longer works (meaning for example an input in base_input[1,1] needs to be instantly reflected in the upper right cell of var_1_input), although the decimals work perfectly. If I remove the isolate(), then another undesirable happens: changing a value in one cell in base_input resets both of the var_x_input matrixes instead of just the var_x_input matrix linked to base_input. The isolate() ensures that each var_x_input is indepenendently linked to its corresponding cell in base_input.

I'm trying to get all 3 features working: downstream reactivity between base_input and the var_x_input matrixes; formatting of specified user input cells with a minimum of 2 decimal places (per the js code); and changing the base_input cell for one var_x_input should reset only that var_x_input matrix and not all of the var_x_input matrixes.

enter image description here

Code:

library(shinyMatrix)
library(shiny)
library(shinyjs)

# js <- "
# $(document).on('shiny:bound', function(event) {
#   var $elem = $(event.target);
#   if($elem.hasClass('vue-input')) {
#     $elem.find('td').each(function() {
#       var $td = $(this);
#       var columnIndex = $td.index();
#       var $table = $td.closest('.vue-input');
#       var tableId = $table.attr('id');
#       var isVarInput = tableId.startsWith('var_') && tableId.endsWith('_input');
#       var text = $td.find('span').text();
#       var num = parseFloat(text);
# 
#       if (!isNaN(num)) {
#         if (isVarInput && columnIndex == 0) {
#           // Format with 0 decimal places for the first column of var_x_input
#           $td.find('span').html(num.toFixed(0));
#         } else {
#           // Use 2 decimal places for the second column of var_x_input and other cases
#           var decimalPlaces = (text.split('.')[1] || []).length;
#           var fixed = decimalPlaces < 2 ? 2 : decimalPlaces;
#           $td.find('span').html(num.toFixed(fixed));
#         }
#       }
#     });
#   }
# });
# 
# $(document).ready(function() {
#   $('body').on('focusout', '.vue-input table td input', function(e) {
#     var $td = $(this).parent();
#     var columnIndex = $td.index();
#     var $table = $td.closest('.vue-input');
#     var tableId = $table.attr('id');
#     var isVarInput = tableId.startsWith('var_') && tableId.endsWith('_input');
# 
#     var interval = setInterval(function() {
#       if ($td.children().is('span')) {
#         clearInterval(interval);
#         var $span = $td.find('span');
#         var text = $span.text();
#         var num = parseFloat(text);
# 
#         if (!isNaN(num)) {
#           if (isVarInput && columnIndex == 0) {
#             // Format with 0 decimal places for the first column of var_x_input
#             $span.html(num.toFixed(0));
#           } else {
#             // Use 2 decimal places for the second column of var_x_input and other cases
#             var decimalPlaces = (text.split('.')[1] || []).length;
#             var fixed = decimalPlaces < 2 ? 2 : decimalPlaces;
#             $span.html(num.toFixed(fixed));
#           }
#         }
#       }
#     }, 50);
#   });
# });
# "

matInputBase <- function(name) {
  matrixInput(name,
              value = matrix(rep(0.20,2),2, 1,dimnames = list(c("A","B"), NULL)),
              rows = list(extend = FALSE, names = TRUE),
              cols = list(extend = FALSE, names = FALSE, editableNames = FALSE),
              class = "numeric")
}

matInputVary <- function(name, x,y) {
  matrixInput(
    name,
    value = matrix(c(x, y), 1, 2, dimnames = list(NULL,c("X","Y"))),
    rows = list(extend = TRUE, names = FALSE),
    cols = list(extend = TRUE, delta = 0, names = TRUE, editableNames = FALSE),
    class = "numeric"
  )
}

ui <- fluidPage(
  useShinyjs(),
  # tags$head(tags$script(HTML(js))),
  sliderInput("periods","Time window (W):", min = 1, max = 120, value = 60),
  h5(strong("Variable (Y) over window (W):")),
  matInputBase("base_input"),
  uiOutput("Vectors")
)

server <- function(input, output, session) {
  prev_base_input <- reactiveValues(data = matrix(NA, nrow = 2, ncol = 1))

  observeEvent(input$base_input, {
    for (i in 1:2) {
      if (is.na(prev_base_input$data[i,1])||input$base_input[i,1]!=prev_base_input$data[i,1]){
        updateMatrixInput(
          session,
          paste0("var_", i, "_input"),
          value = matrix(c(input$periods,input$base_input[i,1]),1,2,dimnames=list(NULL,c("X","Y")))
        )
        prev_base_input$data[i, 1] <- input$base_input[i, 1]
      }
    }
  })
  
  output$Vectors <- renderUI({
    input$resetVectorBtn
    varNames <- c("A", "B")
    tagList(
      lapply(1:2, function(i) {
        list(
          h5(strong(paste("Adjust", varNames[i], "(Y) at time X:"))),
          matInputVary(paste0("var_", i, "_input"), input$periods, isolate(input$base_input[i,1]))
        )
      })
    )
  })
}

shinyApp(ui, server)

Solution

  • library(shiny)
    library(shinyMatrix)
    
    js <- "
    $(document).ready(function() {
      $('#base_input').on('shiny:bound', function(event) {
        var matrix = $(this).data('values');
        $(this).find('td:eq(0) span').text(matrix[0][0].toFixed(2));
        $(this).find('td:eq(1) span').text(matrix[1][0].toFixed(2));
      });
      $(document).on('shiny:updateinput', function(e) {
        var $target = $(e.target);
        if($target.hasClass('vue-input')) {
          var matrix = e.message.value.data;
          $target.find('td:eq(0) span').text(matrix[0][0].toFixed(2));
          $target.find('td:eq(1) span').text(matrix[0][1].toFixed(2));
        }
      });
      $('#base_input').on('blur', 'table td input', function(e) {
        var $td = $(this).parent();
        var val = parseFloat($(this).val()).toFixed(2);
        var interval = setInterval(function() {
          if($td.children().is('span')) {
             clearInterval(interval);
             $td.find('span').text(val);
          }
        });
      });
    });
    "
    
    matInputBase <- function(name) {
      matrixInput(
        name,
        value = matrix(rep(20,2),2, 1,dimnames = list(c("A", "B"), NULL)),
        rows = list(extend = FALSE, names = TRUE),
        cols = list(extend = FALSE, names = FALSE, editableNames = FALSE),
        class = "numeric"
      )
    }
    
    matInputVary <- function(name, x,y) {
      matrixInput(
        name,
        value = matrix(c(x, y), 1, 2, dimnames = list(NULL,c("X","Y"))),
        rows = list(extend = TRUE, names = FALSE),
        cols = list(extend = TRUE, delta = 0, names = TRUE, editableNames = FALSE),
        class = "numeric"
      )
    }
    
    ui <- fluidPage(
      tags$head(
        tags$script(HTML(js))
      ),
      sliderInput("periods","Time window (W):", min = 1, max =10, value = 5),
      h5(strong("Variable (Y) over window (W):")),
      matInputBase("base_input"),
      uiOutput("Vectors")
    )
    
    server <- function(input, output, session) {
      prev_base_input <- reactiveValues(data = matrix(NA, nrow = 2, ncol = 1))
      
      observeEvent(input$base_input, {
        for (i in 1:2) {
          if (is.na(prev_base_input$data[i,1])||input$base_input[i,1]!=prev_base_input$data[i,1]){
            updateMatrixInput(
              session,
              paste0("var_", i, "_input"),
              value = matrix(c(input$periods,input$base_input[i,1]),1,2,dimnames=list(NULL,c("X","Y")))
            )
            prev_base_input$data[i, 1] <- input$base_input[i, 1]
          }
        }
      }, ignoreInit = FALSE)
      
      output$Vectors <- renderUI({
        input$resetVectorBtn
        varNames <- c("A", "B")
        lapply(1:2, function(i) {
          list(
            h5(strong(paste("Adjust", varNames[i], "(Y) at time X:"))),
            matInputVary(paste0("var_", i, "_input"), input$periods, isolate(input$base_input)[i, 1])
          )
        })
      })
      
    }
    
    shinyApp(ui, server)