shinyMobile 2.0.0: a preview

shiny
Author

Veerle van Leemput and David Granjon

Published

May 13, 2024

shinyMobile has been enabling the creation of exceptional R Shiny apps for both iOS and Android for nearly five years, thanks to the impressive open-source Framework7 template that drives its capabilities.

This year shinyMobile gets a major update to v2.2.0. I’d like to warmly thank Veerle van Leemput and Michael S. Czahor from AthlyticZ for providing significant support during this marathon.

What’s new

shinyMobile 1.0.0 and above have been running on an old version of Framework7 (v5). shinyMobile 2.0.0 has been upgraded to run the newer Framework7 v8. With this comes a significant number of changes, but we believe these are all for the best!

Major changes

New multi pages experimental support

We are very excited to bring this feature out for this new release. Under the hood, this is possible owing to the {brochure} package from Colin Fay as well as the internal Framework7 router component.

What does this mean?

You can now develop real multi pages Shiny applications and have different url endpoints and redirections. For instance, https://my-app/home can be the home page while https://my-app/settings brings to the settings page.

How does this work?

At the time of writting of this blog post, you must install a patched {brochure} version with devtools::install_github("DivadNojnarg/brochure").

In the below code, we basically have 3 pages having their own content and a common layout for consistency. The router ensure beautiful transitions from one page to another. We invite you to look at the getting started article which provides more technical details.

Code
library(shiny)
# Needs a specific version of brochure for now.
# This allows to pass wrapper functions with options
# as list. We need it because of the f7Page options parameter
# and to pass the routes list object for JS.
# devtools::install_github("DivadNojnarg/brochure")
library(brochure)
library(shinyMobile)

# Allows to use the app on a server like 
# shinyapps.io where basepath is /app_name
# instead of "/" or "".
make_link <- function(path = NULL, basepath = "") {
  if (is.null(path)) {
    if (nchar(basepath) > 0) {
      return(basepath)
    } else {
      return("/")
    }
  }
  sprintf("%s/%s", basepath, path)
}

links <- lapply(2:3, function(i) {
  tags$li(
    f7Link(
      routable = TRUE,
      label = sprintf("Link to page %s", i),
      href = make_link(i)
    )
  )
})

page_1 <- function() {
  page(
    href = "/",
    ui = function(request) {
      shiny::tags$div(
        class = "page",
        # top navbar goes here
        f7Navbar(title = "Home page"),
        # NOTE: when the main toolbar is enabled in
        # f7MultiLayout, we can't use individual page toolbars.
        # f7Toolbar(
        #  position = "bottom",
        #  tags$a(
        #    href = "/2",
        #    "Second page",
        #    class = "link"
        #  )
        # ),
        # Page content
        tags$div(
          class = "page-content",
          f7List(
            inset = TRUE,
            strong = TRUE,
            outline = TRUE,
            dividers = TRUE,
            mode = "links",
            links
          ),
          f7Block(
            f7Text("text", "Text input", "default"),
            f7Select("select", "Select", colnames(mtcars)),
            textOutput("res"),
            textOutput("res2")
          )
        )
      )
    }
  )
}

page_2 <- function() {
  page(
    href = "/2",
    ui = function(request) {
      shiny::tags$div(
        class = "page",
        # top navbar goes here
        f7Navbar(
          title = "Second page",
          # Allows to go back to main
          leftPanel = tagList(
            tags$a(
              href = make_link(),
              class = "link back",
              tags$i(class = "icon icon-back"),
              tags$span(
                class = "if-not-md",
                "Back"
              )
            )
          )
        ),
        shiny::tags$div(
          class = "page-content",
          f7Block(f7Button(inputId = "update", label = "Update stepper")),
          f7List(
            strong = TRUE,
            inset = TRUE,
            outline = FALSE,
            f7Stepper(
              inputId = "stepper",
              label = "My stepper",
              min = 0,
              max = 10,
              size = "small",
              value = 4,
              wraps = TRUE,
              autorepeat = TRUE,
              rounded = FALSE,
              raised = FALSE,
              manual = FALSE
            )
          ),
          f7Block(textOutput("test"))
        )
      )
    }
  )
}

