With the following piece of code I'm able to trigger a pure javascript alert by clicking on the question-mark of the fileInput:
fileInput('peptides',
span("Peptides file ",
id="peptidesSpan",
tags$a(
tags$i(class='fa fa-question-circle'),
href = "#",
onclick = "alert('Oops!'); return false;")
),
multiple=FALSE,
accept = c('text/csv','text/comma-separated-values',
)
)
I was wondering if I could trigger a shinyalert popup (https://github.com/daattali/shinyalert/) instead of a simple javascript alert directly form the UI without any observer in the server side.
Something like:
shinyalert("Oops!", "Something went wrong.", type = "error")
If there is not a workaround to do that, any other suggestion with an observer would be welcome as well.
I think using an observer is not at all inconvenient.
Instead of alert(), invoke Shiny.setInputValue(id, value);, and then on your server side you can observeEvent(input[id], { shinyalert() }).
Read this article for details: https://shiny.rstudio.com/articles/communicating-with-js.html
You only need to use one observe code block to achieve this.
An example
Define a customized function in your UI Javascript code and call it in your onclick.
You can put this function say in helper.js in the 'www' folder in your project folder, that will be www/helper.js. Include this file in your Shiny UI code by tags$head(tags$script(src = "helper.js"))
function showAlert(message, type = "info") {
Shiny.setInputValue(
'alertMessage',
{
message: message,
type: type
},
{ priority: 'event' }
);
}
Then on the Shiny server side define the observer once
observeEvent(input$alertMessage, {
alertData <- input$alertMessage
req(alertData)
shinyalert("title", alertData$message, type = alertData$type)
})
It's one of the few times that I answer my own post, but after searching a bit more on stack-overflow I found a workaround inspired by this post.
I downloaded the sweetAlert.js file of the Sweet Alert library directly from here
I create a www folder in the root of my Shiny application
I added the sweetAlert.js file in the www directory and in the dashboardBody() of the ui.R I added the following line:
tags$head(tags$script(src="sweetAlert.js"))
Now I'm able to call directly the Swal.fire function with any argument as I would normally do in any other framework which runs javascript.
Related
How can a user defined app state be stored using R Shiny so that it is both accessible in R code and in jQuery code?
For example, my app has two states black and white. Options for storing the current state include
as hidden text in the DOM
as a data attribute of an DOM element
use local storage on the browser
Is there a better approach that is built in to Shiny for this purpose? Something like having a global variable defined in R that can be read in jQuery without having to send a message.
The canonical approach to communicate with JavaScript is indeed via sending a message.
Of course you can rely on pure HTML <-> JS communication means as well:
library(shiny)
read_js <- "$(function() {
$('#output').append(`Text from data: ${$('#constant_data').data('state')}`,
$('<br>'),
`Text from hidden: ${$('#constant_hidden').text()}`,
$('<br>'),
`Text from session: ${sessionStorage.getItem('constant_session')}`,
$('<br>'),
`Text from window: ${window.state}`);
});
"
ui <- fluidPage(
tags$head(tags$script("sessionStorage.setItem('constant_session', 'black');
window.state = 'black';")),
tags$head(tags$script(HTML(read_js))),
div(id = "constant_data", `data-state` = "black"),
div(id = "constant_hidden", "black", style = "display: none"),
div(id = "output"),
)
server <- function(input, output, session) {
}
shinyApp(ui, server)
And the choice of the method is up to you.
Some questions which may guide your choice:
Should the state by visible in the DOM?
Does the state change over time?
Should the state be persistent over sessions?
This was a complicating in logic execution i came across using {polished} and {brochure}.
When placing secure_ui/secure_server inside of a brochure::Page() in the same order of the example given by the {polished} dev team, there are changes to how a Shiny App is deploy on the {brochure} infrastructure. I was not sure where to relocate the polsiehd logic to.
Differences
no global.R file in a brochureApp()
multiple calls to different module_ui/server functions since each brochure::page() is its owns shiny session
single page shinyApp vs true multipage shinyApp
When needing to merge the two logics you must:
move polished_config() in globals.R --> golem::runApp() [initiate global setting for brochureApp()]
run_app <- function(
onStart = NULL,
options = list(),
enableBookmarking = NULL,
...
) {
# old 'globals.R' logic
polished_config(
app_name = "humblFinance",
api_key = "xxxx"
)
with_golem_options(
app = brochureApp(
# Putting the resources here
golem_add_external_resources(),
page1(),
),
golem_opts = list(...)
)
}
wrap each brochure::page() ui/server with polished::secure_ui/server()`
# an example login page
login <- function(id = "login", href = "/login") {
page(
href = href,
ui = secure_ui(
mod_login_ui(id = id),
sign_in_page_ui = sign_in_custom()
),
server = secure_server(
function(input, output, session) {
mod_login_server(id = id)
}
)
)
}
NOTE
sign_in_custom() is a function that returns a customized UI object from polished::sign_in_default() to create personal business webpages.
I would recommend wrapping polished::sign_in_default() in a custom global function since you will need to define this on ever brochure::page() that you want to have protected behind polished auth.
once you authenticate one page through polished, you will be able to access all other protected pages while you are still logged in. After loggign out and attempting to access any one of the protected pages will result in a custom login page
I'm trying to injectScript via Custom Variable Template (not tag).
Here is simplified code:
const log = require('logToConsole');
const setTimeout = require('callLater');
const setInWindow = require('setInWindow');
const copyFromWindow = require('copyFromWindow');
const copyFromDataLayer = require('copyFromDataLayer');
const injectScript = require('injectScript');
const pixelSend = function(eventType, eventParams, tries) {
// logic
log('success')
};
log('event - ', copyFromDataLayer('event'));
if (copyFromDataLayer('event') === 'gtm.js') {
injectScript('https://vk.com/js/api/openapi.js', // this one should create **"VK"** object in global scope, used to actually send the events
pixelSend(),
data.gtmOnFailure);
}
return true;
Unfortunately openapi.js never gets injected (checking in network tab) and thus VK object never gets created and I cannot use it.
If I just run in console:
var head = document.getElementsByTagName('head')[0]
var js = document.createElement('script');
js.src = 'https://vk.com/js/api/openapi.js';
head.appendChild(js);
It gets injected and VK object becomes available.
What am I doing wrong?
Just in case:
queryPermission('inject_script', 'https://vk.com/js/api/openapi.js') = true
I tried this, and there were just a few minor bugs - line 11 is missing a semicolon, and you did not mention if you allowed access to read the "event" key from the datalayer.
After that was fixed, the script worked as expected.
Obviously it will only work on the page view trigger (since this is the only case when event equals gtm.js. I probably would move the condition from the tag to the trigger).
Instead of "return true" you should end this will a call to data.gtmOnSuccess(), else you might have trouble using this tag in a tag sequence.
If in the template UI you hit the "run code" switch you will actually get information on all error in your code (alas one at a time, since execution stops at the first error). You can also write tests with mock input, for templates that require settings via input fields.
I'm developing a simple custom tag template for Google Tag Manager. It's supposed to bind to some events and send event data to our servers as JSON in the body of a POST request.
The sandboxed GTM Javascript runtime provides the sendPixel() API. However, that only provides GET requests.
How one sends a POST request from within this sandboxed runtime?
You can use a combination of the injectScript and copyFromWindow APIs found here Custom Template APIs.
Basically, the workflow goes like this.
Build a simple script that contains a function attached to the window object that sends a normal XHR post request. The script I made and use can be found here: https://storage.googleapis.com/common-scripts/basicMethods.js
Upload that script somewhere publically accessible so you can import it into your template.
Use the injectScript API to add the script to your custom template.
The injectScript API wants you to provide an onSuccess callback function. Within that function, use the copyWindow api to grab the post request function you created in your script and save it as a variable.
You can now use that variable to send a post request the same way you would use a normal JS function.
The script I included above also includes JSON encode and Base64 encode functions which you can use the same way via the copyWindow api.
I hope that helps. If you need some specific code examples for parts I can help.
According to #Ian Mitchell answer - I've made similar solution.
This is the basic code pattern that can be used inside GTM template code section in such as scenario:
const injectScript = require('injectScript');
const callInWindow = require('callInWindow');
const log = require('logToConsole');
const queryPermission = require('queryPermission');
const postScriptUrl = 'https://myPostScriptUrl'; //provide your script url
const endpoint = 'https://myEndpoint'; //provide your endpoint url
//provide your data; data object contains all properties from fields tab of the GTM template
const data = {
sessionId: data.sessionId,
name: data.name,
description: data.description
};
//add appropriate permission to inject script from 'https://myPostScriptUrl' url in GTM template's privileges tab
if (queryPermission('inject_script', postScriptUrl)) {
injectScript(postScriptUrl, onSuccess, data.gtmOnFailure, postScriptUrl);
} else {
log('postScriptUrl: Script load failed due to permissions mismatch.');
data.gtmOnFailure();
}
function onSuccess() {
//add appropriate permission to call `sendData` variable in GTM template's privileges tab
callInWindow('sendData', gtmData, endpoint);
data.gtmOnSuccess();
}
It's important to remember to add all necessary privillages inside GTM template. Appropriate permissions will show automatically in privillages tab after use pertinent options inside code section.
Your script at 'https://myPostScriptUrl' may looks like this:
function sendData(data, endpoint) {
var xhr = new XMLHttpRequest();
var stringifiedData = JSON.stringify(data);
xhr.open('POST', endpoint);
xhr.setRequestHeader('Content-type', 'application/json');
xhr.send(stringifiedData);
xhr.onload = function () {
if (xhr.status.toString()[0] !== '2') {
console.error(xhr.status + '> ' + xhr.statusText);
}
};
}
It is not strictly necessary to load an external script. While still a workaround, you can also pass a fetch reference into the tag through a "JavaScript Variable" type variable:
Create a GTM variable of type "JavaScript Variable" with the content "fetch", thus referencing "window.fetch"
Add a text field to your Custom Tag, e. g. named "js.fetchReference".
Use data.fetchReference in your Custom Tag's like you normally would use window.fetch
Make sure the tag instance actually references the variable created in step 2 with {{js.fetchReference}}
I jotted this down with screenshots at https://hume.dev/articles/post-request-custom-template/
I am trying to set the initial value in a Wijimo Autocomplete control which has been loaded from an external data source. The scenario being a form is used to create some new data and then is saved. Subsequently the data needs to be edited so it is reloaded into the form.
I can successfully use the Autocomplete on the initial form - the source list is a JSON Array of objects which is loaded into the controller. The app is using UI Router so I resolve this first.
When I save the data I serialise the selected Object from the Autocomplete control and is then saved to a Mongo DB store. When loading this data back in it is converted back to an object.
This is what the control looks like:
<wj-auto-complete
selected-index="selectedIndexCombo"
selected-item="selectedAirline"
items-source="airlineCodes"
display-member-path="Title"
placeholder="Airline Code"
max-items="50"/>
An example of the source list looks like this:
{
"#href":"\/airline.nsf\/api\/data\/collections\/name\/(LUAirlines)\/unid\/8DCD734E7BCDA24D80257C99003770C4",
"#link":
{
"rel":"document",
"href":"\/airline.nsf\/api\/data\/documents\/unid\/8DCD734E7BCDA24D80257C99003770C4"
},
"#entryid":"98-8DCD734E7BCDA24D80257C99003770C4",
"#unid":"8DCD734E7BCDA24D80257C99003770C4",
"#noteid":"FB2",
"#position":"98",
"#siblings":100,
"#form":"Airline",
"AirlineCode":"WN",
"Airline":"Southwest Airlines",
"Title":"WN - Southwest Airlines"
}
So when the form is initially created the controller property selectedAirline is correctly set with the selected Object.
So this works fine in the save function:
$scope.formData.selectedAirline = JSON.stringify($scope.selectedAirline);
But when reloading in the data:
AirlineInfoFactory.loadAirlineInfo($scope.reference).then(function success(response) {
$scope.selectedAirline = eval('(' + response.data.selectedAirline + ')');
$scope.information = response.data.information;
$scope.dataLoaded = true;
console.log($scope.selectedAirline)
$scope.selectedIndexCombo=11;
})
The autocomplete control does not bind to the selectedAirline property.
I tried using the selected-index attribute on the directive so see if I could just change it to something when the data loads but it doesnt work either. I suspect its to do with the digest loop but I am not sure.
Any ideas?
Thanks
I tried to replicate the scenario by reloading the data and setting the selectedAirline property and it works well withe latest version 32. Here is the fiddle:
http://jsfiddle.net/n1kpkcud/2/
` $scope.countries = initialList;
$scope.selectedAirline = '';
$scope.setItem = function () {
$scope.countries = reloading;
$scope.selectedAirline = 'Yemen';
}`
I would suggest you to update this fiddle so that it replicates the issue and I can suggest you accordingly.