shinyfilters is built to be fully customizable. This article demonstrates the ways in which you can customize shinyfilters.
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 filterInput() involves two steps:
filterInput().args_filter_input()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().
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():
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.
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:
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().
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:
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 argumentsError 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.