page_3 <- function() {
  page(
    href = "/3",
    ui = function(request) {
      shiny::tags$div(
        class = "page",
        # top navbar goes here
        f7Navbar(
          title = "Third page",
          # Allows to go back to main
          leftPanel = tagList(
            tags$a(
              href = make_link(),
              class = "link back",
              tags$i(class = "icon icon-back"),
              tags$span(
                class = "if-not-md",
                "Back"
              )
            )
          )
        ),
        shiny::tags$div(
          class = "page-content",
          f7Block("Nothing to show yet ...")
        )
      )
    }
  )
}

brochureApp(
  basepath = make_link(),
  # Pages
  page_1(),
  page_2(),
  page_3(),
  # Important: in theory brochure makes
  # each page having its own shiny session/ server function.
  # That's not what we want here so we'll have
  # a global server function.
  server = function(input, output, session) {
    output$res <- renderText(input$text)
    output$res2 <- renderText(input$select)
    output$test <- renderText(input$stepper)

    observeEvent(input$update, {
      updateF7Stepper(
        inputId = "stepper",
        value = 0.1,
        step = 0.01,
        size = "large",
        min = 0,
        max = 1,
        wraps = FALSE,
        autorepeat = FALSE,
        rounded = TRUE,
        raised = TRUE,
        color = "pink",
        manual = TRUE,
        decimalPoint = 2
      )
    })
  },
  wrapped = f7MultiLayout,
  wrapped_options = list(
    basepath = make_link(),
    # Common toolbar
    toolbar = f7Toolbar(
      f7Link(icon = f7Icon("house"), href = make_link(), routable = TRUE)
    ),
    options = list(
      dark = TRUE,
      theme = "md",
      routes = list(
        # Important: don't remove keepAlive
        # for pages as this allows
        # to save the input state when switching
        # between pages. If FALSE, each time a page is
        # changed, inputs are reset.
        list(path = make_link(), url = make_link(), name = "home", keepAlive = TRUE),
        list(path = make_link("2"), url = make_link("2"), name = "2", keepAlive = TRUE),
        list(path = make_link("3"), url = make_link("3"), name = "3", keepAlive = TRUE)
      )
    )
  )
)

Updated material design style

By updating to the latest Framework7 version, we now benefit from a totally revamped Android (md) design, which looks more modern.

#| standalone: true
#| components: [viewer]
#| column: screen-inset-shaded
#| viewerHeight: 800
library(shiny)
library(shinyMobile)

# source modules
e <- environment()
path <- system.file("examples/gallery/tabs/", package = "shinyMobile")
sapply(
  list.files(
    path,
    include.dirs = FALSE,
    pattern = ".R",
    ignore.case = TRUE
  ),
  function(f) {
    source(file.path(path, f), local = e)
  }
)

app_options <- list(
  theme = "md",
  dark = TRUE,
  filled = FALSE,
  preloader = TRUE,
  color = "#007aff",
  navbar = list(
    hideOnPageScroll = TRUE,
    mdCenterTitle = TRUE
  )
)

