The Introduction to shinyvalidate only talks about creating a single InputValidator object per app1. That may suffice for simpler apps, but more complex apps have multiple reasons for wanting to create multiple input validators.

This article discusses some of those more advanced use cases. But first, we need to introduce a couple of InputValidator features that support these advanced uses.

Advanced InputValidator features

Child validators

Hopefully you know by now that you can add rules to InputValidator objects, via the add_rule() method. What you may not know is that you can also add entire InputValidator objects to each other.

parent_iv$add_validator(child_iv)

Each InputValidator object can have any number of child InputValidator objects. It’s also fine for a child InputValidator object to have children of its own.

If child_iv$is_valid() returns FALSE, then parent_iv$is_valid() will also return FALSE—in other words, an input validator is only valid if all of its child validators are valid. And when parent_iv$enable() is called, real-time validation feedback is enabled not only for its own rules, but for the rules of the child validators, and their children, and so on.

I’m not sure, however, it makes sense for one InputValidator object to be added as a child to two different parents. Currently, {shinyvalidate} doesn’t stop you from doing this, but I can’t be sure there aren’t strange edge cases that might emerge from doing so.

Conditional validation

Another feature we’ll need is the ability to temporarily turn validation rules on and off, depending on some reactive input or state. We’ll see below where this comes in handy; for right now I’ll just describe how the feature works.

The InputValidator object has a condition method for this purpose: pass it either a formula or a zero-argument function that returns TRUE when the validation rules should be observed, and FALSE when they should be suspended.

Here are three equivalent ways to apply the isTRUE(input$filter_by_date) condition to iv:

# As formula
iv$condition(~ isTRUE(input$filter_by_date))

# As anonymous function
iv$condition(function() { isTRUE(input$filter_by_date) })

# As named function--or in this case, named reactive expression
filtering_by_date <- reactive({
  isTRUE(input$filter_by_date)
})
iv$condition(filtering_by_date)

The effect of all three variants is the same: if input$filter_by_date is TRUE, then iv will function as normal. But if input$filter_by_date is not TRUE, then iv acts as if all of its rules are passing.

Note that I said “as if all of its rules are passing”, not “as if it has no rules” or “as if iv doesn’t even exist”. If a validator is enabled and has failing rules, there will be validation errors displayed to the user. If the condition then becomes FALSE, these errors will be dismissed. This is why “as if all of its rules are passing” matters; the validator doesn’t just stop validating, it dismisses existing errors for inputs that it has rules for.

To be clear, it’s not the case that you call the condition() method when you want to start or stop bypassing the validator’s rules. Rather, you call the condition() method just once per InputValidator, passing it the logic that determines whether the rules should be in effect or not, and trust that Shiny and {shinyvalidate} will invoke that logic as often as is necessary.

Advanced validation scenarios

Now that we’ve covered these two new features, we can start discussing some more advanced use cases where multiple InputValidator objects are needed.

Multiple independent forms

Some Shiny apps may have more than one totally independent form, that just happen to be rendered within the same app. In these cases, you don’t want to let validation errors in Form A prevent you from being able to submit Form B.

The solution to this is straightforward: create a separate InputValidator for each set of related inputs, making sure to use a different variable name for each. By convention, these variables should be suffixed with _iv.

signup_iv <- InputValidator$new()
signup_iv$add_rule("email", sv_required())

recalculate_iv <- InputValidator$new()
recalculate_iv$add_rule("sample_count", sv_integer())
recalculate_iv$add_rule("sample_count", ~ if (. <= 0) "Must be positive")

[source, live demo] TODO

Now, reactives relating to signup can use signup_iv, and reactives relating to recalculation can use recalculate_iv. These two validator objects have no parent/child relationship; they’re just two independent objects that have nothing in common besides existing in the same environment.

Note that two independent validators may not have any overlap in the sets of inputs that they validate. In other words, since signup_iv$add_rule("email", ...) has been called, you cannot then do recalculate_iv$add_rule("email", ...), since that could leave the validators with conflicting opinions about whether the email input is valid.

Conditional subforms

Some apps need different subsets of their validation rules to be active at different times, depending on actions that the user takes. Consider this UI, that asks the user to choose between several ways of providing a profile picture: taking a webcam picture, uploading an image file, or looking up a Gravatar by email address.

[screencast]

[source, live demo] TODO

The Gravatar email address input is sv_required(), because we don’t want to let users submit the Gravatar option without it. But if the user has “Take selfie” currently selected, then the Gravatar email address is not relevant, and the validation rule should be ignored.

We can achieve this by breaking our validation rules into groups using child validators, and then attaching the appropriate conditional to each child validator.

gravatar_iv <- InputValidator$new()
gravatar_iv$condition(~ input$mode == "gravatar")
gravatar_iv$add_rule("email", sv_required());

Unlike the scenario described above in Multiple independent forms, the individual validator objects associated with each mode–selfie_iv, upload_iv, and gravatar_iv–do not represent independent forms; rather, they all are tied to the same Submit button. We handle this by introducing one more InputValidator object, this time representing the overall form, and adding our three validator objects to it as children.

