Search code examples
rshinyshinyjs

Shiny is undefined in iframe


I am currently trying to develop a minimal working CustomMessageHandling dashbord in R. What I am doing is simply sending a message from my R client side to my Javascript file, which is then run in an html file. The error is as follows:

jQuery.Deferred exception: Shiny is not defined ReferenceError: Shiny is not defined

In my JavaScript file, which is called in my .html file, I simply add it at the bottom of the file, which looks like:

$(document).on('shiny:connected', function() {
  console.log("Hello, I am executing!");
  const clientID = "e800d12fc12c4d60960778b2bc4370af";
  var urlToBase64PDF;

  Shiny.addCustomMessageHandler('handler1', function()
    {
      doUpdate();
    }
    );

  function base64ToArrayBuffer(base64)
    {
    var bin = window.atob(base64);
    var len = bin.length;
    var uInt8Array = new Uint8Array(len);
    for (var i = 0; i < len; i++)
    {
      uInt8Array[i] = bin.charCodeAt(i);

    }
      return uInt8Array.buffer;
    }


  function doUpdate(message1)
    {
    urlToBase64PDF = message1;
    }


  document.write(urlToBase64PDF);
  console.log(urlToBase64PDF);

  document.addEventListener("adobe_dc_view_sdk.ready", function()
  {
    var adobeDCView = new AdobeDC.View({clientId: clientID, divId: "adobe-dc-view"});
    document.write(urlToBase64PDF);
    console.log(urlToBase64PDF);
    adobeDCView.previewFile({content:{ promise: Promise.resolve(base64ToArrayBuffer(urlToBase64PDF))}, metaData:{fileName: "check.pdf"}},
    {});
  });



});

In my .html file, I call it in the following fashion:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8"/>
    <meta http-equiv="X-UA-Compatible" content="IE=edge, chrome=1"/>
    <meta id="viewport" name="viewport" content="width=device-width, initial-scale=1"/>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
    <script type="text/javascript" src="index.js"></script>

</head>

The rest of the functionality is built in Shiny in R, which are extremely simple UI and Server parts:

app_ui <- function() {
  shiny::addResourcePath(
    'www', system.file('app/www', package = 'test')
  )

  tags$iframe(src="www/index.html", height = 600, width = 600)

and lastly the server part:

app_server <- function(input, output, session){
  shinyjs::useShinyjs()
    message1 = "test"
    session$sendCustomMessage("handler1", message1)
  }

I have literally tried everything, searched everywhere, and even the documentation on CustomMessageHandling sends messages in the above fashion. Yet I still get the Shiny undefined Error in my console.

Edit: Exact error:

No output to console at all.


Solution

  • You can't use variables from the parent frame in embedded iframe. This is due to security reasons. So Shiny is never defined in iframe and any shiny event cannot be listened in iframe.

    what you see is following:

    console.log("script starts")
    $(document).on('shiny:connected', function() {
      console.log("oh yeah")
      // do some other things
    });
    console.log("script ends")
    
    // index.js:1 script starts
    // index.js:6 script ends
    

    You can see the middle part is never run, because there is no shiny:connected event at all. When you are listening to unknown events, sadly it doesn't report any error messages.

    Change to this makes it clearer:

    console.log("script starts")
    $(function(){
        console.log(Shiny)
    })
    console.log("script ends")
    
    index.js:1 script starts
    index.js:5 script ends
    jquery.min.js:2 jQuery.Deferred exception: Shiny is not defined ReferenceError: Shiny is not defined
        at HTMLDocument.<anonymous> (http://127.0.0.1:3168/www/index.js:3:17)
        at e (https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js:2:30005)
        at t (https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js:2:30307) undefined
    jquery.min.js:2 Uncaught ReferenceError: Shiny is not defined
        at HTMLDocument.<anonymous> (index.js:3:17)
        at e (jquery.min.js:2:30005)
        at t (jquery.min.js:2:30307)
    

    Even we waited for the document to be ready, you can see there is still no Shiny.

    Then you will ask so where does the jquery comes from. Well, you re-imported in your index.html: <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>. That's why you can use jquery but not Shiny.

    Solution

    A simple solution is to use serverside rendering, so it makes sure the iframe is set up after Shiny is initialized.

    library(shiny)
    
    addResourcePath('www', "./")
    ui <- fluidPage(
        uiOutput("iframe")
    )
    
    server <- function(input, output, session) {
        output$iframe <- renderUI({
            tags$iframe(src="www/index.html", height = 600, width = 600)
        })
    }
    
    shinyApp(ui, server)
    

    This only solves part of your problem. I see you have a custom handler that sends out an update command based on some other things that are controlled by Shiny. In this case, there is no easy solution, you need cross-origin communication.

    In the next example, I use a button to simulate your update event and it is controlled by shiny observeEvent. Once clicked, it sends the sendCustomMessage. On UI, we add some script to listen to this message and then, dispatch the event to iframe by postMessage.

    library(shiny)
    
    addResourcePath('www', "./")
    ui <- fluidPage(
        uiOutput("iframe"),
        actionButton("update", "update"),
        tags$script(HTML(
        "
        Shiny.addCustomMessageHandler('handler1', function(data){
            if(data.msg !== 'update') return ;
            $('#myiframe')[0].contentWindow.postMessage(data.msg, '*');
        });
        "
        ))
    )
    
    server <- function(input, output, session) {
        output$iframe <- renderUI({
            tags$iframe(id = "myiframe", src="www/index.html", height = 600, width = 600)
        })
        
        observeEvent(input$update, {
            session$sendCustomMessage("handler1", list(msg = "update"))
        }, ignoreInit = TRUE)
    }
    
    shinyApp(ui, server)
    

    In iframe index.js we use window.onmessage listener to catch our message

    console.log("script starts")
    $(function(){
        window.addEventListener("message", (e) => {
            if (e.data === 'update') $('body').append('<h1>Oh yeah !</h1>');
        });
    })
    console.log("script ends")
    

    This example appends a h1 every time you click update from the parent site to the iframe.

    enter image description here