# shiny app
shinyApp(
  ui = f7Page(
    allowPWA = TRUE,
    options = app_options,
    f7TabLayout(
      title = "shinyMobile Gallery",
      messagebar = f7MessageBar(inputId = "mymessagebar",
                                placeholder = "Message"),
      panels = tagList(
        f7Panel(
          id = "panelLeft",
          title = "Left Panel",
          side = "left",
          f7Block("A panel with push effect"),
          f7PanelMenu(
            inset = TRUE,
            outline = TRUE,
            # Use items as tab navigation only
            f7PanelItem(
              tabName = "tabset-Inputs",
              title = "Input tabs",
              icon = f7Icon("largecircle_fill_circle"),
              active = TRUE
            ),
            f7PanelItem(
              tabName = "tabset-FABs",
              title = "Buttons tabs",
              icon = f7Icon("largecircle_fill_circle")
            )
          ),
          effect = "push",
          options = list(swipe = TRUE)
        ),
        f7Panel(
          title = "Right Panel",
          side = "right",
          f7Radio(
            inputId = "theme",
            label = "Theme",
            choices = c("md", "ios"),
            selected = app_options$theme
          ),
          f7Radio(
            inputId = "dark",
            label = "Mode",
            choices = c("dark", "light"),
            selected = ifelse(app_options$dark, "dark", "light")
          ),
          f7Radio(
            inputId = "color",
            label = "Color",
            choices = getF7Colors(),
            selected = "primary"
          ),
          effect = "floating",
          options = list(swipe = TRUE)
        )
      ),
      navbar = f7Navbar(
        title = "shinyMobile Gallery",
        hairline = TRUE,
        leftPanel = TRUE,
        rightPanel = TRUE
      ),
      f7Login(
        id = "loginPage",
        title = "You really think you can go here?",
        footer = "This section simulates an authentication process. There
        is actually no user and password database. Put whatever you want but
        don't leave blank!",
        startOpen = FALSE
      ),
      # recover the color picker input and update the text background
      # color accordingly.
      tags$script(
        "$(function() {
          Shiny.addCustomMessageHandler('text-color', function(message) {
            $('#colorPickerVal').css('background-color', message);
          });

          // toggle message bar based on the currently selected tab
          Shiny.addCustomMessageHandler('toggleMessagebar', function(message) {
            if (message === 'chat') {
              $('#mymessagebar').show();
              $('.toolbar.tabLinks').hide();
            } else {
              $('#mymessagebar').hide();
              $('.toolbar.tabLinks').show();
            }
          });
        });
        "
      ),
      f7Tabs(
        id = "tabset",
        animated = FALSE,
        swipeable = TRUE,
        tabInputs,
        tabBtns,
        tabCards,
        tabLists,
        tabText,
        tabInfo,
        tabOthers
      )
    )
  ),
  server = function(input, output, session) {

    # update theme
    observeEvent(input$theme, ignoreInit = TRUE, {
      updateF7App(
        options = list(
          theme = input$theme
        )
      )
    })

    # update mode
    observeEvent(input$dark, ignoreInit = TRUE, {
      updateF7App(
        options = list(
          dark = ifelse(input$dark == "dark", TRUE, FALSE)
        )
      )
    })

    # update color
    observeEvent(input$color, ignoreInit = TRUE, {
      updateF7App(
        options = list(
          color = input$color
        )
      )
    })

    # input validation
    observe({
      validateF7Input(inputId = "text", info = "Whatever")
      validateF7Input(
        inputId = "password",
        pattern = "[0-9]*",
        error = "Only numbers please!"
      )
    })

    # toggle message bar: should only be dislayed when on the messages tab
    observeEvent(input$tabset, {
      session$sendCustomMessage(type = "toggleMessagebar", input$tabset)
      session$sendCustomMessage(type = "toggleSearchbar", input$tabset)
    })

    # user send new message
    observeEvent(input[["mymessagebar-send"]], {
      updateF7Messages(
        id = "mymessages",
        list(
          f7Message(
            text = input$mymessagebar,
            name = "David",
            type = "sent",
            header = "Message Header",
            footer = "Message Footer",
            textHeader = "Text Header",
            textFooter = "text Footer",
            avatar = "https://cdn.framework7.io/placeholder/people-100x100-7.jpg"
          )
        )
      )
    })

    # fake to receive random messages
    observe({
      invalidateLater(5000)
      names <- c("Victor", "John")
      name <- sample(names, 1)

      updateF7Messages(
        id = "mymessages",
        list(
          f7Message(
            text = "Message",
            name = name,
            type = "received",
            avatar = "https://cdn.framework7.io/placeholder/people-100x100-9.jpg"
          )
        )
      )
    })

    # trigger for login
    trigger <- reactive({
      req(input$tabset == "chat")
    })
    # login server module
    f7LoginServer(
      id = "loginPage",
      ignoreInit = TRUE,
      trigger = trigger
    )

    output$sin <- renderPlot(plot(sin, -pi, 2 * pi))
    output$cos <- renderPlot(plot(cos, -pi, 2 * pi))

    output$text <- renderPrint(input$text)
    output$password <- renderPrint(input$password)
    output$textarea <- renderPrint(input$textarea)
    output$slider <- renderPrint(input$sliderInput)
    output$sliderRange <- renderPrint(input$sliderRangeInput)
    output$stepper <- renderPrint(input$stepper)
    output$check <- renderPrint(input$check)
    output$checkgroup <- renderPrint(input$checkgroup)
    output$checkgroup2 <- renderPrint(input$checkgroup2)
    output$radio <- renderPrint(input$radio)
    output$radio2 <- renderPrint(input$radio2)
    output$toggle <- renderPrint(input$toggle)
    output$select <- renderPrint(input$select)
    output$smartdata <- renderTable(
      {
        head(mtcars[, c("mpg", input$smartsel), drop = FALSE])
      },
      rownames = TRUE
    )
    output$dateval <- renderPrint(input$mydatepicker)
    output$autocompleteval <- renderPrint(input$myautocomplete)

    lapply(1:12, function(i) {
      output[[paste0("res", i)]] <- renderText(paste0("Button", i, " is ", input[[paste0("btn", i)]]))
    })
    output$pickerval <- renderText(input$mypicker)
    output$colorPickerVal <- renderPrint(input$mycolorpicker$hex)

    # send the color picker input to JS
    observeEvent(input$mycolorpicker, {
      session$sendCustomMessage(type = "text-color", message = input$mycolorpicker$hex)
    })


    # popup
    output$popupContent <- renderPrint(input$popupText)

    observeEvent(input$togglePopup, {
      f7Popup(
        id = "popup1",
        title = "My first popup",
        f7Block(
          p("Popup can push the view behind. By default it has effect only when
            'safe-area-inset-top' is more than zero (iOS fullscreen webapp or iOS cordova app)"),
          f7Text(inputId = "popupText",
                 label = "Popup content",
                 value = "This is my first popup, I swear!"),
          verbatimTextOutput("popupContent")
        )
      )
    })

    observeEvent(input$popup1, {
      if (input$tabset == "Popups") {
        popupStatus <- if (input$popup1) "opened" else "closed"
        f7Toast(
          position = "top",
          text = paste("Popup is", popupStatus)
        )
      }
    })

    # sheet plot
    output$sheetPlot <- renderPlot({
      hist(rnorm(input$sheetObs))
    })

    observeEvent(input$toggleSheet, {
      updateF7Sheet(id = "sheet1")
    })

    # notifications
    lapply(1:3, function(i) {
      observeEvent(input[[paste0("goNotif", i)]], {
        icon <- if (i %% 2 == 0) f7Icon("bolt_fill") else NULL

        f7Notif(
          text = "test",
          icon = icon,
          title = paste("Notification", i),
          subtitle = "A subtitle",
          titleRightText = i
        )
      })
    })

    # Dialogs
    # notifications
    lapply(1:3, function(i) {
      observeEvent(input[[paste0("goDialog", i)]], {
        if (i == 1) {
          f7Dialog(
            title = "Dialog title",
            text = "This is an alert dialog"
          )
        } else if (i == 2) {
          f7Dialog(
            id = "confirmDialog",
            title = "Dialog title",
            type = "confirm",
            text = "This is an confirm dialog"
          )
        } else if (i == 3) {
          f7Dialog(
            id = "promptDialog",
            title = "Dialog title",
            type = "prompt",
            text = "This is a prompt dialog"
          )
        }
      })
    })

    observeEvent(input$confirmDialog, {
      f7Toast(text = paste("Alert input is:", input$confirmDialog))
    })

    output$promptres <- renderUI({
      if (is.null(input$promptDialog)) {
        f7BlockTitle(title = "Click on dialog button 3")
      }
      f7BlockTitle(title = input$promptDialog)
    })

    # popovers
    observeEvent(input$popoverButton, {
      addF7Popover(
        id = "popoverButton",
        options = list(content = "This is a f7Button")
      )
    })

    # toasts
    observeEvent(input$toast, {
      f7Toast(
        position = "bottom",
        text = "I am a toast. Eat me!"
      )
    })

    # action sheet
    observeEvent(input$goActionSheet, {
      f7ActionSheet(
        grid = TRUE,
        id = "action1",
        buttons = list(
          list(
            text = "Notification",
            icon = f7Icon("info"),
            color = NULL
          ),
          list(
            text = "Dialog",
            icon = f7Icon("lightbulb_fill"),
            color = NULL
          )
        )
      )
    })

    observeEvent(input$action1_button, {
      if (input$action1_button == 1) {
        f7Notif(
          text = "You clicked on the first button",
          icon = f7Icon("bolt_fill"),
          title = "Notification",
          titleRightText = "now"
        )
      } else if (input$action1_button == 2) {
        f7Dialog(
          id = "actionSheetDialog",
          title = "Click me to launch a Toast!",
          type = "confirm",
          text = "You clicked on the second button"
        )
      }
    })

    observeEvent(input$swipeAction_button, {
      if (input$swipeAction_button == 1) {
        f7Notif(
          text = "You clicked on the first button",
          icon = f7Icon("bolt_fill"),
          title = "Notification",
          titleRightText = "now"
        )
      } else if (input$swipeAction_button == 2) {
        f7Dialog(
          id = "actionSheetDialog",
          title = "Click me to launch a Toast!",
          type = "confirm",
          text = "You clicked on the second button"
        )
      }
    })

    observeEvent(input$actionSheetDialog, {
      f7Toast(text = paste("Alert input is:", input$actionSheetDialog))
    })

    # update progress bar
    observeEvent(input$updatepg1, {
      updateF7Progress(id = "pg1", value = input$updatepg1)
    })

    # update gauge
    observeEvent(input$updategauge1, {
      updateF7Gauge(id = "mygauge1", value = input$updategauge1)
    })

    # expand card 3
    observeEvent(input$goCard, {
      updateF7Card(id = "card3")
    })

    # toggle accordion
    observeEvent(input$goAccordion, {
      updateF7Accordion(
        id = "accordion1",
        selected = 1
      )
    })

    # update panel
    observeEvent(input$goPanel, {
      updateF7Panel(id = "panelLeft")
    })

    # swipeout
    observeEvent(input$swipeNotif, {
      f7Notif(
        text = "test",
        icon = f7Icon("bolt_fill"),
        title = "Notification",
        subtitle = "A subtitle",
        titleRightText = "now"
      )
    })

    observeEvent(input$swipeAlert, {
      f7Dialog(
        title = "Dialog title",
        text = "This is an alert dialog"
      )
    })

    observeEvent(input$swipeActionSheet, {
      f7ActionSheet(
        grid = TRUE,
        id = "swipeAction",
        buttons = list(
          list(
            text = "Notification",
            icon = f7Icon("info"),
            color = NULL
          ),
          list(
            text = "Dialog",
            icon = f7Icon("lightbulb_fill"),
            color = NULL
          )
        )
      )
    })

    # preloaders
    observeEvent(input$showLoader, {
      showF7Preloader(target = "#preloaderPlot", color = "blue")
      Sys.sleep(2)
      hideF7Preloader(target = "#preloaderPlot")
    })
    output$preloaderPlot <- renderPlot({
      hist(rnorm(100))
    })

    # photo browser
    observeEvent(input$togglePhoto, {
      f7PhotoBrowser(
        theme = "dark",
        type = "standalone",
        photos = c(
          "https://cdn.framework7.io/placeholder/sports-1024x1024-1.jpg",
          "https://cdn.framework7.io/placeholder/sports-1024x1024-2.jpg",
          "https://cdn.framework7.io/placeholder/sports-1024x1024-3.jpg"
        )
      )
    })

    # Menus
    observeEvent(input$toggleMenu, {
      updateF7MenuDropdown("menu1")
    })

    observeEvent(input$menuItem1, {
      f7Notif(text = "Well done!")
    })

    # skeleton
    observe({
      invalidateLater(4000)
      f7Skeleton(".skeleton-list", "fade", 2)
    })

    # Treeview
    output$treeview <- renderText({
      input$treeview
    })

    # Table
    output$table <- renderUI({
      f7Table(mtcars[1:15,])
    })

    # pull to refresh
    # observeEvent(input$ptr, {
    #
    #   ptrStatus <- if (input$ptr) "on"
    #
    #   f7Dialog(
    #     text = paste('ptr is', ptrStatus),
    #     type = "alert"
    #   )
    # })
  }
)