iv <- InputValidator$new()
iv$add_validator(selfie_iv)
iv$add_validator(upload_iv)
iv$add_validator(gravatar_iv)

Validating Shiny modules

{shinyvalidate} is designed to work with Shiny modules.

Shiny modules provide a way to break up the logic of a Shiny app into smaller, more modular pieces that can each be reasoned about independently. That is the same job of regular R functions, but Shiny modules distinguish themselves versus regular R functions by providing namespacing for Shiny input and output IDs.

For example, in a module server function executing with an id (i.e., namespace) of "one", the expression input$x is actually equivalent to an outside-the-module expression of input$`one-x`.

Similarly, when iv$add_rule("x", ...) is called, the environment is checked to see if we’re currently in a Shiny module server function; if so, then the ID "x" is treated as being in the current namespace ("one-x"). In other words, iv$add_rule() is namespace-aware.

Note that this namespace resolution is not performed when the input validator object itself is created, but rather, whenever a rule is added. This allows a single input validator object to collect rules across multiple modules.

This gives us a lot of flexibility as we try to figure out how to combine input validators with modules—so much so, in fact, that it’s too early at the time of this writing to clearly identify “best practices”. Consider the following to be merely guidelines, and please file GitHub issues as you encounter limitations of the various approaches.

Self-contained validation

Imagine a Shiny module that contains an entire form, submission button and all. The module’s inputs need to be validated and display feedback, and the module’s reactives and outputs need to check is_valid(). Code outside the module, however, doesn’t need to influence the validation in any way, nor query is_valid().

In this case, the module server function can create its own local InputValidator object and use it internally.

library(shiny)
library(shinyvalidate)

# Module UI
contact_us_ui <- function(id) {
  ns <- NS(id)
  actionButton(ns("contact_us"), "Contact Us", class = "btn-sm")
}

# Module server
contact_us <- function(id) {
  moduleServer(id, function(input, output, session) {
    ns <- session$ns
    
    iv <- InputValidator$new()
    
    iv$add_rule("email", sv_required())
    iv$add_rule("email", sv_email())
    
    iv$add_rule("message", sv_required())
    iv$add_rule("message",
      ~ if (nchar(.) > 140) paste("Maximum length exceeded by", nchar(.) - 140))
    
    observeEvent(input$contact_us, {
      showModal(
        modalDialog(size = "s", easyClose = FALSE, title = "Contact Us",
          footer = NULL,
          tagList(
            textInput(ns("email"), "Your email"),
            textAreaInput(ns("message"), "Message", rows = 4),
            helpText("Maximum 140 characters"),
            div(class = "text-right",
              actionButton(ns("cancel"), "Cancel"),
              actionButton(ns("send"), "Send email", class = "btn-primary")
            )
          )
        )
      )
    })
    
    close <- function() {
      removeModal()
      iv$disable()
    }
    
    observeEvent(input$send, {
      iv$enable()
      if (iv$is_valid()) {
        close()
        showNotification("Message sent (not really)", type = "message")
      }
    }, ignoreInit = TRUE)
    
    observeEvent(input$cancel, {
      close()
    }, ignoreInit = TRUE)
  })
}

ui <- fluidPage(style = "padding-top: 12px; padding-bottom: 120px;",
  contact_us_ui("contact")
)

server <- function(input, output, session) {
  contact_us("contact")
}

shinyApp(ui, server)

Source, Live demo

Module as sub-form

Other Shiny modules may have a more transparent relationship with a surrounding form. Consider a Shiny module representing a single input, that returns the value of that input to the caller. It’s reasonable to expect the Shiny module to know how to determine whether the user’s input is valid, but the module shouldn’t need to know anything about the context in which that validation happens. Should validation errors be shown right away, or deferred until a Submit button is pressed—and if so, what button is that? Are there downstream reactive expressions and observers that need to guard against invalid data? These kinds of questions need to be concerns of the caller of the module, not the caller itself.

What this means, concretely, is that a Shiny module that contains an input (or multiple inputs) that need to be consumed by the caller, should call iv$add_rule() for its own inputs but generally leave iv$enable() or iv$condition() to the caller.

library(shiny)
library(shinyvalidate)

# Module UI
email_ui <- function(id, label = "Email", value = NULL) {
  ns <- NS(id)
  textInput(ns("email_address"), label = label, value = value)
}

# Module server
email <- function(id, required = TRUE) {
  moduleServer(id, function(input, output, session) {
    
    # Create and populate InputValidator object
    iv <- InputValidator$new()
    if (required) {
      iv$add_rule("email_address", sv_required())
    }
    iv$add_rule("email_address", sv_email())

    # Allow caller to reset the input
    reset <- function() {
      updateTextInput(session, "email_address", value = "")
    }

    # Return value accessor, InputValidator, and reset function to the caller
    list(
      value = reactive(input$email_address),
      iv = iv,
      reset = reset
    )
  })
}

# App UI
ui <- fluidPage(
  email_ui("email", "Email address")
)

# App server
server <- function(input, output, session) {
  email_result <- email("email")
  email_result$iv$enable()
}

shinyApp(ui, server)

Source, Live demo

Caller-provided validation rules for module inputs

