quarto-ext / shinylive

Quarto extension to embed Shinylive for Python applications
https://quarto-ext.github.io/shinylive/
MIT License
151 stars 8 forks source link

Add support for linking to an external shiny live app #37

Open coatless opened 11 months ago

coatless commented 11 months ago

With rmarkdown, the app source can be outside of the code cell using:


```{r}
shiny::shinyAppDir(
  "path/to/shiny/app/"
)

Would it be possible to get a similar file name directive for shinylive? As it stands, something like: 

````md
```{shinylive-r}
#| standalone: true
#| viewerHeight: 600

shiny::shinyAppDir(
  "path/to/shiny/app/"
)

or 

````md
```{shinylive-r}
#| standalone: true
#| viewerHeight: 600
#| shinyAppDir: path/to/shiny/app/


**Note:** This isn't quite the same as support for the `## file: ` directive (not yet implemented for R) that allows for breaking up a code cell inside of Quarto. This request focuses more on external apps vs. internal/embedded apps.  
gadenbuie commented 9 months ago

Note: This isn't quite the same as support for the ## file: directive (not yet implemented for R) that allows for breaking up a code cell inside of Quarto.

Hi @coatless, I'm having a hard time parsing this sentence. As far as I can tell, there are two things you might mean by ## file: directive:

  1. The shinylive quarto extension comment for adding external files into the shinylive-r or shinylive-python chunk, which marks the start of a new file with ## file: <file_name>.
  2. The knitr chunk option #| file: for including an external file.

I think you might mean the first option, which is implemented for shinylive-r chunks, but you might mean the second, which is not implemented for the shinylive extension in general. After that, I'm not sure which Quarto feature "allows for breaking up a code cell"...?


That said, it would be helpful for the shinylive extension to allow users to pull in the chunk contents from a file, e.g. using a file chunk option, or via a directory, possibly using app_dir as the chunk option name.

coatless commented 9 months ago

👋 @gadenbuie

Sorry for the confusion.

The ask here is to be able to take any external R Shiny App (.app/ui.R + server.R) and incorporate it into the shinylive-r code cell. As it stands, the current approach is to keep a standalone test file for R and, then, copy + paste it verbatim into a {shinylive-r} cell.

The knitr option of #| file: is one way to approach solving that issue. The other is parsing a shinyAppDir() function call in an shinylive-r cell. In the final code cell, I was suggesting an easier parse approach for the directory specification since that does not require executing code in the cell to unfold the app via shinyAppDir().


Regarding the ## file: attribute, I mentioned that to emphasize that I wasn't looking for an in-script option for embedding the script (e.g. no ## file: ui.R and ## file: server.R.

Though, for r-shinylive the ## file: is not fully implemented. Consider the following modifications from the py-shinylive index.qmd example:

---
title: "Embed image file attribute shinylive-r"
format:
  html:
    resources: 
      - shinylive-sw.js
filters:
  - shinylive
---

```{shinylive-r}
#| standalone: true
#| components: [editor, viewer]
#| layout: vertical
#| viewerHeight: 400
## file: app.R
library(shiny)

ui <- fluidPage(
  titlePanel("Hello Shiny!")
)

server <- function(input, output) {

}

# Create Shiny app ----
shinyApp(ui = ui, server = server)

## file: www/logo.png
## type: binary
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


<img width="1370" alt="Editor view showing app instantiation fails to create R object from image data" src="https://github.com/quarto-ext/shinylive/assets/833642/0af85699-1b63-4659-86f5-48bcc9864f96">

<img width="1377" alt="Preview of app data stream" src="https://github.com/quarto-ext/shinylive/assets/833642/9d14a7d5-cf0d-4ddf-933d-03f65d19d4cc">
gadenbuie commented 9 months ago

@coatless thanks for the explanation -- I would expect binary embedded files to work, so I've move that part of the discussion to a new issue: #40.

I certainly agree that it'd be useful to be able to embed an entire app via it's directory and my personal preference is for a chunk option.

By the way, I recently added a command to the CLI in the Python py-shinylive package to create shinylive URLs from local files or to decode shinylive URLs. Given a directory structure like

.
├── app.R
└── images
    ├── basic.txt
    └── logo.png

and after installing shinylive with pip install shinylive, you could run

shinylive url encode app.R images/*
#> https://shinylive.io/r/editor/#code=NobwRAdghgtgpmAXAAjFADugd...

to create the shinylive URL for that app bundle. You can pipe this output back into shinylive url decode to decode the app, which is helpfully printed using the quarto chunk syntax:

shinylive url encode app.R images/* | shinylive url decode
## file: app.R
library(shiny)

ui <- fluidPage(
  titlePanel("Hello Shiny!")
)

server <- function(input, output) {

}

# Create Shiny app ----
shinyApp(ui = ui, server = server)

## file: images/basic.txt
Just plain text

## file: images/logo.png
## type: binary
iVBORw0KGgoAAAANSUhEUgAAACgAAAAuCAYAAABap1twAAAABGdB...

Obviously this isn't as convenient as an actual chunk option, but it might help you out until are able to develop the new feature.

gadenbuie commented 5 months ago

By the way, for text app files, Quarto's include directive works really well:

Instead of this (which doesn't work)

```{shinylive-r}
#| standalone: true
#| viewerHeight: 600

shiny::shinyAppDir(
  "path/to/shiny/app/"
)

You can use this

````markdown
```{shinylive-r}
#| standalone: true
#| viewerHeight: 600

{{< include "path/to/shiny/app/app.R" >}}

Or to rename the file

````markdown
```{shinylive-r}
#| standalone: true
#| viewerHeight: 600

## file: app.R
{{< include "path/to/shiny/app/app-example.R" >}}
gadenbuie commented 4 months ago

I just created a new Quarto extension that provides a base64 shortcode to fill in the last missing gap here: adding base64 encoded files to Shinylive.

Using that extension, you can now do this:

```{shinylive-r}
#| standalone: true
#| viewerHeight: 600

## file: app.R
{{< include "path/to/shiny/app/app-example.R" >}}

## file: www/photo.png
## type: binary
{{< base64 "path/to/shiny/app/www/photo.png" >}}


For more complicated apps, I think we'd like to lean on `shinylive::export()` or `shinylive.export()` so that we can coordinate where the assets live. Maybe we could add a shortcode `{{< shinylive-app-r "app/dir" ... >}}`.