Skip to contents

Introduction

shinyfilters is built to be fully customizable. This article demonstrates the ways in which you can customize shinyfilters.

  1. Extending shinyfilters
  2. Overwriting shinyfilters

Motivation

Let’s say you have an S7 class, Person:

library(S7)

StringNonEmpty <- new_property(
    class = class_character,
    validator = function(value) {
        if (length(value) != 1 || is.na(value) || value == "") {
            return("must be a non-empty string")
        }
    }
)

Person <- new_class(
    name = "Person",
    properties = list(
        first_name = StringNonEmpty,
        last_name = StringNonEmpty
    )
)

And you want to combine a list of Person’s into a new class, People:

People <- new_class(
    name = "People",
    parent = class_list,
    constructor = function(...) new_object(list(...)),
    validator = function(self) {
        if (!all(vapply(self, S7_inherits, logical(1), class = Person))) {
            return("must be a list of `Person`'s")
        }
    }
)

people <- People(
    Person("Ross", "Ihaka"),
    Person("Robert", "Gentleman")
)
people
#> <People> List of 2
#>  $ : <Person>
#>   ..@ first_name: chr "Ross"
#>   ..@ last_name : chr "Ihaka"
#>  $ : <Person>
#>   ..@ first_name: chr "Robert"
#>   ..@ last_name : chr "Gentleman"

Now, in your Shiny app, you want to use filterInput() to select a Person from people; however, if you call filterInput() on people, you will get an error:

library(shinyfilters)
library(shiny)

filterInput(people, inputId = "people", label = "Pick a person:")
#> Error in as.vector(x, "character"): cannot coerce type 'closure' to vector of type 'character'

To allow filterInput() to be called on people, you can extend filterInput().

Extending shinyfilters

Extending filterInput() involves two steps:

  1. Define a method for filterInput().
  2. Define a method for args_filter_input()

Step 1: Define filterInput()

Defining a method for filterInput() involves dispatching the provided x to the appropriate shiny input function.

In this case, we want filterInput() to dispatch to a shiny::selectizeInput for People:

method(filterInput, People) <- function(x, ...) {
    call_filter_input(x, shiny::selectizeInput, ...)
}

It’s recommended that methods for filterInput() use call_filter_input(), as shown above. call_filter_input() prepares the arguments for the input function, then calls the provided input function with the prepared arguments.

Now, if we run filterInput() on people

filterInput(people, inputId = "people", label = "Pick a person:")
#> Error in as.vector(x, "character"): cannot coerce type 'closure' to vector of type 'character'

… we’ll still get an error.

To fix this error, you need to define a method for args_filter_input().

Step 2: Define args_filter_input()

args_filter_input() tells filterInput() how to convert x into the arguments it uses for the shiny input function.

To define args_filter_input(), write a method that returns a named list, representing the arguments passed to the selected input:

full_names <- new_generic("full_names", "x")
method(full_names, People) <- function(x) vapply(x, full_names, character(1))
method(full_names, Person) <- function(x) paste(x@first_name, x@last_name)

method(args_filter_input, People) <- function(x, ...) {
    list(choices = full_names(x))
}

Now you can call filterInput():

filterInput(people, inputId = "people", label = "Pick a person:")

Overwriting shinyfilters

Overwriting filterInput() is similar to extending filterInput(), except that when you overwrite, you replace an existing method. Use overwriting when you want to customize existing functionality.

Step 1: Overwrite filterInput()

Overwrite filterInput() when you want to customize the input function that is selected.

For example, let’s say you want to use shinyWidgets instead of shiny:

library(shinyWidgets)

method(filterInput, class_numeric) <- function(x, ...) {
    call_filter_input(x, numericRangeInput, ...)
}
#> Overwriting method filterInput(<integer>)
#> Overwriting method filterInput(<double>)

Now when you call filterInput() on a character vector, filterInput() will call shinyWidgets instead of shiny:

filterInput(0:10, inputId = "number", label = "Pick a number:")
to

However, this isn’t quite right. Notice how the range shows the same number twice. To fix this, we need to also overwrite args_filter_input().

Step 2: Overwrite args_filter_input()

Overwrite args_filter_input() when you want to modify the arguments passed to the selected input function.

For example, to allow numeric vectors to work with the shinyWidgets input function, we need to pass value as a length-two numeric vector:

method(args_filter_input, class_numeric) <- function(x, ...) {
    list(
        # Value should be a length-two vector, per ?numericRangeInput
        value = c(min(x, na.rm = TRUE), max(x, na.rm = TRUE))
    )
}
#> Overwriting method args_filter_input(<integer>)
#> Overwriting method args_filter_input(<double>)

Now, our overwritten filterInput() will work as intended:

filterInput(0:10, inputId = "number", label = "Pick a number:")
to

Why call_filter_input() ?

call_filter_input() exists to handle the arguments for the provided vector and selected input function.



You can skip the call to call_filter_input(), and in doing so, you skip the call to args_filter_input(). So, you’d need to handle the argument preparation inside your filterInput() method:

method(filterInput, People) <- function(x, ...) {
    shiny::selectizeInput(
        choices = full_names(x),
        ...
    )
}
#> Overwriting method filterInput(<People>)

filterInput(people, inputId = "people", label = "Pick a person:")


However, such an implementation is more bug-prone, and, increases the opportunity for confusing errors to emerge:

filterInput(
    people,
    inputId = "people",
    label = "Pick a person:",
    choices = full_names(people)
)
#> Error in selectInput(inputId, ..., selectize = FALSE, width = width): formal argument "choices" matched by multiple actual arguments

Error in … : formal argument “choices” matched by multiple actual arguments

“But I only provided choices once!”


Additionally, the user of your extension may themselves be extending args_filter_input() only, and not filterInput(). In such cases, they generally would expect call_filter_input() to be called, so that their extension of args_filter_input() would be picked up by your extension of filterInput().



For the best user experience, you should handle arguments in your extension. call_filter_input() exists for this purpose, handling the argument prep dynamically (via args_filter_input()) and sending informative errors:

method(filterInput, People) <- function(x, ...) {
    call_filter_input(x, shiny::selectizeInput, ...)
}
#> Overwriting method filterInput(<People>)

filterInput(
    people,
    inputId = "people",
    label = "Pick a person:",
    choices = full_names(people)
)
#> Error in error_input_args(x, names(args)): The argument `choices` is not supported in when used with `People` objects.