Currently I am working on a tutorial package that uses the learnr package to create the tutorials.
My goal is to modify the function of the "Run Code" button in learnr so that the code written by students are automatically copied to their clipboard when the code is ran.
The reason for this is because usually the next exercise's problem builds upon the pipe from the previous exercise.
I looked around in the learnr package and found that they use the javascript package clipboardjs to copy text onto clipboard for hints.
Other developers working on the package have previously overwritten R functions in the learnr package by putting them in a zzz.R and overwriting the namespace on load.
I was wondering if there is anyway to add the copy to clipboard functionality to the "Run Code" button without having to edit the underlying learnr package?
Rather than making use of the user's clipboard, here's a solution that uses Shiny and a little JavaScript to use the last submission from an exercise to populate the code in another exercise.
---
title: "Tutorial"
output: learnr::tutorial
runtime: shiny_prerendered
---
```{r setup, include=FALSE}
library(learnr)
knitr::opts_chunk$set(echo = FALSE)
```
```{js echo=FALSE}
//
// A custom Shiny message handler that updates the code in any exercise
// From R you'll send a list(label = "exercise-chunk-label", code = "new code for editor"))
//
Shiny.addCustomMessageHandler('set-exercise-code', function(x) {
var el = $(`.tutorial-exercise[data-label="${x.label}"] .tutorial-exercise-code-editor`)
var exerciseInput = Shiny.inputBindings.bindingNames["tutorial.exerciseInput"].binding
exerciseInput.setValue(el, {code: x.code})
})
```
## Topic 1
### Exercise
*Here's a simple exercise with an empty code chunk provided for entering the answer.*
Write the R code required to add two plus two:
```{r two-plus-two, exercise=TRUE}
```
### Exercise with Code
*Here's an exercise with some prepopulated code as well as `exercise.lines = 5` to provide a bit more initial room to work.*
Now write a function that adds any two numbers and then call it:
```{r use-two-plus-two-button}
actionButton("use-two-plus-two", "Use your answer to the previous exercise")
```
```{r handle-use-two-plus-two-button, context="server"}
observeEvent(input[["use-two-plus-two"]], {
# get last submission for the "two-plus-two" exercise
code <- learnr:::get_exercise_submission(session, "two-plus-two")$data$code
# use last submission in template
code <- paste("add <- function() {\n", trimws(code), "\n}\n\nadd()")
# send a custom message to our handler (see js chunk above)
# updates the "add-function" exercise with the new code
session$sendCustomMessage("set-exercise-code", list(label = "add-function", code = code))
})
```
```{r add-function, exercise=TRUE, exercise.lines = 5}
add <- function() {
}
```
Notes
This approach requires that the use clicks "Run Code" (or "Submit Answer") at least once, but I think it's worth it. First, because the trigger of the code updating is a button, so the user is in control. If you instead use the clipboard, you're overwriting whatever the use has on the clipboard and it would be hard to add feedback to let the user know you did this. Also, as a user of a clipboard manager, I'd be frustrated personally if the tutorial added many things to my clipboard stack.
Thank you so much for this answer, your solution is a much better and more elegant way than having to copy to clipboard without notifying the user.
I do have a question regarding the observeEvent function used with the actionButton. Would you be able to pass parameters to observeEvent through actionButton? If so, it might be possible to dynamically set the code for the next exercise without having to create an individual event listener for every exercise.