Refined inputs layout and style

Whenever you have multiple inputs, we now recommend to wrap all of them within f7List() so as to benefit from new styling options such as outline, inset, strong. Internally, we use a function able to detect whether the input is inside a f7List(). If this is the case, you can style this list by passing parameters like f7List(outline = TRUE, inset = TRUE, ...). If not, the input is internally wrapped in a list to have correct rendering, but no styling is possible. Besides, some inputs like f7Text() can have custom styling (add an icon, clear button, outline style), which is independent from the external list wrapper style. Hence, we don’t recommend doing f7List(outline = TRUE, f7Text(outline = TRUE)) since it won’t render well and instead use f7List(outline = TRUE, f7Text()).

Besides, independently from f7List(), some inputs having more specific styling options:

In practices, you can design a supercharged f7Text() like so:

Code
f7Text(
    inputId = "text",
    label = "Text input",
    value = "Some text",
    placeholder = "Your text here",
    style = list(
      description = "A cool text input",
      outline = TRUE,
      media = f7Icon("house"),
      clearable = TRUE,
      floating = TRUE
    )
)

This adds a description to the input (below its main content), as well as the outline style option and an icon on the left side. clearable is TRUE by default meaning that all text-based inputs can be cleared. floating is an effect that makes the label move in and out the input area depending on the content state. When empty, the label is inside and when there is text, the label is pushed outside into its usual location.

