---
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.

<hr>
<br>

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:")
```

<br>

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!"*

<br>

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()`.

<br>
<hr>

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)
```
