Shiny remove extra row added by external link in navbar tab panel - r

I'm trying to add an external link to the tab panel title in a shiny navbar. The link itself works fine, but it moves the tab with the link into a separate row.
How can I include a link and maintain the placement of the tab in the same row as any other tabs that don't contain links?
Here is my minimalistic code:
library(shiny)
ui <- navbarPage(
title = "",
id = "navbar",
header = "",
tabsetPanel(id="tabs",
tabPanel(
title = "Tab1", value = "tab1"
),
tabPanel(
title = a("Tab2",
href = "https://www.google.com/"),
value = "tab2"
)
)
)
server <- function(input, output, session) {
}
shinyApp(ui, server)
I have tried using the HTML function to see if that for some reason gives a different result, but as expected it didn't:
tabPanel(
title = HTML("<a href = 'https://www.google.com/'>tab2</a>"),
value = "tab2"
)
I would really appreciate your advice!
If you also happen to have any idea on how to remove the title row from the navbarPage, that would also be much appreciated.

If you look at the HTML for your tabs, you can see that the tabs themselves already have a <a href ...> tag. So what you're doing is adding another one below the existing one.
A work-around is to do something like
Observe when Tab2 is pressed
Navigate to the URL
library(shiny)
ui <- navbarPage(
title = "",
id = "navbar",
header = "",
tabsetPanel(
id = "tabs",
tabPanel(title = "Tab1"),
tabPanel(title = "Tab2")
)
)
server <- function(input, output, session) {
observeEvent(input$tabs, {
print(input$tabs)
if( input$tabs == "Tab2" ) {
browseURL("https://www.google.com/")
}
})
}
shinyApp(ui, server)

One way to do this, is to use a javascript function to do the linking for us. Then we do not need to include <a href> inside the tab which is already a link!.
We can easily set up a Js function with {shinyjs} and extendShinyjs(). Then we call it in an observeEvent() when the tab is clicked.
library(shiny)
library(shinyjs)
ui <- navbarPage(
# use shinyjs
useShinyjs(),
# write JS function to open window
shinyjs::extendShinyjs(text = "shinyjs.myfun = function() { window.open('https://www.google.com/', '_self'); }",
functions = c("myfun")),
title = "",
id = "navbar",
header = "",
tabsetPanel(id="tabs",
tabPanel(
title = "Tab1", value = "tab1"
),
tabPanel(
title = "Tab2",
value = "tab2"
)
)
)
server <- function(input, output, session) {
# use observeEvent to check if user clicks tab no 2
observeEvent(input$tabs,
{
if (input$tabs == "tab2") {
shinyjs::js$myfun()
}
})
}
shinyApp(ui, server)

Related

Adding tinyMCE editor in Shiny modal