f7Stepper() and f7Toggle() label is now displayed on the left.

#| standalone: true
#| components: [viewer]
#| column: screen-inset-shaded
#| viewerHeight: 800
library(shiny)
library(shinyMobile)

shinyApp(
  ui = f7Page(
    options = list(dark = FALSE, theme = "ios"),
    title = "Inputs Layout",
    f7SingleLayout(
      navbar = f7Navbar(
        title = "Inputs Layout",
        hairline = FALSE
      ),
      f7List(
        inset = TRUE,
        dividers = TRUE,
        strong = TRUE,
        outline = FALSE,
        f7Text(
          inputId = "text",
          label = "Text input",
          value = "Some text",
          placeholder = "Your text here"
        ),
        f7TextArea(
          inputId = "textarea",
          label = "Text area input",
          value = "Some text",
          placeholder = "Your text here"
        ),
        f7Select(
          inputId = "select",
          label = "Make a choice",
          choices = 1:3,
          selected = 1
        ),
        f7AutoComplete(
          inputId = "myautocomplete",
          placeholder = "Some text here!",
          openIn = "dropdown",
          label = "Type a fruit name",
          choices = c(
            "Apple", "Apricot", "Avocado", "Banana", "Melon",
            "Orange", "Peach", "Pear", "Pineapple"
          )
        ),
        f7Stepper(
          inputId = "stepper",
          label = "My stepper",
          min = 0,
          color = "default",
          max = 10,
          value = 4
        ),
        f7Toggle(
          inputId = "toggle",
          label = "Toggle me"
        ),
        f7Picker(
          inputId = "picker",
          placeholder = "Some text here!",
          label = "Picker Input",
          choices = c("a", "b", "c"),
          options = list(sheetPush = TRUE)
        ),
        f7DatePicker(
          inputId = "date",
          label = "Pick a date",
          value = Sys.Date()
        ),
        f7ColorPicker(
          inputId = "mycolorpicker",
          placeholder = "Some text here!",
          label = "Select a color"
        )
      ),
      f7CheckboxGroup(
        inputId = "checkbox",
        label = "Checkbox group",
        choices = c("a", "b", "c"),
        selected = "a",
        style = list(
          inset = TRUE,
          dividers = TRUE,
          strong = TRUE,
          outline = FALSE
        )
      ),
      f7Radio(
        inputId = "radio",
        label = "Radio group",
        choices = c("a", "b", "c"),
        selected = "a",
        style = list(
          inset = TRUE,
          dividers = TRUE,
          strong = TRUE,
          outline = FALSE
        )
      )
    )
  ),
  server = function(input, output) {
  }
) 
Code
library(shiny)
library(shinyMobile)

