README.md

shinychord

Please note

Shinychord is no longer supported, as it is incompatible with shiny 0.13.0.

This is a very good thing, because the shiny modules system is superior to what is being done in shinychord.

Instead, you are referred to shinypod, a package for shiny modules that will do everything that shinychord does (did?) - but it is cleaner, more understandable, and more maintainable.

Introduction

The purpose of this package is to propose a convention for reusable shiny modules.

This might be useful for shiny code where:

One benefit is that the server logic for a given module is encapsulated, while exposing only a few reactive values and the ui elements.

tl;dr

Structure of a shinychord

A shinychord is simply a function that takes id as its argument. It returns a list with three items:

As you might imagine, the names on this list are inspired by the model-view-controller paradigm.

There is nothing mandatory or scared about the names of these elements, it is only a proposed convention. Furthermore, the elements of each shiny::taglist are available if you want to select or arrange them differently.

Example

Let's say we wanted a shinychord to upload and parse a delimited file. We start with a template for a shinychord:

ch_upload_parse <- function(id){

  # controller
  ui_controller <- shiny::tagList()

  # view
  ui_view <- shiny::tagList()

  # model
  server_model <- function(
    input, output, session
  ){

  }

  list(
    ui_controller = ui_controller,
    ui_view = ui_view,
    server_model = server_model
  )
}

Over the next few sections, we show how the parts of the template are filled out so that we end with a complete shinychord.

Naming function

One of the challenges we have in composing shiny applications is a way to keep the names unique among each of the elements.

To help us, I put a function at the top of the shinychord:

id_name <- function(...){
  paste(list(id, ...), collapse = "_")
}

This function will be very handy as we define the inputs and outputs.

Controller

We could make this as fancy as we like, but let's consider a simple set of controls, comprised of a file-upload input and a select input (to choose a delimiter)

ui_controller <- shiny::tagList()

id_controller_file <- id_name("controller", "file")
ui_controller$file <-
  shiny::fileInput(
    inputId = id_controller_file,
    label = "File",
    accept = c("text/csv", ".csv", "text/comma-separated-values", "text/plain")
  )

You can see that we use the id_name() function to combine the id string (supplied as an argument to the shiny chord) with the local identifier.

We also want an input to choose the delimiter:

id_controller_delim <- id_name("controller", "delim")
ui_controller$delim <-
  shiny::selectizeInput(
    inputId = id_controller_delim,
    label = "Delimiter",
    choices = c(Comma = ",", Semicolon = ";", Tab = "\t"),
    selected = ";"
  )

View

We will want a couple of view elements - one to preview the text that has been uploaded, and another to preview the parsed dataframe.

ui_view <- shiny::tagList()

# shows the raw text of the file (first few lines)
id_view_text <- id_name("view", "text")
ui_view$text <- shiny::verbatimTextOutput(id_view_text)

# shows a glimpse of the parsed data-frame
id_view_data <- id_name("view", "data")
ui_view$data <- shiny::verbatimTextOutput(id_view_data)

Model

A couple of notes here:

server_model <- function(
  input, output, session,
  rctval_data, item_data
){

  ## reactives

  # reactive to read in the raw text from the file-specification input
  rct_txt <- reactive({

    shiny::validate(
      shiny::need(env$input[[id_controller_file]], "File not selected")
    )

    infile <- input[[id_controller_file]]$datapath

    readr::read_file(infile)
  })

  ## observers

  # this needs to be wrapped in a reactive expression
  observe({
    rctval_data[[item_data]] <-
      readr::read_delim(
        file = rct_txt(),
        delim = input[[id_controller_delim]]
      )
  })

  ## outputs

  # sets the output for the raw text
  output[[id_view_text]] <-
    renderText({

      shiny::validate(
        shiny::need(rct_txt(), "File did not load properly")
      )

      h <- rct_txt()
      h <- readr::read_lines(h, n_max = 10)

      paste(h, collapse = "\n")
    })


  # sets the output for the parsed dataframe
  output[[id_view_data]] <-
    renderPrint({

      shiny::validate(
        shiny::need(rctval_data[[item_data]], "No data")
      )

      dplyr::glimpse(rctval_data[[item_data]])
    })

}

Completed shinychord

So here are all the elements from above, combined into a shinychord function:

ch_upload_parse <- function(id){

  id_name <- function(...){
    paste(list(id, ...), collapse = "_")
  }

  # controller
  ui_controller <- shiny::tagList()

  id_controller_file <- id_name("controller", "file")
  ui_controller$file <-
    shiny::fileInput(
      inputId = id_controller_file,
      label = "File",
      accept = c("text/csv", ".csv", "text/comma-separated-values", "text/plain")
    )

  id_controller_delim <- id_name("controller", "delim")
  ui_controller$delim <-
    shiny::selectizeInput(
      inputId = id_controller_delim,
      label = "Delimiter",
      choices = c(Comma = ",", Semicolon = ";", Tab = "\t"),
      selected = ";"
    )

  # view
  ui_view <- shiny::tagList()

  # shows the raw text of the file (first few lines)
  id_view_text <- id_name("view", "text")
  ui_view$text <- shiny::verbatimTextOutput(id_view_text)

  # shows a glimpse of the parsed data-frame
  id_view_data <- id_name("view", "data")
  ui_view$data <- shiny::verbatimTextOutput(id_view_data)

  # model
server_model <- function(
  input, output, session,
  rctval_data, item_data
){

  ## reactives

  # reactive to read in the raw text from the file-specification input
  rct_txt <- reactive({

    shiny::validate(
      shiny::need(input[[id_controller_file]], "File not selected")
    )

    infile <- input[[id_controller_file]]$datapath

    readr::read_file(infile)
  })

  ## observers

  # this needs to be wrapped in a reactive expression
  observe({
    rctval_data[[item_data]] <-
      readr::read_delim(
        file = rct_txt(),
        delim = input[[id_controller_delim]]
      )
  })

  ## outputs

  # sets the output for the raw text
  output[[id_view_text]] <-
    renderText({

      shiny::validate(
        shiny::need(rct_txt(), "File did not load properly")
      )

      h <- rct_txt()
      h <- readr::read_lines(h, n_max = 10)

      paste(h, collapse = "\n")
    })


  # sets the output for the parsed dataframe
  output[[id_view_data]] <-
    renderPrint({

      shiny::validate(
        shiny::need(rctval_data[[item_data]], "No data")
      )

      dplyr::glimpse(rctval_data[[item_data]])
    })

}  
  list(
    ui_controller = ui_controller,
    ui_view = ui_view,
    server_model = server_model
  )
}

Using the shinychord in an app

Here's where we can see a benefit of this approach, as all the interal complexity of the shinychord has been encapuslated, exposing only the bits we need.

For the shiny developer, instead of developing a file-parser for every occasion, or cutting and pasting from a template (and managing the id's), she or he can use a shinychord - set an id in one place, and create a reactive value into which the server_model function can leave the resulting dataframe.

The shinychord function can be created once and used everywhere.

library("shiny")
library("readr")
library("dplyr")

chord_parse <- ch_upload_parse("parse")

shinyApp(

  ui = fluidPage(
    sidebarLayout(
      sidebarPanel(chord_parse$ui_controller),
      mainPanel(chord_parse$ui_view)
    )
  ),

  server = function(input, output, session){

    rctval <- reactiveValues(data_csv = NULL)

    chord_parse$server_model(
      input, output, session,
      rctval_data = rctval, item_data = "data_csv"
    )

    observe(print(rctval$data_csv))

  }

)


ijlyttle/shinychord documentation built on May 18, 2019, 3:41 a.m.