diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index eb7cb9a..c9c610f 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -23,6 +23,15 @@ jobs: with: use-public-rspm: true - uses: r-lib/actions/setup-r-dependencies@v2 + with: + extra-packages: | + posit-dev/r-shinylive + any::knitr + any::rmarkdown + any::downlit + any::xml2 + any::shinyMobile + cache-version: 2 - name: Render Quarto Project uses: quarto-dev/quarto-actions/render@v2 diff --git a/.gitignore b/.gitignore index a709f5a..1312f7b 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ /.quarto/ docs .DS_Store + +/.luarc.json diff --git a/DESCRIPTION b/DESCRIPTION index d20fe86..41c6a3c 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -13,3 +13,5 @@ Imports: xml2, rmarkdown, shinylive +Remotes: + posit-dev/r-shinylive diff --git a/_freeze/posts/2024-05-13-shinyMobile-2.0.0/index/execute-results/html.json b/_freeze/posts/2024-05-13-shinyMobile-2.0.0/index/execute-results/html.json index 9bdda22..407a477 100644 --- a/_freeze/posts/2024-05-13-shinyMobile-2.0.0/index/execute-results/html.json +++ b/_freeze/posts/2024-05-13-shinyMobile-2.0.0/index/execute-results/html.json @@ -1,8 +1,8 @@ { - "hash": "24a7112ca29a85f3ce04e9de3a190dc6", + "hash": "bce75eea68c3e7e4f988b100e4136c57", "result": { "engine": "knitr", - "markdown": "---\nlayout: post\ntitle: \"shinyMobile 2.0.0: a preview\"\nimage: logo.png\nauthor: Veerle van Leemput and David Granjon\ndate: '2024-05-13'\ncategories:\n - shiny\nformat: \n html:\n code-fold: true\nfilters: \n - shinylive\n---\n\n\n![](logo.png){width=25% fig-align=\"center\"}\n\nshinyMobile 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](https://framework7.io/) that drives its capabilities.\n\nThis year shinyMobile gets a major update to v2.2.0. I'd like to warmly thank [Veerle van Leemput](https://hypebright.nl/) and Michael S. Czahor from [AthlyticZ](https://linktr.ee/athlyticz) for providing significant support during this marathon.\n\n# What's new\n\nshinyMobile 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](https://github.com/RinteRface/shinyMobile/blob/69da6ca46984bf6c73e2dc32ff8d9f415ec36a30/NEWS.md), but we believe these are all for the best!\n\n## Major changes\n\n### New multi pages experimental support\n\nWe are very excited to bring this feature out for this new release. Under the hood, this is possible owing to the `{brochure}` [package](https://github.com/ColinFay/brochure) from \n[Colin Fay](https://github.com/ColinFay) as well as the internal Framework7 [router](https://framework7.io/docs/view) component.\n\n#### What does this mean? \n\nYou 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.\n\n#### How does this work?\n\nAt the time of writting of this blog post, you must install a patched `{brochure}` version with `devtools::install_github(\"DivadNojnarg/brochure\")`.\n\nIn 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](https://shinymobile.rinterface.com/articles/multipages) which provides more technical details.\n\n\n::: {.cell}\n\n```{.r .cell-code}\nlibrary(shiny)\n# Needs a specific version of brochure for now.\n# This allows to pass wrapper functions with options\n# as list. We need it because of the f7Page options parameter\n# and to pass the routes list object for JS.\n# devtools::install_github(\"DivadNojnarg/brochure\")\nlibrary(brochure)\nlibrary(shinyMobile)\n\n# Allows to use the app on a server like \n# shinyapps.io where basepath is /app_name\n# instead of \"/\" or \"\".\nmake_link <- function(path = NULL, basepath = \"\") {\n if (is.null(path)) {\n if (nchar(basepath) > 0) {\n return(basepath)\n } else {\n return(\"/\")\n }\n }\n sprintf(\"%s/%s\", basepath, path)\n}\n\nlinks <- lapply(2:3, function(i) {\n tags$li(\n f7Link(\n routable = TRUE,\n label = sprintf(\"Link to page %s\", i),\n href = make_link(i)\n )\n )\n})\n\npage_1 <- function() {\n page(\n href = \"/\",\n ui = function(request) {\n shiny::tags$div(\n class = \"page\",\n # top navbar goes here\n f7Navbar(title = \"Home page\"),\n # NOTE: when the main toolbar is enabled in\n # f7MultiLayout, we can't use individual page toolbars.\n # f7Toolbar(\n # position = \"bottom\",\n # tags$a(\n # href = \"/2\",\n # \"Second page\",\n # class = \"link\"\n # )\n # ),\n # Page content\n tags$div(\n class = \"page-content\",\n f7List(\n inset = TRUE,\n strong = TRUE,\n outline = TRUE,\n dividers = TRUE,\n mode = \"links\",\n links\n ),\n f7Block(\n f7Text(\"text\", \"Text input\", \"default\"),\n f7Select(\"select\", \"Select\", colnames(mtcars)),\n textOutput(\"res\"),\n textOutput(\"res2\")\n )\n )\n )\n }\n )\n}\n\npage_2 <- function() {\n page(\n href = \"/2\",\n ui = function(request) {\n shiny::tags$div(\n class = \"page\",\n # top navbar goes here\n f7Navbar(\n title = \"Second page\",\n # Allows to go back to main\n leftPanel = tagList(\n tags$a(\n href = make_link(),\n class = \"link back\",\n tags$i(class = \"icon icon-back\"),\n tags$span(\n class = \"if-not-md\",\n \"Back\"\n )\n )\n )\n ),\n shiny::tags$div(\n class = \"page-content\",\n f7Block(f7Button(inputId = \"update\", label = \"Update stepper\")),\n f7List(\n strong = TRUE,\n inset = TRUE,\n outline = FALSE,\n f7Stepper(\n inputId = \"stepper\",\n label = \"My stepper\",\n min = 0,\n max = 10,\n size = \"small\",\n value = 4,\n wraps = TRUE,\n autorepeat = TRUE,\n rounded = FALSE,\n raised = FALSE,\n manual = FALSE\n )\n ),\n f7Block(textOutput(\"test\"))\n )\n )\n }\n )\n}\n\npage_3 <- function() {\n page(\n href = \"/3\",\n ui = function(request) {\n shiny::tags$div(\n class = \"page\",\n # top navbar goes here\n f7Navbar(\n title = \"Third page\",\n # Allows to go back to main\n leftPanel = tagList(\n tags$a(\n href = make_link(),\n class = \"link back\",\n tags$i(class = \"icon icon-back\"),\n tags$span(\n class = \"if-not-md\",\n \"Back\"\n )\n )\n )\n ),\n shiny::tags$div(\n class = \"page-content\",\n f7Block(\"Nothing to show yet ...\")\n )\n )\n }\n )\n}\n\nbrochureApp(\n basepath = make_link(),\n # Pages\n page_1(),\n page_2(),\n page_3(),\n # Important: in theory brochure makes\n # each page having its own shiny session/ server function.\n # That's not what we want here so we'll have\n # a global server function.\n server = function(input, output, session) {\n output$res <- renderText(input$text)\n output$res2 <- renderText(input$select)\n output$test <- renderText(input$stepper)\n\n observeEvent(input$update, {\n updateF7Stepper(\n inputId = \"stepper\",\n value = 0.1,\n step = 0.01,\n size = \"large\",\n min = 0,\n max = 1,\n wraps = FALSE,\n autorepeat = FALSE,\n rounded = TRUE,\n raised = TRUE,\n color = \"pink\",\n manual = TRUE,\n decimalPoint = 2\n )\n })\n },\n wrapped = f7MultiLayout,\n wrapped_options = list(\n basepath = make_link(),\n # Common toolbar\n toolbar = f7Toolbar(\n f7Link(icon = f7Icon(\"house\"), href = make_link(), routable = TRUE)\n ),\n options = list(\n dark = TRUE,\n theme = \"md\",\n routes = list(\n # Important: don't remove keepAlive\n # for pages as this allows\n # to save the input state when switching\n # between pages. If FALSE, each time a page is\n # changed, inputs are reset.\n list(path = make_link(), url = make_link(), name = \"home\", keepAlive = TRUE),\n list(path = make_link(\"2\"), url = make_link(\"2\"), name = \"2\", keepAlive = TRUE),\n list(path = make_link(\"3\"), url = make_link(\"3\"), name = \"3\", keepAlive = TRUE)\n )\n )\n )\n)\n```\n:::\n\n\n### Updated material design style\n\nBy updating to the latest Framework7 version, we now benefit from a totally revamped Android (md) design, which looks more modern.\n\n:::: {.columns}\n::: {.column width=\"20%\"}\n\n:::\n\n::: {.column width=\"60%\"}\n```{shinylive-r}\n#| standalone: true\n#| components: [viewer]\n#| column: screen-inset-shaded\n#| viewerHeight: 800\nwebr::unmount(\"/usr/lib/R/library/shinyMobile\")\nwebr::install(\n \"shinyMobile\",\n repos = c(\"https://rinterface.github.io/rinterface-wasm-cran/\")\n)\n\nlibrary(shiny)\nlibrary(shinyMobile)\n\n# source modules\ne <- environment()\npath <- system.file(\"examples/gallery/tabs/\", package = \"shinyMobile\")\nsapply(\n list.files(\n path,\n include.dirs = FALSE,\n pattern = \".R\",\n ignore.case = TRUE\n ),\n function(f) {\n source(file.path(path, f), local = e)\n }\n)\n\napp_options <- list(\n theme = \"md\",\n dark = TRUE,\n filled = FALSE,\n preloader = TRUE,\n color = \"#007aff\",\n navbar = list(\n hideOnPageScroll = TRUE,\n mdCenterTitle = TRUE\n )\n)\n\n# shiny app\nshinyApp(\n ui = f7Page(\n allowPWA = TRUE,\n options = app_options,\n f7TabLayout(\n title = \"shinyMobile Gallery\",\n messagebar = f7MessageBar(inputId = \"mymessagebar\",\n placeholder = \"Message\"),\n panels = tagList(\n f7Panel(\n id = \"panelLeft\",\n title = \"Left Panel\",\n side = \"left\",\n f7Block(\"A panel with push effect\"),\n f7PanelMenu(\n inset = TRUE,\n outline = TRUE,\n # Use items as tab navigation only\n f7PanelItem(\n tabName = \"tabset-Inputs\",\n title = \"Input tabs\",\n icon = f7Icon(\"largecircle_fill_circle\"),\n active = TRUE\n ),\n f7PanelItem(\n tabName = \"tabset-FABs\",\n title = \"Buttons tabs\",\n icon = f7Icon(\"largecircle_fill_circle\")\n )\n ),\n effect = \"push\",\n options = list(swipe = TRUE)\n ),\n f7Panel(\n title = \"Right Panel\",\n side = \"right\",\n f7Radio(\n inputId = \"theme\",\n label = \"Theme\",\n choices = c(\"md\", \"ios\"),\n selected = app_options$theme\n ),\n f7Radio(\n inputId = \"dark\",\n label = \"Mode\",\n choices = c(\"dark\", \"light\"),\n selected = ifelse(app_options$dark, \"dark\", \"light\")\n ),\n f7Radio(\n inputId = \"color\",\n label = \"Color\",\n choices = getF7Colors(),\n selected = \"primary\"\n ),\n effect = \"floating\",\n options = list(swipe = TRUE)\n )\n ),\n navbar = f7Navbar(\n title = \"shinyMobile Gallery\",\n hairline = TRUE,\n leftPanel = TRUE,\n rightPanel = TRUE\n ),\n f7Login(\n id = \"loginPage\",\n title = \"You really think you can go here?\",\n footer = \"This section simulates an authentication process. There\n is actually no user and password database. Put whatever you want but\n don't leave blank!\",\n startOpen = FALSE\n ),\n # recover the color picker input and update the text background\n # color accordingly.\n tags$script(\n \"$(function() {\n Shiny.addCustomMessageHandler('text-color', function(message) {\n $('#colorPickerVal').css('background-color', message);\n });\n\n // toggle message bar based on the currently selected tab\n Shiny.addCustomMessageHandler('toggleMessagebar', function(message) {\n if (message === 'chat') {\n $('#mymessagebar').show();\n $('.toolbar.tabLinks').hide();\n } else {\n $('#mymessagebar').hide();\n $('.toolbar.tabLinks').show();\n }\n });\n });\n \"\n ),\n f7Tabs(\n id = \"tabset\",\n animated = FALSE,\n swipeable = TRUE,\n tabInputs,\n tabBtns,\n tabCards,\n tabLists,\n tabText,\n tabInfo,\n tabOthers\n )\n )\n ),\n server = function(input, output, session) {\n\n # update theme\n observeEvent(input$theme, ignoreInit = TRUE, {\n updateF7App(\n options = list(\n theme = input$theme\n )\n )\n })\n\n # update mode\n observeEvent(input$dark, ignoreInit = TRUE, {\n updateF7App(\n options = list(\n dark = ifelse(input$dark == \"dark\", TRUE, FALSE)\n )\n )\n })\n\n # update color\n observeEvent(input$color, ignoreInit = TRUE, {\n updateF7App(\n options = list(\n color = input$color\n )\n )\n })\n\n # input validation\n observe({\n validateF7Input(inputId = \"text\", info = \"Whatever\")\n validateF7Input(\n inputId = \"password\",\n pattern = \"[0-9]*\",\n error = \"Only numbers please!\"\n )\n })\n\n # toggle message bar: should only be dislayed when on the messages tab\n observeEvent(input$tabset, {\n session$sendCustomMessage(type = \"toggleMessagebar\", input$tabset)\n session$sendCustomMessage(type = \"toggleSearchbar\", input$tabset)\n })\n\n # user send new message\n observeEvent(input[[\"mymessagebar-send\"]], {\n updateF7Messages(\n id = \"mymessages\",\n list(\n f7Message(\n text = input$mymessagebar,\n name = \"David\",\n type = \"sent\",\n header = \"Message Header\",\n footer = \"Message Footer\",\n textHeader = \"Text Header\",\n textFooter = \"text Footer\",\n avatar = \"https://cdn.framework7.io/placeholder/people-100x100-7.jpg\"\n )\n )\n )\n })\n\n # fake to receive random messages\n observe({\n invalidateLater(5000)\n names <- c(\"Victor\", \"John\")\n name <- sample(names, 1)\n\n updateF7Messages(\n id = \"mymessages\",\n list(\n f7Message(\n text = \"Message\",\n name = name,\n type = \"received\",\n avatar = \"https://cdn.framework7.io/placeholder/people-100x100-9.jpg\"\n )\n )\n )\n })\n\n # trigger for login\n trigger <- reactive({\n req(input$tabset == \"chat\")\n })\n # login server module\n f7LoginServer(\n id = \"loginPage\",\n ignoreInit = TRUE,\n trigger = trigger\n )\n\n output$sin <- renderPlot(plot(sin, -pi, 2 * pi))\n output$cos <- renderPlot(plot(cos, -pi, 2 * pi))\n\n output$text <- renderPrint(input$text)\n output$password <- renderPrint(input$password)\n output$textarea <- renderPrint(input$textarea)\n output$slider <- renderPrint(input$sliderInput)\n output$sliderRange <- renderPrint(input$sliderRangeInput)\n output$stepper <- renderPrint(input$stepper)\n output$check <- renderPrint(input$check)\n output$checkgroup <- renderPrint(input$checkgroup)\n output$checkgroup2 <- renderPrint(input$checkgroup2)\n output$radio <- renderPrint(input$radio)\n output$radio2 <- renderPrint(input$radio2)\n output$toggle <- renderPrint(input$toggle)\n output$select <- renderPrint(input$select)\n output$smartdata <- renderTable(\n {\n head(mtcars[, c(\"mpg\", input$smartsel), drop = FALSE])\n },\n rownames = TRUE\n )\n output$dateval <- renderPrint(input$mydatepicker)\n output$autocompleteval <- renderPrint(input$myautocomplete)\n\n lapply(1:12, function(i) {\n output[[paste0(\"res\", i)]] <- renderText(paste0(\"Button\", i, \" is \", input[[paste0(\"btn\", i)]]))\n })\n output$pickerval <- renderText(input$mypicker)\n output$colorPickerVal <- renderPrint(input$mycolorpicker$hex)\n\n # send the color picker input to JS\n observeEvent(input$mycolorpicker, {\n session$sendCustomMessage(type = \"text-color\", message = input$mycolorpicker$hex)\n })\n\n\n # popup\n output$popupContent <- renderPrint(input$popupText)\n\n observeEvent(input$togglePopup, {\n f7Popup(\n id = \"popup1\",\n title = \"My first popup\",\n f7Block(\n p(\"Popup can push the view behind. By default it has effect only when\n 'safe-area-inset-top' is more than zero (iOS fullscreen webapp or iOS cordova app)\"),\n f7Text(inputId = \"popupText\",\n label = \"Popup content\",\n value = \"This is my first popup, I swear!\"),\n verbatimTextOutput(\"popupContent\")\n )\n )\n })\n\n observeEvent(input$popup1, {\n if (input$tabset == \"Popups\") {\n popupStatus <- if (input$popup1) \"opened\" else \"closed\"\n f7Toast(\n position = \"top\",\n text = paste(\"Popup is\", popupStatus)\n )\n }\n })\n\n # sheet plot\n output$sheetPlot <- renderPlot({\n hist(rnorm(input$sheetObs))\n })\n\n observeEvent(input$toggleSheet, {\n updateF7Sheet(id = \"sheet1\")\n })\n\n # notifications\n lapply(1:3, function(i) {\n observeEvent(input[[paste0(\"goNotif\", i)]], {\n icon <- if (i %% 2 == 0) f7Icon(\"bolt_fill\") else NULL\n\n f7Notif(\n text = \"test\",\n icon = icon,\n title = paste(\"Notification\", i),\n subtitle = \"A subtitle\",\n titleRightText = i\n )\n })\n })\n\n # Dialogs\n # notifications\n lapply(1:3, function(i) {\n observeEvent(input[[paste0(\"goDialog\", i)]], {\n if (i == 1) {\n f7Dialog(\n title = \"Dialog title\",\n text = \"This is an alert dialog\"\n )\n } else if (i == 2) {\n f7Dialog(\n id = \"confirmDialog\",\n title = \"Dialog title\",\n type = \"confirm\",\n text = \"This is an confirm dialog\"\n )\n } else if (i == 3) {\n f7Dialog(\n id = \"promptDialog\",\n title = \"Dialog title\",\n type = \"prompt\",\n text = \"This is a prompt dialog\"\n )\n }\n })\n })\n\n observeEvent(input$confirmDialog, {\n f7Toast(text = paste(\"Alert input is:\", input$confirmDialog))\n })\n\n output$promptres <- renderUI({\n if (is.null(input$promptDialog)) {\n f7BlockTitle(title = \"Click on dialog button 3\")\n }\n f7BlockTitle(title = input$promptDialog)\n })\n\n # popovers\n observeEvent(input$popoverButton, {\n addF7Popover(\n id = \"popoverButton\",\n options = list(content = \"This is a f7Button\")\n )\n })\n\n # toasts\n observeEvent(input$toast, {\n f7Toast(\n position = \"bottom\",\n text = \"I am a toast. Eat me!\"\n )\n })\n\n # action sheet\n observeEvent(input$goActionSheet, {\n f7ActionSheet(\n grid = TRUE,\n id = \"action1\",\n buttons = list(\n list(\n text = \"Notification\",\n icon = f7Icon(\"info\"),\n color = NULL\n ),\n list(\n text = \"Dialog\",\n icon = f7Icon(\"lightbulb_fill\"),\n color = NULL\n )\n )\n )\n })\n\n observeEvent(input$action1_button, {\n if (input$action1_button == 1) {\n f7Notif(\n text = \"You clicked on the first button\",\n icon = f7Icon(\"bolt_fill\"),\n title = \"Notification\",\n titleRightText = \"now\"\n )\n } else if (input$action1_button == 2) {\n f7Dialog(\n id = \"actionSheetDialog\",\n title = \"Click me to launch a Toast!\",\n type = \"confirm\",\n text = \"You clicked on the second button\"\n )\n }\n })\n\n observeEvent(input$swipeAction_button, {\n if (input$swipeAction_button == 1) {\n f7Notif(\n text = \"You clicked on the first button\",\n icon = f7Icon(\"bolt_fill\"),\n title = \"Notification\",\n titleRightText = \"now\"\n )\n } else if (input$swipeAction_button == 2) {\n f7Dialog(\n id = \"actionSheetDialog\",\n title = \"Click me to launch a Toast!\",\n type = \"confirm\",\n text = \"You clicked on the second button\"\n )\n }\n })\n\n observeEvent(input$actionSheetDialog, {\n f7Toast(text = paste(\"Alert input is:\", input$actionSheetDialog))\n })\n\n # update progress bar\n observeEvent(input$updatepg1, {\n updateF7Progress(id = \"pg1\", value = input$updatepg1)\n })\n\n # update gauge\n observeEvent(input$updategauge1, {\n updateF7Gauge(id = \"mygauge1\", value = input$updategauge1)\n })\n\n # expand card 3\n observeEvent(input$goCard, {\n updateF7Card(id = \"card3\")\n })\n\n # toggle accordion\n observeEvent(input$goAccordion, {\n updateF7Accordion(\n id = \"accordion1\",\n selected = 1\n )\n })\n\n # update panel\n observeEvent(input$goPanel, {\n updateF7Panel(id = \"panelLeft\")\n })\n\n # swipeout\n observeEvent(input$swipeNotif, {\n f7Notif(\n text = \"test\",\n icon = f7Icon(\"bolt_fill\"),\n title = \"Notification\",\n subtitle = \"A subtitle\",\n titleRightText = \"now\"\n )\n })\n\n observeEvent(input$swipeAlert, {\n f7Dialog(\n title = \"Dialog title\",\n text = \"This is an alert dialog\"\n )\n })\n\n observeEvent(input$swipeActionSheet, {\n f7ActionSheet(\n grid = TRUE,\n id = \"swipeAction\",\n buttons = list(\n list(\n text = \"Notification\",\n icon = f7Icon(\"info\"),\n color = NULL\n ),\n list(\n text = \"Dialog\",\n icon = f7Icon(\"lightbulb_fill\"),\n color = NULL\n )\n )\n )\n })\n\n # preloaders\n observeEvent(input$showLoader, {\n showF7Preloader(target = \"#preloaderPlot\", color = \"blue\")\n Sys.sleep(2)\n hideF7Preloader(target = \"#preloaderPlot\")\n })\n output$preloaderPlot <- renderPlot({\n hist(rnorm(100))\n })\n\n # photo browser\n observeEvent(input$togglePhoto, {\n f7PhotoBrowser(\n theme = \"dark\",\n type = \"standalone\",\n photos = c(\n \"https://cdn.framework7.io/placeholder/sports-1024x1024-1.jpg\",\n \"https://cdn.framework7.io/placeholder/sports-1024x1024-2.jpg\",\n \"https://cdn.framework7.io/placeholder/sports-1024x1024-3.jpg\"\n )\n )\n })\n\n # Menus\n observeEvent(input$toggleMenu, {\n updateF7MenuDropdown(\"menu1\")\n })\n\n observeEvent(input$menuItem1, {\n f7Notif(text = \"Well done!\")\n })\n\n # skeleton\n observe({\n invalidateLater(4000)\n f7Skeleton(\".skeleton-list\", \"fade\", 2)\n })\n\n # Treeview\n output$treeview <- renderText({\n input$treeview\n })\n\n # Table\n output$table <- renderUI({\n f7Table(mtcars[1:15,])\n })\n\n # pull to refresh\n # observeEvent(input$ptr, {\n #\n # ptrStatus <- if (input$ptr) \"on\"\n #\n # f7Dialog(\n # text = paste('ptr is', ptrStatus),\n # type = \"alert\"\n # )\n # })\n }\n)\n```\n:::\n\n::: {.column width=\"20%\"}\n\n:::\n\n::::\n\n### Refined inputs layout and style\n\nWhenever 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())`. \n\nBesides, independently from `f7List()`, some inputs having more specific styling options:\n\n- `f7AutoComplete()`.\n- `f7Text()`, `f7Password()`, `f7TextArea()`.\n- `f7Select()`.\n- `f7Picker()`, `f7ColorPicker()` and `f7DatePicker()`.\n- `f7Radio()` and `f7CheckboxGroup()`.\n\nIn practices, you can design a supercharged `f7Text()` like so:\n\n\n::: {.cell}\n\n```{.r .cell-code}\nf7Text(\n inputId = \"text\",\n label = \"Text input\",\n value = \"Some text\",\n placeholder = \"Your text here\",\n style = list(\n description = \"A cool text input\",\n outline = TRUE,\n media = f7Icon(\"house\"),\n clearable = TRUE,\n floating = TRUE\n )\n)\n```\n:::\n\n\nThis 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.\n\n`f7Stepper()` and `f7Toggle()` label is now displayed on the left.\n\n:::: {.columns}\n::: {.column width=\"25%\"}\n\n:::\n\n::: {.column width=\"50%\"}\n```{shinylive-r}\n#| standalone: true\n#| components: [viewer]\n#| column: screen-inset-shaded\n#| viewerHeight: 800\nwebr::unmount(\"/usr/lib/R/library/shinyMobile\")\nwebr::install(\n \"shinyMobile\",\n repos = c(\"https://rinterface.github.io/rinterface-wasm-cran/\")\n)\n\nlibrary(shiny)\nlibrary(shinyMobile)\n\nshinyApp(\n ui = f7Page(\n options = list(dark = FALSE, theme = \"ios\"),\n title = \"Inputs Layout\",\n f7SingleLayout(\n navbar = f7Navbar(\n title = \"Inputs Layout\",\n hairline = FALSE\n ),\n f7List(\n inset = TRUE,\n dividers = TRUE,\n strong = TRUE,\n outline = FALSE,\n f7Text(\n inputId = \"text\",\n label = \"Text input\",\n value = \"Some text\",\n placeholder = \"Your text here\"\n ),\n f7TextArea(\n inputId = \"textarea\",\n label = \"Text area input\",\n value = \"Some text\",\n placeholder = \"Your text here\"\n ),\n f7Select(\n inputId = \"select\",\n label = \"Make a choice\",\n choices = 1:3,\n selected = 1\n ),\n f7AutoComplete(\n inputId = \"myautocomplete\",\n placeholder = \"Some text here!\",\n openIn = \"dropdown\",\n label = \"Type a fruit name\",\n choices = c(\n \"Apple\", \"Apricot\", \"Avocado\", \"Banana\", \"Melon\",\n \"Orange\", \"Peach\", \"Pear\", \"Pineapple\"\n )\n ),\n f7Stepper(\n inputId = \"stepper\",\n label = \"My stepper\",\n min = 0,\n color = \"default\",\n max = 10,\n value = 4\n ),\n f7Toggle(\n inputId = \"toggle\",\n label = \"Toggle me\"\n ),\n f7Picker(\n inputId = \"picker\",\n placeholder = \"Some text here!\",\n label = \"Picker Input\",\n choices = c(\"a\", \"b\", \"c\"),\n options = list(sheetPush = TRUE)\n ),\n f7DatePicker(\n inputId = \"date\",\n label = \"Pick a date\",\n value = Sys.Date()\n ),\n f7ColorPicker(\n inputId = \"mycolorpicker\",\n placeholder = \"Some text here!\",\n label = \"Select a color\"\n )\n ),\n f7CheckboxGroup(\n inputId = \"checkbox\",\n label = \"Checkbox group\",\n choices = c(\"a\", \"b\", \"c\"),\n selected = \"a\",\n style = list(\n inset = TRUE,\n dividers = TRUE,\n strong = TRUE,\n outline = FALSE\n )\n ),\n f7Radio(\n inputId = \"radio\",\n label = \"Radio group\",\n choices = c(\"a\", \"b\", \"c\"),\n selected = \"a\",\n style = list(\n inset = TRUE,\n dividers = TRUE,\n strong = TRUE,\n outline = FALSE\n )\n )\n )\n ),\n server = function(input, output) {\n }\n) \n```\n:::\n\n::: {.column width=\"25%\"}\n\n:::\n\n::::\n\n\n::: {.cell}\n\n```{.r .cell-code}\nlibrary(shiny)\nlibrary(shinyMobile)\n\nshinyApp(\n ui = f7Page(\n options = list(dark = FALSE, theme = \"ios\"),\n title = \"Inputs Layout\",\n f7SingleLayout(\n navbar = f7Navbar(\n title = \"Inputs Layout\",\n hairline = FALSE\n ),\n f7List(\n inset = TRUE,\n dividers = TRUE,\n strong = TRUE,\n outline = FALSE,\n f7Text(\n inputId = \"text\",\n label = \"Text input\",\n value = \"Some text\",\n placeholder = \"Your text here\"\n ),\n f7TextArea(\n inputId = \"textarea\",\n label = \"Text area input\",\n value = \"Some text\",\n placeholder = \"Your text here\"\n ),\n f7Select(\n inputId = \"select\",\n label = \"Make a choice\",\n choices = 1:3,\n selected = 1\n ),\n f7AutoComplete(\n inputId = \"myautocomplete\",\n placeholder = \"Some text here!\",\n openIn = \"dropdown\",\n label = \"Type a fruit name\",\n choices = c(\n \"Apple\", \"Apricot\", \"Avocado\", \"Banana\", \"Melon\",\n \"Orange\", \"Peach\", \"Pear\", \"Pineapple\"\n )\n ),\n f7Stepper(\n inputId = \"stepper\",\n label = \"My stepper\",\n min = 0,\n color = \"default\",\n max = 10,\n value = 4\n ),\n f7Toggle(\n inputId = \"toggle\",\n label = \"Toggle me\"\n ),\n f7Picker(\n inputId = \"picker\",\n placeholder = \"Some text here!\",\n label = \"Picker Input\",\n choices = c(\"a\", \"b\", \"c\"),\n options = list(sheetPush = TRUE)\n ),\n f7DatePicker(\n inputId = \"date\",\n label = \"Pick a date\",\n value = Sys.Date()\n ),\n f7ColorPicker(\n inputId = \"mycolorpicker\",\n placeholder = \"Some text here!\",\n label = \"Select a color\"\n )\n ),\n f7CheckboxGroup(\n inputId = \"checkbox\",\n label = \"Checkbox group\",\n choices = c(\"a\", \"b\", \"c\"),\n selected = \"a\",\n style = list(\n inset = TRUE,\n dividers = TRUE,\n strong = TRUE,\n outline = FALSE\n )\n ),\n f7Radio(\n inputId = \"radio\",\n label = \"Radio group\",\n choices = c(\"a\", \"b\", \"c\"),\n selected = \"a\",\n style = list(\n inset = TRUE,\n dividers = TRUE,\n strong = TRUE,\n outline = FALSE\n )\n )\n )\n ),\n server = function(input, output) {\n }\n)\n```\n:::\n\n\nMoreover, 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):\n\n:::: {.columns}\n::: {.column width=\"25%\"}\n\n:::\n\n::: {.column width=\"50%\"}\n```{shinylive-r}\n#| standalone: true\n#| components: [viewer]\n#| column: screen-inset-shaded\n#| viewerHeight: 800\nwebr::unmount(\"/usr/lib/R/library/shinyMobile\")\nwebr::install(\n \"shinyMobile\",\n repos = c(\"https://rinterface.github.io/rinterface-wasm-cran/\")\n)\n\nlibrary(shiny)\nlibrary(shinyMobile)\n\nshinyApp(\n ui = f7Page(\n title = \"Update radio\",\n f7SingleLayout(\n navbar = f7Navbar(title = \"Update f7Radio\"),\n f7Block(\n f7Radio(\n inputId = \"radio\",\n label = \"Custom choices\",\n choices = list(\n f7RadioChoice(\n \"Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n Nulla sagittis tellus ut turpis condimentum,\n ut dignissim lacus tincidunt\",\n title = \"Choice 1\",\n subtitle = \"David\",\n after = \"March 16, 2024\"\n ),\n f7RadioChoice(\n \"Cras dolor metus, ultrices condimentum sodales sit\n amet, pharetra sodales eros. Phasellus vel felis tellus.\n Mauris rutrum ligula nec dapibus feugiat\",\n title = \"Choice 2\",\n subtitle = \"Veerle\",\n after = \"March 17, 2024\"\n )\n ),\n selected = 2,\n style = list(\n outline = TRUE,\n strong = TRUE,\n inset = TRUE,\n dividers = TRUE\n )\n ),\n textOutput(\"res\")\n )\n )\n ),\n server = function(input, output, session) {\n output$res <- renderText(input$radio)\n }\n)\n```\n:::\n\n::: {.column width=\"25%\"}\n\n:::\n\n::::\n\n\n::: {.cell}\n\n```{.r .cell-code}\nlibrary(shiny)\nlibrary(shinyMobile)\n\nshinyApp(\n ui = f7Page(\n title = \"Update radio\",\n f7SingleLayout(\n navbar = f7Navbar(title = \"Update f7Radio\"),\n f7Block(\n f7Radio(\n inputId = \"radio\",\n label = \"Custom choices\",\n choices = list(\n f7RadioChoice(\n \"Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n Nulla sagittis tellus ut turpis condimentum,\n ut dignissim lacus tincidunt\",\n title = \"Choice 1\",\n subtitle = \"David\",\n after = \"March 16, 2024\"\n ),\n f7RadioChoice(\n \"Cras dolor metus, ultrices condimentum sodales sit\n amet, pharetra sodales eros. Phasellus vel felis tellus.\n Mauris rutrum ligula nec dapibus feugiat\",\n title = \"Choice 2\",\n subtitle = \"Veerle\",\n after = \"March 17, 2024\"\n )\n ),\n selected = 2,\n style = list(\n outline = TRUE,\n strong = TRUE,\n inset = TRUE,\n dividers = TRUE\n )\n ),\n textOutput(\"res\")\n )\n )\n ),\n server = function(input, output, session) {\n output$res <- renderText(input$radio)\n }\n)\n```\n:::\n\n\n### New `f7Treeview()` component\n\nThe new release welcomes a brand new input widget. As its name suggests, `f7Treewiew()` enables sorting items hierarchically within a collapsible nested list of items.\nThis is ideal, for instance, to select files within multiple folders, as an alternative to the classic `fileInput()`.\n\n:::: {.columns}\n::: {.column width=\"25%\"}\n\n:::\n\n::: {.column width=\"50%\"}\n```{shinylive-r}\n#| standalone: true\n#| components: [viewer]\n#| column: screen-inset-shaded\n#| viewerHeight: 800\nwebr::unmount(\"/usr/lib/R/library/shinyMobile\")\nwebr::install(\n \"shinyMobile\",\n repos = c(\"https://rinterface.github.io/rinterface-wasm-cran/\")\n)\n\nlibrary(shiny)\nlibrary(shinyMobile)\n\nshinyApp(\n ui = f7Page(\n title = \"My app\",\n f7SingleLayout(\n navbar = f7Navbar(title = \"f7Treeview\"),\n # group treeview with selectable items\n f7BlockTitle(\"Selectable items\"),\n f7Block(\n f7Treeview(\n id = \"treeview1\",\n selectable = TRUE,\n f7TreeviewGroup(\n title = \"Selected images\",\n icon = f7Icon(\"folder_fill\"),\n itemToggle = TRUE,\n lapply(1:3, function(i) f7TreeviewItem(label = paste0(\"image\", i, \".png\"),\n icon = f7Icon(\"photo_fill\")))\n )\n ),\n textOutput(\"selected\")\n ),\n\n # group treeview with checkbox items\n f7BlockTitle(\"Checkbox\"),\n f7Block(\n f7Treeview(\n id = \"treeview2\",\n withCheckbox = TRUE,\n f7TreeviewGroup(\n title = \"Selected images\",\n icon = f7Icon(\"folder_fill\"),\n itemToggle = TRUE,\n lapply(1:3, function(i) f7TreeviewItem(label = paste0(\"image\", i, \".png\"),\n icon = f7Icon(\"photo_fill\")))\n )\n ),\n textOutput(\"selected2\")\n )\n )\n ),\n server = function(input, output) {\n output$selected <- renderText(input$treeview1)\n output$selected2 <- renderText(input$treeview2)\n }\n)\n```\n:::\n\n::: {.column width=\"25%\"}\n\n:::\n\n::::\n\n\n::: {.cell}\n\n```{.r .cell-code}\nlibrary(shiny)\nlibrary(shinyMobile)\n\nshinyApp(\n ui = f7Page(\n title = \"My app\",\n f7SingleLayout(\n navbar = f7Navbar(title = \"f7Treeview\"),\n # group treeview with selectable items\n f7BlockTitle(\"Selectable items\"),\n f7Block(\n f7Treeview(\n id = \"treeview1\",\n selectable = TRUE,\n f7TreeviewGroup(\n title = \"Selected images\",\n icon = f7Icon(\"folder_fill\"),\n itemToggle = TRUE,\n lapply(1:3, function(i) f7TreeviewItem(label = paste0(\"image\", i, \".png\"),\n icon = f7Icon(\"photo_fill\")))\n )\n ),\n textOutput(\"selected\")\n ),\n\n # group treeview with checkbox items\n f7BlockTitle(\"Checkbox\"),\n f7Block(\n f7Treeview(\n id = \"treeview2\",\n withCheckbox = TRUE,\n f7TreeviewGroup(\n title = \"Selected images\",\n icon = f7Icon(\"folder_fill\"),\n itemToggle = TRUE,\n lapply(1:3, function(i) f7TreeviewItem(label = paste0(\"image\", i, \".png\"),\n icon = f7Icon(\"photo_fill\")))\n )\n ),\n textOutput(\"selected2\")\n )\n )\n ),\n server = function(input, output) {\n output$selected <- renderText(input$treeview1)\n output$selected2 <- renderText(input$treeview2)\n }\n)\n```\n:::\n\n\n### New `f7Form()`\n\nShiny does not provide HTML forms handling out of the box (a [form](https://www.w3schools.com/html/html_forms.asp) 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:\n\n- `f7Text()`\n- `f7TextArea()`\n- `f7Password()`\n- `f7Select()`\n\n:::: {.columns}\n::: {.column width=\"25%\"}\n\n:::\n\n::: {.column width=\"50%\"}\n```{shinylive-r}\n#| standalone: true\n#| components: [viewer]\n#| column: screen-inset-shaded\n#| viewerHeight: 800\n#| viewerWidth: 390\nwebr::unmount(\"/usr/lib/R/library/shinyMobile\")\nwebr::install(\n \"shinyMobile\",\n repos = c(\"https://rinterface.github.io/rinterface-wasm-cran/\")\n)\n\nlibrary(shiny)\nlibrary(shinyMobile)\n\nshinyApp(\n ui = f7Page(\n f7SingleLayout(\n navbar = f7Navbar(title = \"Inputs form\"),\n f7Block(f7Button(\"update\", \"Click me\")),\n f7BlockTitle(\"A list of inputs in a form\"),\n f7List(\n inset = TRUE,\n dividers = FALSE,\n strong = TRUE,\n f7Form(\n id = \"myform\",\n f7Text(\n inputId = \"text\",\n label = \"Text input\",\n value = \"Some text\",\n placeholder = \"Your text here\",\n style = list(\n description = \"A cool text input\",\n outline = TRUE,\n media = f7Icon(\"house\"),\n clearable = TRUE,\n floating = TRUE\n )\n ),\n f7TextArea(\n inputId = \"textarea\",\n label = \"Text Area\",\n value = \"Lorem ipsum dolor sit amet, consectetur\n adipiscing elit, sed do eiusmod tempor incididunt ut\n labore et dolore magna aliqua\",\n placeholder = \"Your text here\",\n resize = TRUE,\n style = list(\n description = \"A cool text input\",\n outline = TRUE,\n media = f7Icon(\"house\"),\n clearable = TRUE,\n floating = TRUE\n )\n ),\n f7Password(\n inputId = \"password\",\n label = \"Password:\",\n placeholder = \"Your password here\",\n style = list(\n description = \"A cool text input\",\n outline = TRUE,\n media = f7Icon(\"house\"),\n clearable = TRUE,\n floating = TRUE\n )\n )\n )\n ),\n verbatimTextOutput(\"form\")\n )\n ),\n server = function(input, output, session) {\n output$form <- renderPrint(input$myform)\n\n observeEvent(input$update, {\n updateF7Form(\n \"myform\",\n data = list(\n \"text\" = \"New text\",\n \"textarea\" = \"New text area\",\n \"password\" = \"New password\"\n )\n )\n })\n }\n)\n```\n:::\n\n::: {.column width=\"25%\"}\n\n:::\n\n::::\n\n\n::: {.cell}\n\n```{.r .cell-code}\nlibrary(shiny)\nlibrary(shinyMobile)\n\nshinyApp(\n ui = f7Page(\n f7SingleLayout(\n navbar = f7Navbar(title = \"Inputs form\"),\n f7Block(f7Button(\"update\", \"Click me\")),\n f7BlockTitle(\"A list of inputs in a form\"),\n f7List(\n inset = TRUE,\n dividers = FALSE,\n strong = TRUE,\n f7Form(\n id = \"myform\",\n f7Text(\n inputId = \"text\",\n label = \"Text input\",\n value = \"Some text\",\n placeholder = \"Your text here\",\n style = list(\n description = \"A cool text input\",\n outline = TRUE,\n media = f7Icon(\"house\"),\n clearable = TRUE,\n floating = TRUE\n )\n ),\n f7TextArea(\n inputId = \"textarea\",\n label = \"Text Area\",\n value = \"Lorem ipsum dolor sit amet, consectetur\n adipiscing elit, sed do eiusmod tempor incididunt ut\n labore et dolore magna aliqua\",\n placeholder = \"Your text here\",\n resize = TRUE,\n style = list(\n description = \"A cool text input\",\n outline = TRUE,\n media = f7Icon(\"house\"),\n clearable = TRUE,\n floating = TRUE\n )\n ),\n f7Password(\n inputId = \"password\",\n label = \"Password:\",\n placeholder = \"Your password here\",\n style = list(\n description = \"A cool text input\",\n outline = TRUE,\n media = f7Icon(\"house\"),\n clearable = TRUE,\n floating = TRUE\n )\n )\n )\n ),\n verbatimTextOutput(\"form\")\n )\n ),\n server = function(input, output, session) {\n output$form <- renderPrint(input$myform)\n\n observeEvent(input$update, {\n updateF7Form(\n \"myform\",\n data = list(\n \"text\" = \"New text\",\n \"textarea\" = \"New text area\",\n \"password\" = \"New password\"\n )\n )\n })\n }\n)\n```\n:::\n\n\n## Breaking changes\n\nSome 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](https://github.com/RinteRface/shinyMobile/blob/69da6ca46984bf6c73e2dc32ff8d9f415ec36a30/NEWS.md#breaking-changes) to see a list of all changes in this release.\n\n## Soft deprecation\n\nSome 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.\n\n# Conclusion\n\nSince 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](https://docs.r-wasm.org/webr/latest/) side, it isn't impossible that one day, we might run a totally offline Shiny app on mobile.\n", + "markdown": "---\nlayout: post\ntitle: \"shinyMobile 2.0.0: a preview\"\nimage: logo.png\nauthor: Veerle van Leemput and David Granjon\ndate: '2024-05-13'\ncategories:\n - shiny\nformat: \n html:\n code-fold: true\nfilters: \n - shinylive\n---\n\n\n\n\n![](logo.png){width=25% fig-align=\"center\"}\n\nshinyMobile 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](https://framework7.io/) that drives its capabilities.\n\nThis year shinyMobile gets a major update to v2.2.0. I'd like to warmly thank [Veerle van Leemput](https://hypebright.nl/) and Michael S. Czahor from [AthlyticZ](https://linktr.ee/athlyticz) for providing significant support during this marathon.\n\n# What's new\n\nshinyMobile 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](https://github.com/RinteRface/shinyMobile/blob/69da6ca46984bf6c73e2dc32ff8d9f415ec36a30/NEWS.md), but we believe these are all for the best!\n\n## Major changes\n\n### New multi pages experimental support\n\nWe are very excited to bring this feature out for this new release. Under the hood, this is possible owing to the `{brochure}` [package](https://github.com/ColinFay/brochure) from \n[Colin Fay](https://github.com/ColinFay) as well as the internal Framework7 [router](https://framework7.io/docs/view) component.\n\n#### What does this mean? \n\nYou 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.\n\n#### How does this work?\n\nAt the time of writting of this blog post, you must install a patched `{brochure}` version with `devtools::install_github(\"DivadNojnarg/brochure\")`.\n\nIn 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](https://shinymobile.rinterface.com/articles/multipages) which provides more technical details.\n\n\n\n\n::: {.cell}\n\n```{.r .cell-code}\nlibrary(shiny)\n# Needs a specific version of brochure for now.\n# This allows to pass wrapper functions with options\n# as list. We need it because of the f7Page options parameter\n# and to pass the routes list object for JS.\n# devtools::install_github(\"DivadNojnarg/brochure\")\nlibrary(brochure)\nlibrary(shinyMobile)\n\n# Allows to use the app on a server like \n# shinyapps.io where basepath is /app_name\n# instead of \"/\" or \"\".\nmake_link <- function(path = NULL, basepath = \"\") {\n if (is.null(path)) {\n if (nchar(basepath) > 0) {\n return(basepath)\n } else {\n return(\"/\")\n }\n }\n sprintf(\"%s/%s\", basepath, path)\n}\n\nlinks <- lapply(2:3, function(i) {\n tags$li(\n f7Link(\n routable = TRUE,\n label = sprintf(\"Link to page %s\", i),\n href = make_link(i)\n )\n )\n})\n\npage_1 <- function() {\n page(\n href = \"/\",\n ui = function(request) {\n shiny::tags$div(\n class = \"page\",\n # top navbar goes here\n f7Navbar(title = \"Home page\"),\n # NOTE: when the main toolbar is enabled in\n # f7MultiLayout, we can't use individual page toolbars.\n # f7Toolbar(\n # position = \"bottom\",\n # tags$a(\n # href = \"/2\",\n # \"Second page\",\n # class = \"link\"\n # )\n # ),\n # Page content\n tags$div(\n class = \"page-content\",\n f7List(\n inset = TRUE,\n strong = TRUE,\n outline = TRUE,\n dividers = TRUE,\n mode = \"links\",\n links\n ),\n f7Block(\n f7Text(\"text\", \"Text input\", \"default\"),\n f7Select(\"select\", \"Select\", colnames(mtcars)),\n textOutput(\"res\"),\n textOutput(\"res2\")\n )\n )\n )\n }\n )\n}\n\npage_2 <- function() {\n page(\n href = \"/2\",\n ui = function(request) {\n shiny::tags$div(\n class = \"page\",\n # top navbar goes here\n f7Navbar(\n title = \"Second page\",\n # Allows to go back to main\n leftPanel = tagList(\n tags$a(\n href = make_link(),\n class = \"link back\",\n tags$i(class = \"icon icon-back\"),\n tags$span(\n class = \"if-not-md\",\n \"Back\"\n )\n )\n )\n ),\n shiny::tags$div(\n class = \"page-content\",\n f7Block(f7Button(inputId = \"update\", label = \"Update stepper\")),\n f7List(\n strong = TRUE,\n inset = TRUE,\n outline = FALSE,\n f7Stepper(\n inputId = \"stepper\",\n label = \"My stepper\",\n min = 0,\n max = 10,\n size = \"small\",\n value = 4,\n wraps = TRUE,\n autorepeat = TRUE,\n rounded = FALSE,\n raised = FALSE,\n manual = FALSE\n )\n ),\n f7Block(textOutput(\"test\"))\n )\n )\n }\n )\n}\n\npage_3 <- function() {\n page(\n href = \"/3\",\n ui = function(request) {\n shiny::tags$div(\n class = \"page\",\n # top navbar goes here\n f7Navbar(\n title = \"Third page\",\n # Allows to go back to main\n leftPanel = tagList(\n tags$a(\n href = make_link(),\n class = \"link back\",\n tags$i(class = \"icon icon-back\"),\n tags$span(\n class = \"if-not-md\",\n \"Back\"\n )\n )\n )\n ),\n shiny::tags$div(\n class = \"page-content\",\n f7Block(\"Nothing to show yet ...\")\n )\n )\n }\n )\n}\n\nbrochureApp(\n basepath = make_link(),\n # Pages\n page_1(),\n page_2(),\n page_3(),\n # Important: in theory brochure makes\n # each page having its own shiny session/ server function.\n # That's not what we want here so we'll have\n # a global server function.\n server = function(input, output, session) {\n output$res <- renderText(input$text)\n output$res2 <- renderText(input$select)\n output$test <- renderText(input$stepper)\n\n observeEvent(input$update, {\n updateF7Stepper(\n inputId = \"stepper\",\n value = 0.1,\n step = 0.01,\n size = \"large\",\n min = 0,\n max = 1,\n wraps = FALSE,\n autorepeat = FALSE,\n rounded = TRUE,\n raised = TRUE,\n color = \"pink\",\n manual = TRUE,\n decimalPoint = 2\n )\n })\n },\n wrapped = f7MultiLayout,\n wrapped_options = list(\n basepath = make_link(),\n # Common toolbar\n toolbar = f7Toolbar(\n f7Link(icon = f7Icon(\"house\"), href = make_link(), routable = TRUE)\n ),\n options = list(\n dark = TRUE,\n theme = \"md\",\n routes = list(\n # Important: don't remove keepAlive\n # for pages as this allows\n # to save the input state when switching\n # between pages. If FALSE, each time a page is\n # changed, inputs are reset.\n list(path = make_link(), url = make_link(), name = \"home\", keepAlive = TRUE),\n list(path = make_link(\"2\"), url = make_link(\"2\"), name = \"2\", keepAlive = TRUE),\n list(path = make_link(\"3\"), url = make_link(\"3\"), name = \"3\", keepAlive = TRUE)\n )\n )\n )\n)\n```\n:::\n\n\n\n\n### Updated material design style\n\nBy updating to the latest Framework7 version, we now benefit from a totally revamped Android (md) design, which looks more modern.\n\n:::: {.columns}\n::: {.column width=\"20%\"}\n\n:::\n\n::: {.column width=\"60%\"}\n```{shinylive-r}\n#| standalone: true\n#| components: [viewer]\n#| column: screen-inset-shaded\n#| viewerHeight: 800\nlibrary(shiny)\nlibrary(shinyMobile)\n\n# source modules\ne <- environment()\npath <- system.file(\"examples/gallery/tabs/\", package = \"shinyMobile\")\nsapply(\n list.files(\n path,\n include.dirs = FALSE,\n pattern = \".R\",\n ignore.case = TRUE\n ),\n function(f) {\n source(file.path(path, f), local = e)\n }\n)\n\napp_options <- list(\n theme = \"md\",\n dark = TRUE,\n filled = FALSE,\n preloader = TRUE,\n color = \"#007aff\",\n navbar = list(\n hideOnPageScroll = TRUE,\n mdCenterTitle = TRUE\n )\n)\n\n# shiny app\nshinyApp(\n ui = f7Page(\n allowPWA = TRUE,\n options = app_options,\n f7TabLayout(\n title = \"shinyMobile Gallery\",\n messagebar = f7MessageBar(inputId = \"mymessagebar\",\n placeholder = \"Message\"),\n panels = tagList(\n f7Panel(\n id = \"panelLeft\",\n title = \"Left Panel\",\n side = \"left\",\n f7Block(\"A panel with push effect\"),\n f7PanelMenu(\n inset = TRUE,\n outline = TRUE,\n # Use items as tab navigation only\n f7PanelItem(\n tabName = \"tabset-Inputs\",\n title = \"Input tabs\",\n icon = f7Icon(\"largecircle_fill_circle\"),\n active = TRUE\n ),\n f7PanelItem(\n tabName = \"tabset-FABs\",\n title = \"Buttons tabs\",\n icon = f7Icon(\"largecircle_fill_circle\")\n )\n ),\n effect = \"push\",\n options = list(swipe = TRUE)\n ),\n f7Panel(\n title = \"Right Panel\",\n side = \"right\",\n f7Radio(\n inputId = \"theme\",\n label = \"Theme\",\n choices = c(\"md\", \"ios\"),\n selected = app_options$theme\n ),\n f7Radio(\n inputId = \"dark\",\n label = \"Mode\",\n choices = c(\"dark\", \"light\"),\n selected = ifelse(app_options$dark, \"dark\", \"light\")\n ),\n f7Radio(\n inputId = \"color\",\n label = \"Color\",\n choices = getF7Colors(),\n selected = \"primary\"\n ),\n effect = \"floating\",\n options = list(swipe = TRUE)\n )\n ),\n navbar = f7Navbar(\n title = \"shinyMobile Gallery\",\n hairline = TRUE,\n leftPanel = TRUE,\n rightPanel = TRUE\n ),\n f7Login(\n id = \"loginPage\",\n title = \"You really think you can go here?\",\n footer = \"This section simulates an authentication process. There\n is actually no user and password database. Put whatever you want but\n don't leave blank!\",\n startOpen = FALSE\n ),\n # recover the color picker input and update the text background\n # color accordingly.\n tags$script(\n \"$(function() {\n Shiny.addCustomMessageHandler('text-color', function(message) {\n $('#colorPickerVal').css('background-color', message);\n });\n\n // toggle message bar based on the currently selected tab\n Shiny.addCustomMessageHandler('toggleMessagebar', function(message) {\n if (message === 'chat') {\n $('#mymessagebar').show();\n $('.toolbar.tabLinks').hide();\n } else {\n $('#mymessagebar').hide();\n $('.toolbar.tabLinks').show();\n }\n });\n });\n \"\n ),\n f7Tabs(\n id = \"tabset\",\n animated = FALSE,\n swipeable = TRUE,\n tabInputs,\n tabBtns,\n tabCards,\n tabLists,\n tabText,\n tabInfo,\n tabOthers\n )\n )\n ),\n server = function(input, output, session) {\n\n # update theme\n observeEvent(input$theme, ignoreInit = TRUE, {\n updateF7App(\n options = list(\n theme = input$theme\n )\n )\n })\n\n # update mode\n observeEvent(input$dark, ignoreInit = TRUE, {\n updateF7App(\n options = list(\n dark = ifelse(input$dark == \"dark\", TRUE, FALSE)\n )\n )\n })\n\n # update color\n observeEvent(input$color, ignoreInit = TRUE, {\n updateF7App(\n options = list(\n color = input$color\n )\n )\n })\n\n # input validation\n observe({\n validateF7Input(inputId = \"text\", info = \"Whatever\")\n validateF7Input(\n inputId = \"password\",\n pattern = \"[0-9]*\",\n error = \"Only numbers please!\"\n )\n })\n\n # toggle message bar: should only be dislayed when on the messages tab\n observeEvent(input$tabset, {\n session$sendCustomMessage(type = \"toggleMessagebar\", input$tabset)\n session$sendCustomMessage(type = \"toggleSearchbar\", input$tabset)\n })\n\n # user send new message\n observeEvent(input[[\"mymessagebar-send\"]], {\n updateF7Messages(\n id = \"mymessages\",\n list(\n f7Message(\n text = input$mymessagebar,\n name = \"David\",\n type = \"sent\",\n header = \"Message Header\",\n footer = \"Message Footer\",\n textHeader = \"Text Header\",\n textFooter = \"text Footer\",\n avatar = \"https://cdn.framework7.io/placeholder/people-100x100-7.jpg\"\n )\n )\n )\n })\n\n # fake to receive random messages\n observe({\n invalidateLater(5000)\n names <- c(\"Victor\", \"John\")\n name <- sample(names, 1)\n\n updateF7Messages(\n id = \"mymessages\",\n list(\n f7Message(\n text = \"Message\",\n name = name,\n type = \"received\",\n avatar = \"https://cdn.framework7.io/placeholder/people-100x100-9.jpg\"\n )\n )\n )\n })\n\n # trigger for login\n trigger <- reactive({\n req(input$tabset == \"chat\")\n })\n # login server module\n f7LoginServer(\n id = \"loginPage\",\n ignoreInit = TRUE,\n trigger = trigger\n )\n\n output$sin <- renderPlot(plot(sin, -pi, 2 * pi))\n output$cos <- renderPlot(plot(cos, -pi, 2 * pi))\n\n output$text <- renderPrint(input$text)\n output$password <- renderPrint(input$password)\n output$textarea <- renderPrint(input$textarea)\n output$slider <- renderPrint(input$sliderInput)\n output$sliderRange <- renderPrint(input$sliderRangeInput)\n output$stepper <- renderPrint(input$stepper)\n output$check <- renderPrint(input$check)\n output$checkgroup <- renderPrint(input$checkgroup)\n output$checkgroup2 <- renderPrint(input$checkgroup2)\n output$radio <- renderPrint(input$radio)\n output$radio2 <- renderPrint(input$radio2)\n output$toggle <- renderPrint(input$toggle)\n output$select <- renderPrint(input$select)\n output$smartdata <- renderTable(\n {\n head(mtcars[, c(\"mpg\", input$smartsel), drop = FALSE])\n },\n rownames = TRUE\n )\n output$dateval <- renderPrint(input$mydatepicker)\n output$autocompleteval <- renderPrint(input$myautocomplete)\n\n lapply(1:12, function(i) {\n output[[paste0(\"res\", i)]] <- renderText(paste0(\"Button\", i, \" is \", input[[paste0(\"btn\", i)]]))\n })\n output$pickerval <- renderText(input$mypicker)\n output$colorPickerVal <- renderPrint(input$mycolorpicker$hex)\n\n # send the color picker input to JS\n observeEvent(input$mycolorpicker, {\n session$sendCustomMessage(type = \"text-color\", message = input$mycolorpicker$hex)\n })\n\n\n # popup\n output$popupContent <- renderPrint(input$popupText)\n\n observeEvent(input$togglePopup, {\n f7Popup(\n id = \"popup1\",\n title = \"My first popup\",\n f7Block(\n p(\"Popup can push the view behind. By default it has effect only when\n 'safe-area-inset-top' is more than zero (iOS fullscreen webapp or iOS cordova app)\"),\n f7Text(inputId = \"popupText\",\n label = \"Popup content\",\n value = \"This is my first popup, I swear!\"),\n verbatimTextOutput(\"popupContent\")\n )\n )\n })\n\n observeEvent(input$popup1, {\n if (input$tabset == \"Popups\") {\n popupStatus <- if (input$popup1) \"opened\" else \"closed\"\n f7Toast(\n position = \"top\",\n text = paste(\"Popup is\", popupStatus)\n )\n }\n })\n\n # sheet plot\n output$sheetPlot <- renderPlot({\n hist(rnorm(input$sheetObs))\n })\n\n observeEvent(input$toggleSheet, {\n updateF7Sheet(id = \"sheet1\")\n })\n\n # notifications\n lapply(1:3, function(i) {\n observeEvent(input[[paste0(\"goNotif\", i)]], {\n icon <- if (i %% 2 == 0) f7Icon(\"bolt_fill\") else NULL\n\n f7Notif(\n text = \"test\",\n icon = icon,\n title = paste(\"Notification\", i),\n subtitle = \"A subtitle\",\n titleRightText = i\n )\n })\n })\n\n # Dialogs\n # notifications\n lapply(1:3, function(i) {\n observeEvent(input[[paste0(\"goDialog\", i)]], {\n if (i == 1) {\n f7Dialog(\n title = \"Dialog title\",\n text = \"This is an alert dialog\"\n )\n } else if (i == 2) {\n f7Dialog(\n id = \"confirmDialog\",\n title = \"Dialog title\",\n type = \"confirm\",\n text = \"This is an confirm dialog\"\n )\n } else if (i == 3) {\n f7Dialog(\n id = \"promptDialog\",\n title = \"Dialog title\",\n type = \"prompt\",\n text = \"This is a prompt dialog\"\n )\n }\n })\n })\n\n observeEvent(input$confirmDialog, {\n f7Toast(text = paste(\"Alert input is:\", input$confirmDialog))\n })\n\n output$promptres <- renderUI({\n if (is.null(input$promptDialog)) {\n f7BlockTitle(title = \"Click on dialog button 3\")\n }\n f7BlockTitle(title = input$promptDialog)\n })\n\n # popovers\n observeEvent(input$popoverButton, {\n addF7Popover(\n id = \"popoverButton\",\n options = list(content = \"This is a f7Button\")\n )\n })\n\n # toasts\n observeEvent(input$toast, {\n f7Toast(\n position = \"bottom\",\n text = \"I am a toast. Eat me!\"\n )\n })\n\n # action sheet\n observeEvent(input$goActionSheet, {\n f7ActionSheet(\n grid = TRUE,\n id = \"action1\",\n buttons = list(\n list(\n text = \"Notification\",\n icon = f7Icon(\"info\"),\n color = NULL\n ),\n list(\n text = \"Dialog\",\n icon = f7Icon(\"lightbulb_fill\"),\n color = NULL\n )\n )\n )\n })\n\n observeEvent(input$action1_button, {\n if (input$action1_button == 1) {\n f7Notif(\n text = \"You clicked on the first button\",\n icon = f7Icon(\"bolt_fill\"),\n title = \"Notification\",\n titleRightText = \"now\"\n )\n } else if (input$action1_button == 2) {\n f7Dialog(\n id = \"actionSheetDialog\",\n title = \"Click me to launch a Toast!\",\n type = \"confirm\",\n text = \"You clicked on the second button\"\n )\n }\n })\n\n observeEvent(input$swipeAction_button, {\n if (input$swipeAction_button == 1) {\n f7Notif(\n text = \"You clicked on the first button\",\n icon = f7Icon(\"bolt_fill\"),\n title = \"Notification\",\n titleRightText = \"now\"\n )\n } else if (input$swipeAction_button == 2) {\n f7Dialog(\n id = \"actionSheetDialog\",\n title = \"Click me to launch a Toast!\",\n type = \"confirm\",\n text = \"You clicked on the second button\"\n )\n }\n })\n\n observeEvent(input$actionSheetDialog, {\n f7Toast(text = paste(\"Alert input is:\", input$actionSheetDialog))\n })\n\n # update progress bar\n observeEvent(input$updatepg1, {\n updateF7Progress(id = \"pg1\", value = input$updatepg1)\n })\n\n # update gauge\n observeEvent(input$updategauge1, {\n updateF7Gauge(id = \"mygauge1\", value = input$updategauge1)\n })\n\n # expand card 3\n observeEvent(input$goCard, {\n updateF7Card(id = \"card3\")\n })\n\n # toggle accordion\n observeEvent(input$goAccordion, {\n updateF7Accordion(\n id = \"accordion1\",\n selected = 1\n )\n })\n\n # update panel\n observeEvent(input$goPanel, {\n updateF7Panel(id = \"panelLeft\")\n })\n\n # swipeout\n observeEvent(input$swipeNotif, {\n f7Notif(\n text = \"test\",\n icon = f7Icon(\"bolt_fill\"),\n title = \"Notification\",\n subtitle = \"A subtitle\",\n titleRightText = \"now\"\n )\n })\n\n observeEvent(input$swipeAlert, {\n f7Dialog(\n title = \"Dialog title\",\n text = \"This is an alert dialog\"\n )\n })\n\n observeEvent(input$swipeActionSheet, {\n f7ActionSheet(\n grid = TRUE,\n id = \"swipeAction\",\n buttons = list(\n list(\n text = \"Notification\",\n icon = f7Icon(\"info\"),\n color = NULL\n ),\n list(\n text = \"Dialog\",\n icon = f7Icon(\"lightbulb_fill\"),\n color = NULL\n )\n )\n )\n })\n\n # preloaders\n observeEvent(input$showLoader, {\n showF7Preloader(target = \"#preloaderPlot\", color = \"blue\")\n Sys.sleep(2)\n hideF7Preloader(target = \"#preloaderPlot\")\n })\n output$preloaderPlot <- renderPlot({\n hist(rnorm(100))\n })\n\n # photo browser\n observeEvent(input$togglePhoto, {\n f7PhotoBrowser(\n theme = \"dark\",\n type = \"standalone\",\n photos = c(\n \"https://cdn.framework7.io/placeholder/sports-1024x1024-1.jpg\",\n \"https://cdn.framework7.io/placeholder/sports-1024x1024-2.jpg\",\n \"https://cdn.framework7.io/placeholder/sports-1024x1024-3.jpg\"\n )\n )\n })\n\n # Menus\n observeEvent(input$toggleMenu, {\n updateF7MenuDropdown(\"menu1\")\n })\n\n observeEvent(input$menuItem1, {\n f7Notif(text = \"Well done!\")\n })\n\n # skeleton\n observe({\n invalidateLater(4000)\n f7Skeleton(\".skeleton-list\", \"fade\", 2)\n })\n\n # Treeview\n output$treeview <- renderText({\n input$treeview\n })\n\n # Table\n output$table <- renderUI({\n f7Table(mtcars[1:15,])\n })\n\n # pull to refresh\n # observeEvent(input$ptr, {\n #\n # ptrStatus <- if (input$ptr) \"on\"\n #\n # f7Dialog(\n # text = paste('ptr is', ptrStatus),\n # type = \"alert\"\n # )\n # })\n }\n)\n```\n:::\n\n::: {.column width=\"20%\"}\n\n:::\n\n::::\n\n### Refined inputs layout and style\n\nWhenever 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())`. \n\nBesides, independently from `f7List()`, some inputs having more specific styling options:\n\n- `f7AutoComplete()`.\n- `f7Text()`, `f7Password()`, `f7TextArea()`.\n- `f7Select()`.\n- `f7Picker()`, `f7ColorPicker()` and `f7DatePicker()`.\n- `f7Radio()` and `f7CheckboxGroup()`.\n\nIn practices, you can design a supercharged `f7Text()` like so:\n\n\n\n\n::: {.cell}\n\n```{.r .cell-code}\nf7Text(\n inputId = \"text\",\n label = \"Text input\",\n value = \"Some text\",\n placeholder = \"Your text here\",\n style = list(\n description = \"A cool text input\",\n outline = TRUE,\n media = f7Icon(\"house\"),\n clearable = TRUE,\n floating = TRUE\n )\n)\n```\n:::\n\n\n\n\nThis 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.\n\n`f7Stepper()` and `f7Toggle()` label is now displayed on the left.\n\n:::: {.columns}\n::: {.column width=\"25%\"}\n\n:::\n\n::: {.column width=\"50%\"}\n```{shinylive-r}\n#| standalone: true\n#| components: [viewer]\n#| column: screen-inset-shaded\n#| viewerHeight: 800\nlibrary(shiny)\nlibrary(shinyMobile)\n\nshinyApp(\n ui = f7Page(\n options = list(dark = FALSE, theme = \"ios\"),\n title = \"Inputs Layout\",\n f7SingleLayout(\n navbar = f7Navbar(\n title = \"Inputs Layout\",\n hairline = FALSE\n ),\n f7List(\n inset = TRUE,\n dividers = TRUE,\n strong = TRUE,\n outline = FALSE,\n f7Text(\n inputId = \"text\",\n label = \"Text input\",\n value = \"Some text\",\n placeholder = \"Your text here\"\n ),\n f7TextArea(\n inputId = \"textarea\",\n label = \"Text area input\",\n value = \"Some text\",\n placeholder = \"Your text here\"\n ),\n f7Select(\n inputId = \"select\",\n label = \"Make a choice\",\n choices = 1:3,\n selected = 1\n ),\n f7AutoComplete(\n inputId = \"myautocomplete\",\n placeholder = \"Some text here!\",\n openIn = \"dropdown\",\n label = \"Type a fruit name\",\n choices = c(\n \"Apple\", \"Apricot\", \"Avocado\", \"Banana\", \"Melon\",\n \"Orange\", \"Peach\", \"Pear\", \"Pineapple\"\n )\n ),\n f7Stepper(\n inputId = \"stepper\",\n label = \"My stepper\",\n min = 0,\n color = \"default\",\n max = 10,\n value = 4\n ),\n f7Toggle(\n inputId = \"toggle\",\n label = \"Toggle me\"\n ),\n f7Picker(\n inputId = \"picker\",\n placeholder = \"Some text here!\",\n label = \"Picker Input\",\n choices = c(\"a\", \"b\", \"c\"),\n options = list(sheetPush = TRUE)\n ),\n f7DatePicker(\n inputId = \"date\",\n label = \"Pick a date\",\n value = Sys.Date()\n ),\n f7ColorPicker(\n inputId = \"mycolorpicker\",\n placeholder = \"Some text here!\",\n label = \"Select a color\"\n )\n ),\n f7CheckboxGroup(\n inputId = \"checkbox\",\n label = \"Checkbox group\",\n choices = c(\"a\", \"b\", \"c\"),\n selected = \"a\",\n style = list(\n inset = TRUE,\n dividers = TRUE,\n strong = TRUE,\n outline = FALSE\n )\n ),\n f7Radio(\n inputId = \"radio\",\n label = \"Radio group\",\n choices = c(\"a\", \"b\", \"c\"),\n selected = \"a\",\n style = list(\n inset = TRUE,\n dividers = TRUE,\n strong = TRUE,\n outline = FALSE\n )\n )\n )\n ),\n server = function(input, output) {\n }\n) \n```\n:::\n\n::: {.column width=\"25%\"}\n\n:::\n\n::::\n\n\n\n\n::: {.cell}\n\n```{.r .cell-code}\nlibrary(shiny)\nlibrary(shinyMobile)\n\nshinyApp(\n ui = f7Page(\n options = list(dark = FALSE, theme = \"ios\"),\n title = \"Inputs Layout\",\n f7SingleLayout(\n navbar = f7Navbar(\n title = \"Inputs Layout\",\n hairline = FALSE\n ),\n f7List(\n inset = TRUE,\n dividers = TRUE,\n strong = TRUE,\n outline = FALSE,\n f7Text(\n inputId = \"text\",\n label = \"Text input\",\n value = \"Some text\",\n placeholder = \"Your text here\"\n ),\n f7TextArea(\n inputId = \"textarea\",\n label = \"Text area input\",\n value = \"Some text\",\n placeholder = \"Your text here\"\n ),\n f7Select(\n inputId = \"select\",\n label = \"Make a choice\",\n choices = 1:3,\n selected = 1\n ),\n f7AutoComplete(\n inputId = \"myautocomplete\",\n placeholder = \"Some text here!\",\n openIn = \"dropdown\",\n label = \"Type a fruit name\",\n choices = c(\n \"Apple\", \"Apricot\", \"Avocado\", \"Banana\", \"Melon\",\n \"Orange\", \"Peach\", \"Pear\", \"Pineapple\"\n )\n ),\n f7Stepper(\n inputId = \"stepper\",\n label = \"My stepper\",\n min = 0,\n color = \"default\",\n max = 10,\n value = 4\n ),\n f7Toggle(\n inputId = \"toggle\",\n label = \"Toggle me\"\n ),\n f7Picker(\n inputId = \"picker\",\n placeholder = \"Some text here!\",\n label = \"Picker Input\",\n choices = c(\"a\", \"b\", \"c\"),\n options = list(sheetPush = TRUE)\n ),\n f7DatePicker(\n inputId = \"date\",\n label = \"Pick a date\",\n value = Sys.Date()\n ),\n f7ColorPicker(\n inputId = \"mycolorpicker\",\n placeholder = \"Some text here!\",\n label = \"Select a color\"\n )\n ),\n f7CheckboxGroup(\n inputId = \"checkbox\",\n label = \"Checkbox group\",\n choices = c(\"a\", \"b\", \"c\"),\n selected = \"a\",\n style = list(\n inset = TRUE,\n dividers = TRUE,\n strong = TRUE,\n outline = FALSE\n )\n ),\n f7Radio(\n inputId = \"radio\",\n label = \"Radio group\",\n choices = c(\"a\", \"b\", \"c\"),\n selected = \"a\",\n style = list(\n inset = TRUE,\n dividers = TRUE,\n strong = TRUE,\n outline = FALSE\n )\n )\n )\n ),\n server = function(input, output) {\n }\n)\n```\n:::\n\n\n\n\nMoreover, 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):\n\n:::: {.columns}\n::: {.column width=\"25%\"}\n\n:::\n\n::: {.column width=\"50%\"}\n```{shinylive-r}\n#| standalone: true\n#| components: [viewer]\n#| column: screen-inset-shaded\n#| viewerHeight: 800\nlibrary(shiny)\nlibrary(shinyMobile)\n\nshinyApp(\n ui = f7Page(\n title = \"Update radio\",\n f7SingleLayout(\n navbar = f7Navbar(title = \"Update f7Radio\"),\n f7Block(\n f7Radio(\n inputId = \"radio\",\n label = \"Custom choices\",\n choices = list(\n f7RadioChoice(\n \"Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n Nulla sagittis tellus ut turpis condimentum,\n ut dignissim lacus tincidunt\",\n title = \"Choice 1\",\n subtitle = \"David\",\n after = \"March 16, 2024\"\n ),\n f7RadioChoice(\n \"Cras dolor metus, ultrices condimentum sodales sit\n amet, pharetra sodales eros. Phasellus vel felis tellus.\n Mauris rutrum ligula nec dapibus feugiat\",\n title = \"Choice 2\",\n subtitle = \"Veerle\",\n after = \"March 17, 2024\"\n )\n ),\n selected = 2,\n style = list(\n outline = TRUE,\n strong = TRUE,\n inset = TRUE,\n dividers = TRUE\n )\n ),\n textOutput(\"res\")\n )\n )\n ),\n server = function(input, output, session) {\n output$res <- renderText(input$radio)\n }\n)\n```\n:::\n\n::: {.column width=\"25%\"}\n\n:::\n\n::::\n\n\n\n\n::: {.cell}\n\n```{.r .cell-code}\nlibrary(shiny)\nlibrary(shinyMobile)\n\nshinyApp(\n ui = f7Page(\n title = \"Update radio\",\n f7SingleLayout(\n navbar = f7Navbar(title = \"Update f7Radio\"),\n f7Block(\n f7Radio(\n inputId = \"radio\",\n label = \"Custom choices\",\n choices = list(\n f7RadioChoice(\n \"Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n Nulla sagittis tellus ut turpis condimentum,\n ut dignissim lacus tincidunt\",\n title = \"Choice 1\",\n subtitle = \"David\",\n after = \"March 16, 2024\"\n ),\n f7RadioChoice(\n \"Cras dolor metus, ultrices condimentum sodales sit\n amet, pharetra sodales eros. Phasellus vel felis tellus.\n Mauris rutrum ligula nec dapibus feugiat\",\n title = \"Choice 2\",\n subtitle = \"Veerle\",\n after = \"March 17, 2024\"\n )\n ),\n selected = 2,\n style = list(\n outline = TRUE,\n strong = TRUE,\n inset = TRUE,\n dividers = TRUE\n )\n ),\n textOutput(\"res\")\n )\n )\n ),\n server = function(input, output, session) {\n output$res <- renderText(input$radio)\n }\n)\n```\n:::\n\n\n\n\n### New `f7Treeview()` component\n\nThe new release welcomes a brand new input widget. As its name suggests, `f7Treewiew()` enables sorting items hierarchically within a collapsible nested list of items.\nThis is ideal, for instance, to select files within multiple folders, as an alternative to the classic `fileInput()`.\n\n:::: {.columns}\n::: {.column width=\"25%\"}\n\n:::\n\n::: {.column width=\"50%\"}\n```{shinylive-r}\n#| standalone: true\n#| components: [viewer]\n#| column: screen-inset-shaded\n#| viewerHeight: 800\nlibrary(shiny)\nlibrary(shinyMobile)\n\nshinyApp(\n ui = f7Page(\n title = \"My app\",\n f7SingleLayout(\n navbar = f7Navbar(title = \"f7Treeview\"),\n # group treeview with selectable items\n f7BlockTitle(\"Selectable items\"),\n f7Block(\n f7Treeview(\n id = \"treeview1\",\n selectable = TRUE,\n f7TreeviewGroup(\n title = \"Selected images\",\n icon = f7Icon(\"folder_fill\"),\n itemToggle = TRUE,\n lapply(1:3, function(i) f7TreeviewItem(label = paste0(\"image\", i, \".png\"),\n icon = f7Icon(\"photo_fill\")))\n )\n ),\n textOutput(\"selected\")\n ),\n\n # group treeview with checkbox items\n f7BlockTitle(\"Checkbox\"),\n f7Block(\n f7Treeview(\n id = \"treeview2\",\n withCheckbox = TRUE,\n f7TreeviewGroup(\n title = \"Selected images\",\n icon = f7Icon(\"folder_fill\"),\n itemToggle = TRUE,\n lapply(1:3, function(i) f7TreeviewItem(label = paste0(\"image\", i, \".png\"),\n icon = f7Icon(\"photo_fill\")))\n )\n ),\n textOutput(\"selected2\")\n )\n )\n ),\n server = function(input, output) {\n output$selected <- renderText(input$treeview1)\n output$selected2 <- renderText(input$treeview2)\n }\n)\n```\n:::\n\n::: {.column width=\"25%\"}\n\n:::\n\n::::\n\n\n\n\n::: {.cell}\n\n```{.r .cell-code}\nlibrary(shiny)\nlibrary(shinyMobile)\n\nshinyApp(\n ui = f7Page(\n title = \"My app\",\n f7SingleLayout(\n navbar = f7Navbar(title = \"f7Treeview\"),\n # group treeview with selectable items\n f7BlockTitle(\"Selectable items\"),\n f7Block(\n f7Treeview(\n id = \"treeview1\",\n selectable = TRUE,\n f7TreeviewGroup(\n title = \"Selected images\",\n icon = f7Icon(\"folder_fill\"),\n itemToggle = TRUE,\n lapply(1:3, function(i) f7TreeviewItem(label = paste0(\"image\", i, \".png\"),\n icon = f7Icon(\"photo_fill\")))\n )\n ),\n textOutput(\"selected\")\n ),\n\n # group treeview with checkbox items\n f7BlockTitle(\"Checkbox\"),\n f7Block(\n f7Treeview(\n id = \"treeview2\",\n withCheckbox = TRUE,\n f7TreeviewGroup(\n title = \"Selected images\",\n icon = f7Icon(\"folder_fill\"),\n itemToggle = TRUE,\n lapply(1:3, function(i) f7TreeviewItem(label = paste0(\"image\", i, \".png\"),\n icon = f7Icon(\"photo_fill\")))\n )\n ),\n textOutput(\"selected2\")\n )\n )\n ),\n server = function(input, output) {\n output$selected <- renderText(input$treeview1)\n output$selected2 <- renderText(input$treeview2)\n }\n)\n```\n:::\n\n\n\n\n### New `f7Form()`\n\nShiny does not provide HTML forms handling out of the box (a [form](https://www.w3schools.com/html/html_forms.asp) 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:\n\n- `f7Text()`\n- `f7TextArea()`\n- `f7Password()`\n- `f7Select()`\n\n:::: {.columns}\n::: {.column width=\"25%\"}\n\n:::\n\n::: {.column width=\"50%\"}\n```{shinylive-r}\n#| standalone: true\n#| components: [viewer]\n#| column: screen-inset-shaded\n#| viewerHeight: 800\n#| viewerWidth: 390\nlibrary(shiny)\nlibrary(shinyMobile)\n\nshinyApp(\n ui = f7Page(\n f7SingleLayout(\n navbar = f7Navbar(title = \"Inputs form\"),\n f7Block(f7Button(\"update\", \"Click me\")),\n f7BlockTitle(\"A list of inputs in a form\"),\n f7List(\n inset = TRUE,\n dividers = FALSE,\n strong = TRUE,\n f7Form(\n id = \"myform\",\n f7Text(\n inputId = \"text\",\n label = \"Text input\",\n value = \"Some text\",\n placeholder = \"Your text here\",\n style = list(\n description = \"A cool text input\",\n outline = TRUE,\n media = f7Icon(\"house\"),\n clearable = TRUE,\n floating = TRUE\n )\n ),\n f7TextArea(\n inputId = \"textarea\",\n label = \"Text Area\",\n value = \"Lorem ipsum dolor sit amet, consectetur\n adipiscing elit, sed do eiusmod tempor incididunt ut\n labore et dolore magna aliqua\",\n placeholder = \"Your text here\",\n resize = TRUE,\n style = list(\n description = \"A cool text input\",\n outline = TRUE,\n media = f7Icon(\"house\"),\n clearable = TRUE,\n floating = TRUE\n )\n ),\n f7Password(\n inputId = \"password\",\n label = \"Password:\",\n placeholder = \"Your password here\",\n style = list(\n description = \"A cool text input\",\n outline = TRUE,\n media = f7Icon(\"house\"),\n clearable = TRUE,\n floating = TRUE\n )\n )\n )\n ),\n verbatimTextOutput(\"form\")\n )\n ),\n server = function(input, output, session) {\n output$form <- renderPrint(input$myform)\n\n observeEvent(input$update, {\n updateF7Form(\n \"myform\",\n data = list(\n \"text\" = \"New text\",\n \"textarea\" = \"New text area\",\n \"password\" = \"New password\"\n )\n )\n })\n }\n)\n```\n:::\n\n::: {.column width=\"25%\"}\n\n:::\n\n::::\n\n\n\n\n::: {.cell}\n\n```{.r .cell-code}\nlibrary(shiny)\nlibrary(shinyMobile)\n\nshinyApp(\n ui = f7Page(\n f7SingleLayout(\n navbar = f7Navbar(title = \"Inputs form\"),\n f7Block(f7Button(\"update\", \"Click me\")),\n f7BlockTitle(\"A list of inputs in a form\"),\n f7List(\n inset = TRUE,\n dividers = FALSE,\n strong = TRUE,\n f7Form(\n id = \"myform\",\n f7Text(\n inputId = \"text\",\n label = \"Text input\",\n value = \"Some text\",\n placeholder = \"Your text here\",\n style = list(\n description = \"A cool text input\",\n outline = TRUE,\n media = f7Icon(\"house\"),\n clearable = TRUE,\n floating = TRUE\n )\n ),\n f7TextArea(\n inputId = \"textarea\",\n label = \"Text Area\",\n value = \"Lorem ipsum dolor sit amet, consectetur\n adipiscing elit, sed do eiusmod tempor incididunt ut\n labore et dolore magna aliqua\",\n placeholder = \"Your text here\",\n resize = TRUE,\n style = list(\n description = \"A cool text input\",\n outline = TRUE,\n media = f7Icon(\"house\"),\n clearable = TRUE,\n floating = TRUE\n )\n ),\n f7Password(\n inputId = \"password\",\n label = \"Password:\",\n placeholder = \"Your password here\",\n style = list(\n description = \"A cool text input\",\n outline = TRUE,\n media = f7Icon(\"house\"),\n clearable = TRUE,\n floating = TRUE\n )\n )\n )\n ),\n verbatimTextOutput(\"form\")\n )\n ),\n server = function(input, output, session) {\n output$form <- renderPrint(input$myform)\n\n observeEvent(input$update, {\n updateF7Form(\n \"myform\",\n data = list(\n \"text\" = \"New text\",\n \"textarea\" = \"New text area\",\n \"password\" = \"New password\"\n )\n )\n })\n }\n)\n```\n:::\n\n\n\n\n## Breaking changes\n\nSome 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](https://github.com/RinteRface/shinyMobile/blob/69da6ca46984bf6c73e2dc32ff8d9f415ec36a30/NEWS.md#breaking-changes) to see a list of all changes in this release.\n\n## Soft deprecation\n\nSome 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.\n\n# Conclusion\n\nSince 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](https://docs.r-wasm.org/webr/latest/) side, it isn't impossible that one day, we might run a totally offline Shiny app on mobile.\n", "supporting": [], "filters": [ "rmarkdown/pagebreak.lua" diff --git a/posts/2024-05-13-shinyMobile-2.0.0/index.qmd b/posts/2024-05-13-shinyMobile-2.0.0/index.qmd index 266267e..a1b49a1 100644 --- a/posts/2024-05-13-shinyMobile-2.0.0/index.qmd +++ b/posts/2024-05-13-shinyMobile-2.0.0/index.qmd @@ -9,9 +9,8 @@ categories: format: html: code-fold: true -filters: +filters: - shinylive -draft: true --- ![](logo.png){width=25% fig-align="center"} @@ -270,12 +269,6 @@ By updating to the latest Framework7 version, we now benefit from a totally reva #| components: [viewer] #| column: screen-inset-shaded #| viewerHeight: 800 -webr::unmount("/usr/lib/R/library/shinyMobile") -webr::install( - "shinyMobile", - repos = c("https://rinterface.github.io/rinterface-wasm-cran/") -) - library(shiny) library(shinyMobile) @@ -890,12 +883,6 @@ This adds a description to the input (below its main content), as well as the ou #| components: [viewer] #| column: screen-inset-shaded #| viewerHeight: 800 -webr::unmount("/usr/lib/R/library/shinyMobile") -webr::install( - "shinyMobile", - repos = c("https://rinterface.github.io/rinterface-wasm-cran/") -) - library(shiny) library(shinyMobile) @@ -1129,12 +1116,6 @@ Moreover, we added a new way to pass options to `f7Radio()` and `f7CheckboxGroup #| components: [viewer] #| column: screen-inset-shaded #| viewerHeight: 800 -webr::unmount("/usr/lib/R/library/shinyMobile") -webr::install( - "shinyMobile", - repos = c("https://rinterface.github.io/rinterface-wasm-cran/") -) - library(shiny) library(shinyMobile) @@ -1255,12 +1236,6 @@ This is ideal, for instance, to select files within multiple folders, as an alte #| components: [viewer] #| column: screen-inset-shaded #| viewerHeight: 800 -webr::unmount("/usr/lib/R/library/shinyMobile") -webr::install( - "shinyMobile", - repos = c("https://rinterface.github.io/rinterface-wasm-cran/") -) - library(shiny) library(shinyMobile) @@ -1390,12 +1365,6 @@ Shiny does not provide HTML forms handling out of the box (a [form](https://www. #| column: screen-inset-shaded #| viewerHeight: 800 #| viewerWidth: 390 -webr::unmount("/usr/lib/R/library/shinyMobile") -webr::install( - "shinyMobile", - repos = c("https://rinterface.github.io/rinterface-wasm-cran/") -) - library(shiny) library(shinyMobile)