In the previous section, we assumed that a Shiny module knows everything there is to know about its own inputs’ validation rules. What if the module wants the caller to help decide if an input is valid?

Let’s consider a password_input Shiny module that provides the UI and logic for this kind of subform, the kind you’ve filled out a hundred times online:

[screenshot: two password fields]

This Shiny module can and should include validation rules to ensure that the “Password” and “Password (confirm)” fields always match. But there are other possible validation rules that might or might not be important for any particular app: Is there a minimum/maximum password length? Are passwords required to contain at least one number, uppercase letter, and/or special character? What special characters are allowed? These all feel like the responsibility of the module caller to decide, not the module implementation.

One bad but obvious way to do it would be for the caller to add its own rules directly to the InputValidator object returned by password_input:

server <- function(input, output, session) {
  password_result <- password_input("password")

  # Adding a rule for one of password_input's inputs, from outside
  # of password_input. DON'T DO THIS!
  password_result$iv$add_rule(NS("password", "pw1"), ~ {
    # Custom validation logic
    if (!grepl("[0-9]", pw) || !grepl("[A-Z]", pw)) {
      "Must include a number and an upper-case character"
    } else if (nchar(pw) < 8) {
      "Must be at least 8 characters"
    }
  })
}

The reason this is bad is because it requires the caller to know the input IDs used within the module (NS("password", "pw1") in this case). This violates a module boundary that we should try hard to keep sacred!

Instead, the module should take a validation rule—i.e., the second argument to add_rule—as a parameter. (If multiple inputs need to be validated, then one such parameter should be created for each input.)

password_input <- function(id, password_rule = NULL) {
  moduleServer(id, function(input, output, session) {
    iv <- InputValidator$new()
    iv$add_rule("pw1", sv_required())
    iv$add_rule("pw2", sv_required())
    iv$add_rule("pw2", ~ if (!identical(., input$pw1)) "Passwords do not match")
    
    # Add custom rule passed in by caller
    if (!is.null(password_rule)) {
      iv$add_rule("pw1", password_rule)
    }
  })
}

server <- function(input, output, session) {
  # Initialize the module, passing custom validation logic via password_rule
  password_result <- password_input("password", password_rule = function(pw) {
    if (!grepl("[0-9]", pw) || !grepl("[A-Z]", pw)) {
      "Must include a number and an upper-case character"
    } else if (nchar(pw) < 8) {
      "Must be at least 8 characters"
    }
  })
}

Here is a complete example that illustrates this technique.

library(shiny)
library(shinyvalidate)

#' A Shiny module UI function that pairs with password_input().
#' 
#' @param id Module instance ID. The same value must be passed to the matching
#'   password_input() call.
#' @param value The default value.
password_input_ui <- function(id, value = NULL) {
  ns <- NS(id)
  
  tagList(
    passwordInput(ns("pw1"), "Password", value = value),
    passwordInput(ns("pw2"), "Password (confirm)", value = value)
  )
}

#' A Shiny module server function that pairs with password_input_ui().
#' 
#' Includes
#' 
#' @param id Module instance ID. The same value must be passed to the matching
#'   password_input_ui() call.
#' @param required If `TRUE` (the default), the returned validator will include
#'   a rule that ensures a password is provided.
#' @param password_rule Optional validation function or formula that can impose
#'   further validation rules on the password. Like all validation functions and
#'   formulas, the return value should be either a string describing an error,
#'   or `NULL` if the input value is acceptable.
password_input <- function(id, required = TRUE, password_rule = NULL) {
  moduleServer(id, function(input, output, session) {
    iv <- InputValidator$new()
    if (isTRUE(required)) {
      iv$add_rule("pw1", sv_required())
    }
    iv$add_rule("pw2", ~ if (!identical(., input$pw1)) "Passwords do not match")
    
    # Add custom rule passed in by caller
    if (!is.null(password_rule)) {
      iv$add_rule("pw1", password_rule)
    }
    
    list(
      value = reactive({
        if (iv$is_valid()) {
          input$pw1
        }
      }),
      reset = function() {
        updateTextInput(session, "pw1", value = "")
        updateTextInput(session, "pw2", value = "")
      },
      iv = iv
    )
  })
}

ui <- fluidPage(
  password_input_ui("password"),
  actionButton("continue", "Continue")
)

server <- function(input, output, session) {
  # Initialize the module, passing custom validation logic via password_rule
  password_result <- password_input("password", password_rule = function(pw) {
    if (!grepl("[0-9]", pw) || !grepl("[A-Z]", pw)) {
      "Must include a number and an upper-case character"
    } else if (nchar(pw) < 8) {
      "Must be at least 8 characters"
    }
  })
  
  iv <- InputValidator$new()
  iv$add_validator(password_result$iv)
  iv$enable()
  
  observeEvent(input$continue, {
    if (iv$is_valid()) {
      showModal(modalDialog(
        "Success, password hash is ",
        tags$code(digest::digest(password_result$value(), "sha256"))
      ))
      password_result$reset()
    }
  })
}

shinyApp(ui, server)

Source, Live demo


  1. To be precise, it’s actually one InputValidator object per session, not per app.↩︎