21

As I am preparing tutorials for students, I need a way to hide content in collapsible panels which can be revealed by clicking on a button. I have got this to work using the code below. The RMarkdown file looks like this:

---
title: Collapsible Panel
output:
  html_document:
    theme: flatly
    highlight: tango
---

<p>
  <a class="btn btn-primary" data-toggle="collapse" href="#collapseExample1" role="button" aria-expanded="false" aria-controls="collapseExample1">
    Click For Answer
  </a>
</p>
<div class="collapse" id="collapseExample1">
  <div class="card card-body">

  ```{r}
  hist(1:10)
  ```

  </div>
</div>

And it looks like this when rendered:

enter image description here

This works! I can also control if the code and/or results must be shown by controlling the chunk options.

But, this is not optimal because the code is messy and ugly with all the raw html. Copy-pasting this multiple times is not ideal. The ID used collapseExample1 needs to be unique every time this code block is used.

Is there some way to package this block into a reusable unit like a function or something? I am thinking something like an R function, where I can pass in code to be evaluated (or code that don't need to be evaluated), chunk options (eval, echo, results, etc..) and state of the panel (open/closed).

collapsible_panel(code=NULL,echo=TRUE,results="show",state="closed")

I have many unclear questions at this point. Can I run R chunks inside R chunks? Maybe I need to use child Rmd files? Do I need to write some custom javascript?

pogibas
  • 27,303
  • 19
  • 84
  • 117
mindlessgreen
  • 11,059
  • 16
  • 68
  • 113

3 Answers3

38

Another simple solution that would work (but without buttons and styling).

```{r, eval=FALSE}
hist(1:10)
```

<details>
  <summary>Click for Answer</summary>
    ```{r, echo=FALSE, eval=TRUE}
    hist(1:10)
    ```
</details> 

And here are the two states:

Collapsed collapsed

Expanded expanded

OmaymaS
  • 1,671
  • 1
  • 14
  • 18
  • 1
    This works well for me. Just in case it helps someone else, though, I was seeing raw HTML instead of the image for the histogram. [This answer](https://stackoverflow.com/a/40734207/1714) explains why, and the fix – Hobo Aug 06 '19 at 01:36
  • 1
    Great solution. And for those who (like me initially) wonder where the `details` and the `summary` tags are defined, see https://www.w3schools.com/tags/tag_details.asp – Holger Brandl Aug 06 '19 at 07:32
  • @Hobo I see BOTH histogram and a bit a raw code (`::: {.cell} ::: {.cell-output-display}`). How exactly did you fix the issue? – Emile Zäkiev Feb 22 '23 at 15:41
  • @EmileZäkiev - I'm sorry, but I don't remember anything about this at all (I don't even remember hitting the problem). Though what you've chatted looks like a different issue to whatever I was talking about - I assume by "raw html" I meant html tags like `
    `, and those `{.cell}` values aren't html tags
    – Hobo Feb 22 '23 at 22:05
19

You can use multiple tabs (add {.tabset} after the header). It's very simple to generate them using r-markdown and they look almost the same as collapsible panel (of course you need to have more than one option).
Not to paste same code multiple times specify code argument in chunk options (code = readLines("code.R")). Or you can have only one panel for code and answer so you wouldn't need external document.

---
title: Collapsible Panel
output:
  html_document:
    theme: flatly
    highlight: tango
---

# Question 1 {.tabset .tabset-fade .tabset-pills}

## Question

How does uniform distribution look like?

## Code 

```{r, echo = TRUE, eval = FALSE, code = readLines("Q1.R")}
```

## Answer

```{r, echo = FALSE, eval = TRUE, code = readLines("Q1.R")}
```

Code file (Q1.R):

hist(1:10)

enter image description here


To not have any content and then show answer you can make first tab completely empty with:

# Question 1 {.tabset}

##  <span>&#8203;</span>

## Answer

```{r, echo = FALSE, eval = TRUE, code = readLines("Q1.R")}
```

enter image description here

pogibas
  • 27,303
  • 19
  • 84
  • 117
  • Interesting solution! I hadn't thought of using tabs for this purpose. But, it's not possible to show/hide the code part by the user. – mindlessgreen Sep 30 '18 at 10:01
  • One solution is to use 3 tabs. First tab will have the question. Second can have the code and third, the result. Or two tabs with first tab as question and second tab as code+results. – mindlessgreen Sep 30 '18 at 10:07
  • 2
    Thanks @PoGibas, This is pretty good. But I would like to see if there is a better solution that doesn't use tabs. Tabs do not work well with printing the HTML to PDF for example. – mindlessgreen Sep 30 '18 at 10:14
  • 3
    @rmd There should be a way to make collapsible sections (for example your original code), my answer is just a *clean* (uses already implemented r-markdown options) workaround. – pogibas Sep 30 '18 at 10:17
12

enter image description here

Two slightly different methods are shown. Both approaches use only HTML and CSS. Here is the full working Rmd.

---
title: Accordion
output:
  html_document
---

## Method 1

This method uses button.

```{css,echo=FALSE}
button.btn.collapsed:before
{
    content:'+' ;
    display:block;
    width:15px;
}
button.btn:before
{
    content:'-' ;
    display:block;
    width:15px;
}
```

```{r,echo=FALSE,results='hide'}
knitr::knit_hooks$set(drop1=function(before, options, envir) {
    if (before) {
        paste(
            '<p>',
'<button class="btn btn-primary collapsed" data-toggle="collapse" data-target="#ce1">',
'</button>',
'</p>',
'<div class="collapse" id="ce1">',
'<div class="card card-body">',  sep = "\n")
    } else {
        paste("</div>", "</div>", sep = "\n")
    }
})
```



```{r,drop1=TRUE,results="markup"}
str(iris)
```

## Method 2

This method uses a link which behaves like a button.

```{css,echo=FALSE}
[data-toggle="collapse"].collapsed .if-not-collapsed {
  display: none;
}
[data-toggle="collapse"]:not(.collapsed) .if-collapsed {
  display: none;
}
```

```{r,echo=FALSE,results='hide'}
knitr::knit_hooks$set(drop2=function(before, options, envir) {
    if (before) {
        paste(
            '<p>',
'<a class="btn btn-primary collapsed" data-toggle="collapse" href="#ce2">',
'<span class="if-collapsed">+</span>',
'<span class="if-not-collapsed">-</span>',
'</a>',
'</p>',
'<div class="collapse" id="ce2">',
'<div class="card card-body">',  sep = "\n")
    } else {
        paste("</div>", "</div>", sep = "\n")
    }
})
```

```{r,drop2=TRUE,results="markup"}
str(iris)
```

Executed R chunks can be hidden in collapsible containers (collapsed by default). The containers are defined in the R chunk options using a custom knitr hook (drop1/drop2). The collapsible states of the container is controlled using a button or a link (looks like a button). Custom CSS is used to change text on the button for collapsed/open states.

mindlessgreen
  • 11,059
  • 16
  • 68
  • 113
  • 1
    Unfortunately, both methods do not allow to have multiple collapsible content elements. With both versions, it is always un/collapsing the first cell's content. – Holger Brandl Jul 24 '22 at 17:18
  • Cool method, but doesn't work in the current markdown version 2.20 – Emile Zäkiev Feb 22 '23 at 15:27
  • @Emile It works fine for me using **knitr 1.42** and **rmarkdown 2.20**. But it doesn't seem to work in quarto document. I haven't looked into why. @Holger If multiple copies are used in the same document, the `data-target` label must be unique. In real world use, I generate a random string based on current date-time. – mindlessgreen Feb 22 '23 at 16:04