--- title: "Customizing shinyfilters" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Customizing shinyfilters} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ``` # Introduction shinyfilters is built to be fully customizable. This article demonstrates the ways in which you can customize shinyfilters. 1. [Extending shinyfilters](#extending-shinyfilters) 2. [Overwriting shinyfilters](#overwriting-shinyfilters) # Motivation Let's say you have an S7 class, [`Person`](https://github.com/RConsortium/S7/blob/v0.2.0/vignettes/classes-objects.Rmd#L324): ```{r, message=FALSE} 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`: ```{r} 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 ``` 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: ```{r, message=FALSE, error=TRUE} library(shinyfilters) library(shiny) filterInput(people, inputId = "people", label = "Pick a person:") ``` 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`: ```{r} 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`... ```{r, error=TRUE} filterInput(people, inputId = "people", label = "Pick a person:") ``` ... 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: ```{r} 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()`: ```{r, results='asis'} 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`: ```{r, echo=FALSE} if (!requireNamespace("shinyWidgets", quietly = TRUE)) { message("shinyWidgets not installed; code chunks will not be evaluated.") knitr::opts_chunk$set(eval = FALSE) } ``` ```{r} library(shinyWidgets) method(filterInput, class_numeric) <- function(x, ...) { call_filter_input(x, numericRangeInput, ...) } ``` Now when you call `filterInput()` on a `character` vector, `filterInput()` will call `shinyWidgets` instead of `shiny`: ```{r} filterInput(0:10, inputId = "number", label = "Pick a number:") ``` 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: ```{r} 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)) ) } ``` Now, our overwritten `filterInput()` will work as intended: ```{r} filterInput(0:10, inputId = "number", label = "Pick a number:") ``` ```{r, echo=FALSE} knitr::opts_chunk$set(eval = TRUE) ``` # 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: ```{r} method(filterInput, People) <- function(x, ...) { shiny::selectizeInput( choices = full_names(x), ... ) } 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: ```{r, error=TRUE} filterInput( people, inputId = "people", label = "Pick a person:", choices = full_names(people) ) ``` >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: ```{r, error=TRUE} method(filterInput, People) <- function(x, ...) { call_filter_input(x, shiny::selectizeInput, ...) } filterInput( people, inputId = "people", label = "Pick a person:", choices = full_names(people) ) ``` ```{r, echo=FALSE} unloadNamespace("shinyfilters") rm(filterInput) rm(args_filter_input) ```