--- output: github_document --- ```{r setup, include=FALSE} knitr::opts_chunk$set(eval = FALSE) github_sha_link <- function(sha) { glue::glue("[{sha}](https://github.com/gadenbuie/js4shiny-frappeCharts/commit/{sha})") } get_last_sha <- function() { commit <- git2r::commits()[[1]] message(commit$summary) message(commit$sha) clipr::write_clip(glue::glue( "`r github_sha_link(\"{commit$sha}\")`" )) } ``` # Building a Shiny Input In this project, we're going to create a typing speed app using a custom Shiny input. The app will give users typing prompts, monitor their typing speed, and use a Frappe Chart line chart to show their speed over time as they type. ## Setup a folder for our app inside the frappeCharts package `r github_sha_link("113340074c3af9c2cdf46cd7787829d4ec56bfcf")` Create the directory `inst/shiny-input-app` and add `app.R` and `typing.js`. ```{r} usethis::use_directory("inst/shiny-input-app") file.create("inst/shiny-input-app/app.R") file.create("inst/shiny-input-app/typing.js") ``` ## Create a basic Shiny app with a typing area `r github_sha_link("ff3a962f7e6d16a75ebdb620aac0fdfc9949086e")` We'll start with typical Shiny inputs. ```r library(shiny) ui <- fluidPage( textAreaInput("typing", "Type here..."), verbatimTextOutput("debug") ) server <- function(input, output, session) { output$debug <- renderPrint(input$typing) } shinyApp(ui, server) ``` Run this app and type in the box. ## Create our own typingSpeedInput() `r github_sha_link("b7108029b58635652ce87f3e1ea9a2c5a6232020")` Use Shiny's `textAreaInput()` to get the template for our own `typingSpeedInput()` ```{r, eval=TRUE} shiny_text_input <- shiny::textAreaInput( "INPUT", "LABEL", placeholder = "PLACEHOLDER" ) cat(format(shiny_text_input)) ``` ```{r} typingSpeedInput <- function(inputId, label, placeholder = NULL) { .label <- label htmltools::withTags( div( class = "form-group typing-speed", label(class = "control-label", `for` = inputId, .label), textarea(id = inputId, class = "form-control", placeholder = placeholder) ) ) } ``` Two points: 1. Notice that I used `htmltools::withTags()`, which makes it easier to write multiple tags at once. But it has the downside of masking the `label` argument of `typingSpeedInput()`. Hence, the first line `.label <- label`. 1. I added `.typing-speed` to our parent container so that we can find or style our custom input. Replace the `textAreaInput()` with our new `typingSpeedInput()` and run the app. It works the same! Wait, why? ```{r} ui <- fluidPage( # textAreaInput("typing", "Type here..."), typingSpeedInput("typing", "Type here..."), verbatimTextOutput("debug") ) ``` ## Start creating an input binding for `typingSpeedInput()` Now we can open `typing.js` and create a Shiny input binding. If you used `js4shiny::snippets_install()`, you have a `ShinyInputBinding` snippet that provides a template for you. Or you can copy the chunk below.
Shiny Input Binding Template ```js // Ref: https://shiny.rstudio.com/articles/building-inputs.html // Ref: https://github.com/rstudio/shiny/blob/master/srcjs/input_binding.js const bindingName = new Shiny.InputBinding(); $.extend(bindingName, { find: function(scope) { // Specify the selector that identifies your input. `scope` is a general // parent of your input elements. This function should return the nodes of // ALL of the inputs that are inside `scope`. These elements should all // have IDs that are used as the inputId on the server side. return scope.querySelectorAll("inputBindingSelector"); }, getValue: function(el) { // For a particular input, this function is given the element containing // your input. In this function, find or construct the value that will be // returned to Shiny. The ID of `el` is used for the inputId. // e.g: return el.value return 'FIXME'; }, setValue: function(el, value) { // This method is used for restoring the bookmarked state of your input // and allows you to set the input's state without triggering reactivity. // Basically, reverses .getValue() // e.g.; el.value = value console.error('bindingName.setValue() is not yet defined'); }, receiveMessage: function(el, data) { // Given the input's container and data, update the input // and its elements to reflect the given data. // The messages are sent from R/Shiny via // R> session$sendInputMessage(inputId, data) console.error('bindingName.receiveMessage() is not yet defined'); // If you want the update to trigger reactivity, trigger a subscribed event $(el).trigger("change") }, subscribe: function(el, callback) { // Listen to events on your input element. The following block listens to // the change event, but you might want to listen to another event. // Repeat the block for each event type you want to subscribe to. $(el).on("change.bindingName", function(e) { // Use callback() or callback(true). // If using callback(true) the rate policy applies, // for example if you need to throttle or debounce // the values being sent back to the server. callback(); }); }, getRatePolicy: function() { return { policy: 'debounce', // 'debounce', 'throttle' or 'direct' (default) delay: 100 // milliseconds for debounce or throttle }; }, unsubscribe: function(el) { $(el).off(".bindingName"); } }); Shiny.inputBindings.register(bindingName, 'pkgName.bindingName'); ```