26 Fine-tune {shinyMobile}

26.1 Enhance the disconnect screen

As depicted in Figure 26.1, having the classic Shiny disconnect screen in a mobile device is not that beautiful, especially knowing about all the Framework7 capabilities.

Vanilla Shiny disconnect screen.

FIGURE 26.1: Vanilla Shiny disconnect screen.

Let’s do better! Upon disconnection, we want to display a toast with two buttons:

  • A reload button that reloads the window and re-initializes the app. This button calls location.reload() upon click.
  • A reconnect button, that tries to reconnect with the server websocket, so that we don’t lose any input, output elements. This button calls Shiny.shinyapp.reconnect() upon click.

How do we know when shiny is disconnected? As described in Chapter 11, whenever the client socket connection is closed, for any reason, the shiny:disconnected event is raised:

socket.onclose = function() {
  // These things are needed only if we've successfully 
  // opened the websocket.
  if (hasOpened) {
    $(document).trigger({
      type: 'shiny:disconnected',
      socket: socket
    });

    self.$notifyDisconnected();
  }

  self.onDisconnected(); // run before self.$removeSocket()
  self.$removeSocket();
}

This allows us to listen to that event on the JS side:

$(document).on('shiny:disconnected', function(event) {
  // Do things
});

In the next step, we to remove the default Shiny reconnect elements. They are inserted by the onDisconnected method, which adds a disconnect overlay (gray-out screen) and optionally a reconnect notification:

// From within Shiny.shinyapp...
this.onDisconnected = function() {
  // Add gray-out overlay, if not already present
  var $overlay = $('#shiny-disconnected-overlay');
  if ($overlay.length === 0) {
    $(document.body)
      .append('<div id="shiny-disconnected-overlay"></div>');
  }

  // To try a reconnect, both the app (this.$allowReconnect) 
  // and the server (this.$socket.allowReconnect) must allow 
  // reconnections, or session$allowReconnect("force") was 
  // called. The "force" option should only be used for 
  // testing.
  if (
    (this.$allowReconnect === true && 
    this.$socket.allowReconnect === true) ||
      this.$allowReconnect === 'force')
  {
    var delay = reconnectDelay.next();
    exports.showReconnectDialog(delay);
    this.$scheduleReconnect(delay);
  }
}

To remove default shiny reconnect elements, there are multiple alternatives. The easiest way is to wait for the client to be connected, that is listening to shiny:connected, and set the Shiny.shinyapp.onDisconnected method to only add the gray overlay.

Before modifying any vanilla shiny elements, make sure to check all the possible side effects.

// remove shiny reconnect stuff;
$(document).on('shiny:connected', function(event) {
  Shiny.shinyapp.onDisconnected = function() {
    // Add gray-out overlay, if not already present
    let $overlay = $('#shiny-disconnected-overlay');
    if ($overlay.length === 0) {
      $(document.body)
        .append('<div id="shiny-disconnected-overlay"></div>');
    }
  };
});

We edit the previous disconnected event listener to add a custom Framework7 toast, which closes upon click:

$(document).on('shiny:disconnected', function(event) {    
  let reconnectToast = app.toast
    .create({
      position: 'center',
      text:
        `Oups... disconnected </br> </br> 
        <div class="row">
          <button 
            onclick="Shiny.shinyapp.reconnect();" 
            class="toast-button button color-green col">
            Reconnect
          </button>
          <button 
            onclick="location.reload();" 
            class="toast-button button color-red col">
            Reload
          </button>
        </div>`
    })
    .open();

  // close toast whenever a choice is made ...
  $('.toast-button').on('click', function() {
    reconnectToast.close();
  });
});

The result is shown in Figure 26.2.

Vanilla Shiny disconnect screen.

FIGURE 26.2: Vanilla Shiny disconnect screen.

The above JS code ignores the user reconnect setup and proposes reconnecting regardless of the session$allowReconnect configuration. If you want to keep the original behavior, you may include the following condition before showing the toast:

if (
  (Shiny.shinyapp.$allowReconnect === true && 
  Shiny.shinyapp.$socket.allowReconnect === true) ||
      Shiny.shinyapp.$allowReconnect === 'force') {
  // Toast logic
}