I'd like to create an HTML editor in a Shiny app, using the shinyMCE package.
This works well in the example below.
library(shiny)
library(shinyMCE)
library(shinyjs)
library(shinyWidgets)
library(shinydashboard)
ui <- dashboardPage(
useShinyjs(),
header = dashboardHeader(disable = T),
sidebar = dashboardSidebar(disable = T),
body = dashboardBody(
tags$script(src = "http://cdn.tinymce.com/4/tinymce.min.js",
referrerpolicy = "origin"),
tinyMCE("editor", "The content"),
actionButton("ok", "OK")
))
server <- function(input, output, session)
{
observeEvent(
input$ok,
{
print(input$editor)
}
)
observeEvent(
input$open,
{
showModal(myModal())
})
}
shinyApp(ui, server = server)
Indeed, if you press OK, the content of the editor is printed in the R console.
Now, I'd like to put the editor in a modal. If I do the following the editor appears, but if I press OK the content doesn't get updated. That is, the R console always shows "the content", independently of what is written in the textarea.
library(shiny)
library(shinyMCE)
library(shinyjs)
library(shinyWidgets)
library(shinydashboard)
ui <- dashboardPage(
useShinyjs(),
header = dashboardHeader(disable = T),
sidebar = dashboardSidebar(disable = T),
body = dashboardBody(
tags$script(src = "http://cdn.tinymce.com/4/tinymce.min.js",
referrerpolicy = "origin"),
flowLayout (
actionButton("open", "Open")
)))
myModal <- function()
{
modalDialog(size = "l",
title = "A modal dialog",
tinyMCE("tinyTxt", "the content"),
actionButton("ok", "OK"),
easyClose = T)
}
server <- function(input, output, session)
{
observeEvent(
input$ok,
{
print(input$tinyTxt)
}
)
observeEvent(
input$open,
{
showModal(myModal())
})
}
shinyApp(ui, server = server)
In the JS console I get
Uncaught TypeError: Cannot read property 'getContent' of null
at exports.InputBinding.getValue (<anonymous>:9:41)
at c (init_shiny.js:117)
at init_shiny.js:163
at eN.<anonymous> (<anonymous>:16:18)
at mp.c.fire (tinymce.min.js:2)
at eN.fire (tinymce.min.js:2)
at eN.<anonymous> (tinymce.min.js:2)
at mp.c.fire (tinymce.min.js:2)
at eN.fire (tinymce.min.js:2)
at Rp (tinymce.min.js:2)
Any idea of how to get around the problem?
EDIT: One further observation. In the first (working) example tinyMCE.editors contains one instance of an editor, while in the second it is empty (although the editor does display!).
I managed to solve this, by manually creating the TinyMCE editor (which solves the issue of the editor not appearing in tinymce.editors) and then use some custom JS to retrieve the value.
This seems a bit hacky to me, but it works...
Here's an example
library(shiny)
library(shinyjs)
library(shinyWidgets)
library(shinydashboard)
ui <- dashboardPage(
useShinyjs(),
header = dashboardHeader(disable = T),
sidebar = dashboardSidebar(disable = T),
body = dashboardBody(
singleton(tags$head(tags$script(src = "http://cdn.tinymce.com/4/tinymce.min.js",
referrerpolicy = "origin"))),
# Register a custom message handler that gets the content of the editor
# and forces update of the textarea
singleton(tags$head(tags$script("Shiny.addCustomMessageHandler('getTxt',
function(message) {
var content = tinyMCE.get('tinyTxt').getContent();
Shiny.onInputChange('tinyTxt', content);
})"))),
flowLayout (
actionButton("open", "Open"),
htmlOutput("content")
)))
myModal <- function()
{
modalDialog(size = "l",
title = "A modal dialog",
textAreaInput("tinyTxt", "the content"),
actionButton("ok", "OK"),
easyClose = T)
}
server <- function(input, output, session)
{
observeEvent(
input$ok,
{
# Retrieve the content of the editor
session$sendCustomMessage("getTxt", "")
removeModal()
})
output$content <- renderText(
input$tinyTxt
)
observeEvent(
input$open,
{
showModal(myModal())
# Create the tinyMCE editor
runjs("var ed = new tinymce.Editor('tinyTxt', {
selector: 'tinyTxt',
theme: 'modern'},
tinymce.EditorManager);
ed.render();")
})
}
shinyApp(ui, server = server)
For anyone looking at this in 2022, this is an updated solution working with version 6 of TinyMCE. You need to get an API key which is available on registration at https://www.tiny.cloud/
I also added a print to the R console with a delay() as the returned input was blank otherwise.
There are a few additional options compared to the original solution. I kept them there to show how it's done. Hopefully this is useful to someone!
library(shinyjs)
library(shinyWidgets)
library(shinydashboard)
ui <- dashboardPage(
useShinyjs(),
header = dashboardHeader(disable = T),
sidebar = dashboardSidebar(disable = T),
body = dashboardBody(
tags$head(tags$script(src = "https://cdn.tiny.cloud/1/--API-KEY-HERE--/tinymce/6/tinymce.min.js",
referrerpolicy = "origin")),
# Register a custom message handler that gets the content of the editor
# and forces update of the textarea
tags$head(tags$script("Shiny.addCustomMessageHandler('getTxt',
function(message) {
var content = tinymce.activeEditor.getContent();;
Shiny.onInputChange('tinyTxt', content);
})")),
flowLayout (
actionButton("open", "Open"),
htmlOutput("content")
)))
myModal <- function()
{
modalDialog(size = "l",
title = "A modal dialog",
textAreaInput("tinyTxt", "the content"),
actionButton("ok", "OK"),
easyClose = T)
}
server <- function(input, output, session)
{
observeEvent(
input$open,
{
showModal(myModal())
# Create the tinyMCE editor
runjs("var ed = new tinymce.Editor('tinyTxt', {
menubar: false,
branding: false,
plugins: 'lists, table, link',
contextmenu: 'lists, link, table',
toolbar1: 'bold italic forecolor backcolor | formatselect fontselect fontsizeselect | alignleft aligncenter alignright alignjustify',
toolbar2: 'undo redo removeformat bullist numlist table blockquote code superscript subscript strikethrough link'},
tinymce.EditorManager);
ed.render();")
})
observeEvent(
input$ok,
{
# Retrieve the content of the editor
session$sendCustomMessage("getTxt", "")
output$content <- renderText(
input$tinyTxt
)
delay(500, print(input$tinyTxt))
removeModal()
})
}
shinyApp(ui, server = server)

updateTabItems not working in module if input passed

I want to create a function/a module to update tabset items.
I'm aware of this and this question, but my issue relates to how the button input is handled in updateTabItems.
Here you can find an example:
library(shiny)
library(shinydashboard)
library(shinydashboardPlus)
mod_updateTabset <- function(
input, output, session, triggerId, dashboardId, tab, parent
) {
observeEvent(triggerId, {
updateTabItems(parent, dashboardId, selected = tab)
})
}
ui <- dashboardPagePlus(
header = dashboardHeaderPlus(),
sidebar = dashboardSidebar(
sidebarMenu(
id = 'Tabs',
menuItem("Tab 01", tabName = "tab01", icon = icon("dice-one")),
menuItem("Tab 02", tabName = "tab02", icon = icon("dice-two"))
)
),
body = dashboardBody(
tabItems(
tabItem(
tabName = "tab01",
actionButton("updateButton", label = "To Tab02")
),
tabItem(
tabName = "tab02",
h4("New Tab")
)
)
)
)
server <- function(input, output, session) {
callModule(
mod_updateTabset,
"updateLink",
triggerId = input$updateButton,
dashboardId = "Tabs",
tab = "tab02",
parent = session
)
}
shinyApp(ui = ui, server = server)
I know it's working when creating mod_updateTabset_UI, shifting the actionButton to the module. That's why I suppose the issue lies in the button handling.
Still, I'd like to have a function/module which can handle all kind of links, buttons etc. to update Tab items, not just one button
For anyone who runs into the same problem: You need to use reactive() for triggerId when calling the module
callModule(
mod_updateTabset,
"updateLink",
triggerId = reactive(input$updateButton),
dashboardId = "Tabs",
tab = "tab02",
parent = session
)
The module then needs to handle a reactive value:
mod_updateTabset <- function(
input, output, session, triggerId, dashboardId, tab, parent
) {
observeEvent(triggerId(), {
updateTabItems(parent, dashboardId, selected = tab)
})
}

Pop up a shinyFiles dialog R Shiny without a shinyFiles button

I would like to have one of the tabPanels in my Shiny app launch a shinyFiles style input. In this case I would like to launch a shinySaveButton, without the shinySaveButton being in my dataset (By clicking the save icon [which is actually a tabPanel])
Reproducible example below
library(shiny)
library(shinyFiles)
ui <- navbarPage('Test App',id = "inTabset", selected="panel1",
tabPanel(title = "", value = "Save", icon = icon("save")),
tabPanel(title = "Panel 1", value = "panel1",
h1("Panel1")),
tabPanel(title = "Panel 2",value = "panel2",
h1("Panel2"))
)
server <- function(input, output, session) {
values = reactiveValues(tabSelected="panel1")
observe({
if (input$inTabset=="Save") {
updateNavbarPage(session,"inTabset",selected=values$tabSelected)
#CODE FOR LOADING SHINYFILES DIALOG IN HERE
} else {
values$tabSelected<-input$inTabset
}
})
}
shinyApp(ui, server)
Any help would be greatly appreciated.
Work around using hidden element trick
library(shiny)
library(shinyFiles)
library(shinyjs)
jsCode<-"shinyjs.saveButton=function(){ $('#buttonFileSaveHidden').click(); }"
ui <- fluidPage(
useShinyjs(),
extendShinyjs(text = jsCode),
navbarPage('Test App',id = "inTabset", selected="panel1",
tabPanel(title = "", value = "Save", icon = icon("save")),
tabPanel(title = "Panel 1", value = "panel1",
h1("Panel1")
),
tabPanel(title = "Panel 2",value = "panel2",
h1("Panel2"))
),
# HIDDEN BUTTON TO INITIATE THE SAVE
hidden(shinySaveButton( "buttonFileSaveHidden",
label="",
title="Save as ...",
list('hidden_mime_type'=c("R")),
class='hiddenButton')),
wellPanel( #ONLY INCLUDED TO DISPLAY OF PATH INFO OF THE CHOICE
h3('Current save path info'),
tableOutput('table')
)
)
server <- function(input, output, session) {
values = reactiveValues(tabSelected="panel1")
observe({
if (input$inTabset=="Save") {
updateNavbarPage(session,"inTabset",selected=values$tabSelected)
#CODE FOR LOADING SHINYFILES DIALOG IN HERE
js$saveButton()
} else {
values$tabSelected<-input$inTabset
}
})
shinyFileSave(input, "buttonFileSaveHidden", session=session, roots=c(wd="~"), filetypes=c('R') ) #hidden
# GET THE SAVE PATH CHOICE AND RECORD IT IN fp.dt.rv
fp.dt.rv<-reactiveVal("")
observeEvent(input$buttonFileSaveHidden,{
fp.dt<-parseSavePath(c(wd='~'), input$buttonFileSaveHidden)
fp.dt.rv(fp.dt) #or just use to immediately write.
})
# ONLY TO DISPLAY THE SAVE CHOICE
output$table <- renderTable(fp.dt.rv())
}
shinyApp(ui, server)

R Shiny reactive triggered by navigating to particular tabPanel in navbarPage

writing with a shiny question. I have a navbarPage, id = "navbar", and in the navbarMenu user can select one among several tabPanels. Each tabPanel is assigned a value (value = 1, value = 2, etc). So input$navbar is reactive value with the value of the selected tabPanel.
I have a reactive expression defined which reacts to the changing of the tabPanel (reacts based on input$navbar). What I actually want is for this to react to navigating to a particular tabPanel, but not navigating away from that tabPanel. So, when input$navbar changes from 1 to 2 I want a reaction, but when changing from 2 to 1 no reaction. How can I achieve this?
Here is relevant snippet of my code, I don't think I need a full reproducible example for this but let me know if I'm wrong.
#ui snippet
navbarPage(id = "navbar",
navbarMenu(title = "Title",
tabPanel(title = "tp1", value = 1),
tabPanel(title = "tp2", value = 2),
#more tabPanels and ui stuff...
#server snippet
rctvfx <- reactive({
#want this to react only when input$navbar changes from ==1 to ==2
input$navbar
isolate({
#do stuff
})
})
You can use an if statement. This makes sure the code only runs if the user navigated to the corresponding tab.
library(shiny)
shinyApp(
ui = navbarPage(
"App Title",
id = "navbar",
tabPanel("Plot"),
navbarMenu(
"More",
tabPanel("Summary"),
"----",
"Section header",
tabPanel("Table")
)
),
server = function(input, output){
observe({
if (req(input$navbar) == "Table")
message("Table has been selected")
if (req(input$navbar) == "Plot")
message("Plot has been selected")
})
}
)
I would recomment do use observe rather than reactive to make sure everything runs even if all observers for the reactive are idle.
Another example of the same answer as above
library(shiny)
shinyApp(
ui = navbarPage(
"App Title",
id = "navbar",
tabPanel("Plot"),
navbarMenu(
"More",
tabPanel("Summary"),
tabPanel("Table"),
mainPanel(dataTableOutput("d"))
)
),
server = function(input, output){
output$d = renderDataTable({
if ((input$navbar) == "Table") {
head(mtcars)
} else {
((input$navbar) == "Plot")
head(iris)
}
})
}
)

close (Chrome) browser window on navbar click

I have been using the approach suggested by #wch on SO to close the browser window when clicking an action button in a Shiny app. Works great.
I would now like to stop my app and close the bowser window (in Chrome) when clicking an element in the navbar. Below the tabPanel call I'm using
tabPanel(title = "", value = "Stop", icon = icon("power-off"))
I use an observer to stop the app when the value of input$navbar == "Stop" (i.e, when the icon in the navbar is selected) but I'm not sure how to activate the window.close() call.
Code for action button to close browser windows by #wch
tags$button(
id = 'close',
type = "button",
class = "btn action-button",
onclick = "window.close();",
"Close window"
)
EDIT:
Found a work-around that does what I want.
tabPanel(tags$a(id = "quitApp", href = "#", class = "action-button",
list(icon("power-off"), ""), onclick = "window.close();"))
Unfortunately it leads to a rather badly aligned navbar. I asked a related question on the Shiny google group
You can use the shinyjs package to easily call javascript functions, which is essentially all you need to do. Disclaimer: I wrote that package. Here's the code to do what you want:
library(shinyjs)
jscode <- "shinyjs.closewindow = function() { window.close(); }"
runApp(shinyApp(
ui = tagList(
useShinyjs(),
extendShinyjs(text = jscode),
navbarPage(
"test",
id = "navbar",
tabPanel(title = "tab1"),
tabPanel(title = "", value = "Stop", icon = icon("power-off"))
)
),
server = function(input, output, session) {
observe({
if (input$navbar == "Stop") {
js$closewindow();
stopApp()
}
})
}
))
EDIT:
If you don't want to use a JS package, you can do the same thing with native shiny:
jscode <- "Shiny.addCustomMessageHandler('closeWindow', function(m) {window.close();});"
runApp(shinyApp(
ui = tagList(
tags$head(tags$script(HTML(jscode))),
navbarPage(
"test",
id = "navbar",
tabPanel(title = "tab1"),
tabPanel(title = "", value = "Stop", icon = icon("power-off"))
)
),
server = function(input, output, session) {
observe({
if (input$navbar == "Stop") {
session$sendCustomMessage(type = "closeWindow", message = "message")
stopApp()
}
})
}
))

Resources