shinyApp(
  ui = f7Page(
    options = list(dark = FALSE, theme = "ios"),
    title = "Inputs Layout",
    f7SingleLayout(
      navbar = f7Navbar(
        title = "Inputs Layout",
        hairline = FALSE
      ),
      f7List(
        inset = TRUE,
        dividers = TRUE,
        strong = TRUE,
        outline = FALSE,
        f7Text(
          inputId = "text",
          label = "Text input",
          value = "Some text",
          placeholder = "Your text here"
        ),
        f7TextArea(
          inputId = "textarea",
          label = "Text area input",
          value = "Some text",
          placeholder = "Your text here"
        ),
        f7Select(
          inputId = "select",
          label = "Make a choice",
          choices = 1:3,
          selected = 1
        ),
        f7AutoComplete(
          inputId = "myautocomplete",
          placeholder = "Some text here!",
          openIn = "dropdown",
          label = "Type a fruit name",
          choices = c(
            "Apple", "Apricot", "Avocado", "Banana", "Melon",
            "Orange", "Peach", "Pear", "Pineapple"
          )
        ),
        f7Stepper(
          inputId = "stepper",
          label = "My stepper",
          min = 0,
          color = "default",
          max = 10,
          value = 4
        ),
        f7Toggle(
          inputId = "toggle",
          label = "Toggle me"
        ),
        f7Picker(
          inputId = "picker",
          placeholder = "Some text here!",
          label = "Picker Input",
          choices = c("a", "b", "c"),
          options = list(sheetPush = TRUE)
        ),
        f7DatePicker(
          inputId = "date",
          label = "Pick a date",
          value = Sys.Date()
        ),
        f7ColorPicker(
          inputId = "mycolorpicker",
          placeholder = "Some text here!",
          label = "Select a color"
        )
      ),
      f7CheckboxGroup(
        inputId = "checkbox",
        label = "Checkbox group",
        choices = c("a", "b", "c"),
        selected = "a",
        style = list(
          inset = TRUE,
          dividers = TRUE,
          strong = TRUE,
          outline = FALSE
        )
      ),
      f7Radio(
        inputId = "radio",
        label = "Radio group",
        choices = c("a", "b", "c"),
        selected = "a",
        style = list(
          inset = TRUE,
          dividers = TRUE,
          strong = TRUE,
          outline = FALSE
        )
      )
    )
  ),
  server = function(input, output) {
  }
)

