Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.

231 lines
6.7KB

  1. ---
  2. output: github_document
  3. ---
  4. ```{r setup, include=FALSE}
  5. knitr::opts_chunk$set(eval = FALSE)
  6. github_sha_link <- function(sha) {
  7. glue::glue("[{sha}](https://github.com/gadenbuie/js4shiny-frappeCharts/commit/{sha})")
  8. }
  9. get_last_sha <- function() {
  10. commit <- git2r::commits()[[1]]
  11. message(commit$summary)
  12. message(commit$sha)
  13. clipr::write_clip(glue::glue(
  14. "`r github_sha_link(\"{commit$sha}\")`"
  15. ))
  16. }
  17. ```
  18. # Building a Shiny Input
  19. In this project,
  20. we're going to create a typing speed app
  21. using a custom Shiny input.
  22. The app will give users typing prompts,
  23. monitor their typing speed,
  24. and use a Frappe Chart line chart
  25. to show their speed over time as they type.
  26. ## Setup a folder for our app inside the frappeCharts package
  27. `r github_sha_link("113340074c3af9c2cdf46cd7787829d4ec56bfcf")`
  28. Create the directory `inst/shiny-input-app` and add `app.R` and `typing.js`.
  29. ```{r}
  30. usethis::use_directory("inst/shiny-input-app")
  31. file.create("inst/shiny-input-app/app.R")
  32. file.create("inst/shiny-input-app/typing.js")
  33. ```
  34. ## Create a basic Shiny app with a typing area
  35. `r github_sha_link("ff3a962f7e6d16a75ebdb620aac0fdfc9949086e")`
  36. We'll start with typical Shiny inputs.
  37. ```r
  38. library(shiny)
  39. ui <- fluidPage(
  40. textAreaInput("typing", "Type here..."),
  41. verbatimTextOutput("debug")
  42. )
  43. server <- function(input, output, session) {
  44. output$debug <- renderPrint(input$typing)
  45. }
  46. shinyApp(ui, server)
  47. ```
  48. Run this app and type in the box.
  49. ## Create our own typingSpeedInput()
  50. `r github_sha_link("b7108029b58635652ce87f3e1ea9a2c5a6232020")`
  51. Use Shiny's `textAreaInput()` to get the template
  52. for our own `typingSpeedInput()`
  53. ```{r, eval=TRUE}
  54. shiny_text_input <- shiny::textAreaInput(
  55. "INPUT", "LABEL", placeholder = "PLACEHOLDER"
  56. )
  57. cat(format(shiny_text_input))
  58. ```
  59. ```{r}
  60. typingSpeedInput <- function(inputId, label, placeholder = NULL) {
  61. .label <- label
  62. htmltools::withTags(
  63. div(
  64. class = "form-group typing-speed",
  65. label(class = "control-label", `for` = inputId, .label),
  66. textarea(id = inputId, class = "form-control", placeholder = placeholder)
  67. )
  68. )
  69. }
  70. ```
  71. Two points:
  72. 1. Notice that I used `htmltools::withTags()`,
  73. which makes it easier to write multiple tags at once.
  74. But it has the downside of masking the `label` argument of `typingSpeedInput()`.
  75. Hence, the first line `.label <- label`.
  76. 1. I added `.typing-speed` to our parent container so that we can
  77. find or style our custom input.
  78. Replace the `textAreaInput()` with our new `typingSpeedInput()` and run the app.
  79. It works the same!
  80. Wait, why?
  81. ```{r}
  82. ui <- fluidPage(
  83. # textAreaInput("typing", "Type here..."),
  84. typingSpeedInput("typing", "Type here..."),
  85. verbatimTextOutput("debug")
  86. )
  87. ```
  88. ## Start creating an input binding for `typingSpeedInput()`
  89. `r github_sha_link("02adb896c50a97ebe7f9a7fef2a6f00488f9418d")`
  90. Now we can open `typing.js` and create a Shiny input binding.
  91. If you used `js4shiny::snippets_install()`,
  92. you have a `ShinyInputBinding` snippet that provides a template for you.
  93. Or you can copy the chunk below.
  94. <details><summary>Shiny Input Binding Template</summary>
  95. ```js
  96. // Ref: https://shiny.rstudio.com/articles/building-inputs.html
  97. // Ref: https://github.com/rstudio/shiny/blob/master/srcjs/input_binding.js
  98. const bindingName = new Shiny.InputBinding();
  99. $.extend(bindingName, {
  100. find: function(scope) {
  101. // Specify the selector that identifies your input. `scope` is a general
  102. // parent of your input elements. This function should return the nodes of
  103. // ALL of the inputs that are inside `scope`. These elements should all
  104. // have IDs that are used as the inputId on the server side.
  105. return scope.querySelectorAll("inputBindingSelector");
  106. },
  107. getValue: function(el) {
  108. // For a particular input, this function is given the element containing
  109. // your input. In this function, find or construct the value that will be
  110. // returned to Shiny. The ID of `el` is used for the inputId.
  111. // e.g: return el.value
  112. return 'FIXME';
  113. },
  114. setValue: function(el, value) {
  115. // This method is used for restoring the bookmarked state of your input
  116. // and allows you to set the input's state without triggering reactivity.
  117. // Basically, reverses .getValue()
  118. // e.g.; el.value = value
  119. console.error('bindingName.setValue() is not yet defined');
  120. },
  121. receiveMessage: function(el, data) {
  122. // Given the input's container and data, update the input
  123. // and its elements to reflect the given data.
  124. // The messages are sent from R/Shiny via
  125. // R> session$sendInputMessage(inputId, data)
  126. console.error('bindingName.receiveMessage() is not yet defined');
  127. // If you want the update to trigger reactivity, trigger a subscribed event
  128. $(el).trigger("change")
  129. },
  130. subscribe: function(el, callback) {
  131. // Listen to events on your input element. The following block listens to
  132. // the change event, but you might want to listen to another event.
  133. // Repeat the block for each event type you want to subscribe to.
  134. $(el).on("change.bindingName", function(e) {
  135. // Use callback() or callback(true).
  136. // If using callback(true) the rate policy applies,
  137. // for example if you need to throttle or debounce
  138. // the values being sent back to the server.
  139. callback();
  140. });
  141. },
  142. getRatePolicy: function() {
  143. return {
  144. policy: 'debounce', // 'debounce', 'throttle' or 'direct' (default)
  145. delay: 100 // milliseconds for debounce or throttle
  146. };
  147. },
  148. unsubscribe: function(el) {
  149. $(el).off(".bindingName");
  150. }
  151. });
  152. Shiny.inputBindings.register(bindingName, 'pkgName.bindingName');
  153. ```
  154. </details>
  155. If you use the snippet,
  156. it automatically walks you through
  157. the first pass of parts that need to be changed.
  158. If you copied the template,
  159. you need to find and replace all:
  160. - `bindingName`: the name of the JavaScript object with the specifics of your
  161. Shiny input. This name isn't user facing, but helps Shiny keep track of which
  162. the inputs in an app.
  163. We'll call ours `typingSpeed`
  164. - `inputBindingSelector`: this is the CSS selector that can be used to find
  165. the HTML element of your input. The element you find here is the element in
  166. your input's HTML that has the `id` with the input's `inputId`.
  167. In our case, we want the `textarea` element
  168. that's a descendent of `.typing-speed`, so we use
  169. ```css
  170. .typing-speed textarea
  171. ```
  172. - `change`: This is the event that will be listened to by Shiny to know when
  173. the input has updated. For our typing speed input, we're going to listen to
  174. the `keyup` event.
  175. - `pkgName`: if you're writing this input as part of a package, use your package
  176. name. It's not critical; this just provides some namespacing in case there's
  177. another package that impelements an in put with the same `bindingName`.
  178. Here we use `js4shiny`.