Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

196 lines
5.5KB

  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. Now we can open `typing.js` and create a Shiny input binding.
  90. If you used `js4shiny::snippets_install()`,
  91. you have a `ShinyInputBinding` snippet that provides a template for you.
  92. Or you can copy the chunk below.
  93. <details><summary>Shiny Input Binding Template</summary>
  94. ```js
  95. // Ref: https://shiny.rstudio.com/articles/building-inputs.html
  96. // Ref: https://github.com/rstudio/shiny/blob/master/srcjs/input_binding.js
  97. const bindingName = new Shiny.InputBinding();
  98. $.extend(bindingName, {
  99. find: function(scope) {
  100. // Specify the selector that identifies your input. `scope` is a general
  101. // parent of your input elements. This function should return the nodes of
  102. // ALL of the inputs that are inside `scope`. These elements should all
  103. // have IDs that are used as the inputId on the server side.
  104. return scope.querySelectorAll("inputBindingSelector");
  105. },
  106. getValue: function(el) {
  107. // For a particular input, this function is given the element containing
  108. // your input. In this function, find or construct the value that will be
  109. // returned to Shiny. The ID of `el` is used for the inputId.
  110. // e.g: return el.value
  111. return 'FIXME';
  112. },
  113. setValue: function(el, value) {
  114. // This method is used for restoring the bookmarked state of your input
  115. // and allows you to set the input's state without triggering reactivity.
  116. // Basically, reverses .getValue()
  117. // e.g.; el.value = value
  118. console.error('bindingName.setValue() is not yet defined');
  119. },
  120. receiveMessage: function(el, data) {
  121. // Given the input's container and data, update the input
  122. // and its elements to reflect the given data.
  123. // The messages are sent from R/Shiny via
  124. // R> session$sendInputMessage(inputId, data)
  125. console.error('bindingName.receiveMessage() is not yet defined');
  126. // If you want the update to trigger reactivity, trigger a subscribed event
  127. $(el).trigger("change")
  128. },
  129. subscribe: function(el, callback) {
  130. // Listen to events on your input element. The following block listens to
  131. // the change event, but you might want to listen to another event.
  132. // Repeat the block for each event type you want to subscribe to.
  133. $(el).on("change.bindingName", function(e) {
  134. // Use callback() or callback(true).
  135. // If using callback(true) the rate policy applies,
  136. // for example if you need to throttle or debounce
  137. // the values being sent back to the server.
  138. callback();
  139. });
  140. },
  141. getRatePolicy: function() {
  142. return {
  143. policy: 'debounce', // 'debounce', 'throttle' or 'direct' (default)
  144. delay: 100 // milliseconds for debounce or throttle
  145. };
  146. },
  147. unsubscribe: function(el) {
  148. $(el).off(".bindingName");
  149. }
  150. });
  151. Shiny.inputBindings.register(bindingName, 'pkgName.bindingName');
  152. ```
  153. </details>