Moreover, we added a new way to pass options to f7Radio() and f7CheckboxGroup(), namely f7CheckboxChoice() and f7RadioChoice() (note: you can’t use update_* functions on them yet), so that you can pass more metadata and a description to each option (instead of just the choice name in basic shiny inputs):

Code
library(shiny)
library(shinyMobile)

shinyApp(
  ui = f7Page(
    title = "Update radio",
    f7SingleLayout(
      navbar = f7Navbar(title = "Update f7Radio"),
      f7Block(
        f7Radio(
          inputId = "radio",
          label = "Custom choices",
          choices = list(
            f7RadioChoice(
              "Lorem ipsum dolor sit amet, consectetur adipiscing elit.
            Nulla sagittis tellus ut turpis condimentum,
            ut dignissim lacus tincidunt",
              title = "Choice 1",
              subtitle = "David",
              after = "March 16, 2024"
            ),
            f7RadioChoice(
              "Cras dolor metus, ultrices condimentum sodales sit
            amet, pharetra sodales eros. Phasellus vel felis tellus.
            Mauris rutrum ligula nec dapibus feugiat",
              title = "Choice 2",
              subtitle = "Veerle",
              after = "March 17, 2024"
            )
          ),
          selected = 2,
          style = list(
            outline = TRUE,
            strong = TRUE,
            inset = TRUE,
            dividers = TRUE
          )
        ),
        textOutput("res")
      )
    )
  ),
  server = function(input, output, session) {
    output$res <- renderText(input$radio)
  }
)

New f7Treeview() component

The new release welcomes a brand new input widget. As its name suggests, f7Treewiew() enables sorting items hierarchically within a collapsible nested list of items. This is ideal, for instance, to select files within multiple folders, as an alternative to the classic fileInput().

Code
library(shiny)
library(shinyMobile)

shinyApp(
  ui = f7Page(
    title = "My app",
    f7SingleLayout(
      navbar = f7Navbar(title = "f7Treeview"),
      # group treeview with selectable items
      f7BlockTitle("Selectable items"),
      f7Block(
        f7Treeview(
          id = "treeview1",
          selectable = TRUE,
          f7TreeviewGroup(
            title = "Selected images",
            icon = f7Icon("folder_fill"),
            itemToggle = TRUE,
            lapply(1:3, function(i) f7TreeviewItem(label = paste0("image", i, ".png"),
                                                   icon = f7Icon("photo_fill")))
          )
        ),
        textOutput("selected")
      ),

      # group treeview with checkbox items
      f7BlockTitle("Checkbox"),
      f7Block(
        f7Treeview(
          id = "treeview2",
          withCheckbox = TRUE,
          f7TreeviewGroup(
            title = "Selected images",
            icon = f7Icon("folder_fill"),
            itemToggle = TRUE,
            lapply(1:3, function(i) f7TreeviewItem(label = paste0("image", i, ".png"),
                                                   icon = f7Icon("photo_fill")))
          )
        ),
        textOutput("selected2")
      )
    )
  ),
  server = function(input, output) {
    output$selected <- renderText(input$treeview1)
    output$selected2 <- renderText(input$treeview2)
  }
)

New f7Form()

Shiny does not provide HTML forms handling out of the box (a form being composed of multiple input elements). That’s why we introduce f7Form(). Contrary to basic shiny inputs, we don’t get one input value per element but a single input value with a nested list for all inputs within the form, thereby allowing a reduction in the number of inputs on the server side. updateF7Form() can quickly update any input from the form. As a side note, the current list of supported inputs is:

Code
library(shiny)
library(shinyMobile)

shinyApp(
  ui = f7Page(
    f7SingleLayout(
      navbar = f7Navbar(title = "Inputs form"),
      f7Block(f7Button("update", "Click me")),
      f7BlockTitle("A list of inputs in a form"),
      f7List(
        inset = TRUE,
        dividers = FALSE,
        strong = TRUE,
        f7Form(
          id = "myform",
          f7Text(
            inputId = "text",
            label = "Text input",
            value = "Some text",
            placeholder = "Your text here",
            style = list(
              description = "A cool text input",
              outline = TRUE,
              media = f7Icon("house"),
              clearable = TRUE,
              floating = TRUE
            )
          ),
          f7TextArea(
            inputId = "textarea",
            label = "Text Area",
            value = "Lorem ipsum dolor sit amet, consectetur
              adipiscing elit, sed do eiusmod tempor incididunt ut
              labore et dolore magna aliqua",
            placeholder = "Your text here",
            resize = TRUE,
            style = list(
              description = "A cool text input",
              outline = TRUE,
              media = f7Icon("house"),
              clearable = TRUE,
              floating = TRUE
            )
          ),
          f7Password(
            inputId = "password",
            label = "Password:",
            placeholder = "Your password here",
            style = list(
              description = "A cool text input",
              outline = TRUE,
              media = f7Icon("house"),
              clearable = TRUE,
              floating = TRUE
            )
          )
        )
      ),
      verbatimTextOutput("form")
    )
  ),
  server = function(input, output, session) {
    output$form <- renderPrint(input$myform)

    observeEvent(input$update, {
      updateF7Form(
        "myform",
        data = list(
          "text" = "New text",
          "textarea" = "New text area",
          "password" = "New password"
        )
      )
    })
  }
)

Breaking changes

Some components have disappeared from Framework7 and we had to deprecate them as they no longer work. Other long time deprecated shinyMobile elements are also removed to cleanup the codebase. We invite you to review the changelog to see a list of all changes in this release.

Soft deprecation

Some function parameters have changed and are now deprecated with lifecycle. You’ll see a warning message if you use them and we invite you to accordingly update your code.

Conclusion

Since the release of shiny in 2012, many packages have been released that substantially improve its layout and design such as bslib, bs4Dash or shiny.fluent. Yet not much was done for mobile development. shinyMobile tries to fill this gap by exposing people to the rich Framework7 mobile-first template. Coupled with progressive web app support (PWA), you can run Shiny apps on a mobile that look very close to native apps with a desktop icon, a launch screen and running fullscreen, that is without the web browser navigation bar. By including an experimental implementation of the multipage navigation as described earlier, we move one step closer to the native apps. Finally, owing to the progress on the webR side, it isn’t impossible that one day, we might run a totally offline Shiny app on mobile.