-
-
Notifications
You must be signed in to change notification settings - Fork 118
/
Copy pathextendShinyjs.R
331 lines (321 loc) · 12.6 KB
/
extendShinyjs.R
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
#' Extend shinyjs by calling your own JavaScript functions
#'
#' Add your own JavaScript functions that can be called from R as if they were
#' regular R functions. This is a more advanced technique and can only
#' be used if you know JavaScript. See 'Basic Usage' below for more information
#' or \href{http://deanattali.com/shinyjs}{view the shinyjs webpage}
#' to learn more.
#'
#' @param script Path to a JavaScript file that contains all the functions.
#' Each function name must begin with `shinyjs.`, for example
#' `shinyjs.myfunc`. See 'Basic Usage' below.
#' @param text Inline JavaScript code to use. If your JavaScript function is very
#' short and you don't want to create a separate file for it, you can provide the
#' code as a string. See 'Basic Usage' below.
#' @param functions The names of the shinyjs JavaScript functions which you defined and
#' want to be able to call using \code{shinyjs}. Only use this argument if you cannot
#' install \code{V8} on your machine. I repeat: do not use this argument if you're
#' able to install \code{V8} on your machine. For example, if you defined JavaScript functions
#' named \code{shinyjs.foo} and \code{shinyjs.bar}, then use \code{functions = c("foo", "bar")}.
#'
#' @section Basic Usage:
#' Any JavaScript function defined in your script that begins with `shinyjs.`
#' will be available to run from R through the `js$` variable. For example,
#' if you write a JavaScript function called `shinyjs.myfunc`, then you can
#' call it in R with `js$myfunc()`.
#'
#' It's recommended to write JavaScript code in a separate file and provide the
#' filename as the \code{script} argument, but it's also possible to use the
#' \code{text} argument to provide a string containing valid JavaScript code. Using the
#' \code{text} argument is meant to be used when your JavaScript code is very short
#' and simple.
#'
#' As a simple example, here is a basic example of using \code{extendShinyjs}
#' to define a function that changes the colour of the page.
#'
#' \preformatted{
#' library(shiny)
#' library(shinyjs)
#'
#' jsCode <- "shinyjs.pageCol = function(params){$('body').css('background', params);}"
#'
#' shinyApp(
#' ui = fluidPage(
#' useShinyjs(),
#' extendShinyjs(text = jsCode),
#' selectInput("col", "Colour:",
#' c("white", "yellow", "red", "blue", "purple"))
#' ),
#' server = function(input, output) {
#' observeEvent(input$col, {
#' js$pageCol(input$col)
#' })
#' }
#' )
#' }
#'
#' As the example above shows, after defining the JavaScript function
#' \code{shinyjs.pageCol} and passing it to \code{extendShinyjs}, it's possible
#' to call \code{js$pageCol()}.
#'
#' You can add more functions to the JavaScript code, but remember that every
#' function you want to use in R has to have a name beginning with
#' `shinyjs.`. See the section on passing arguments and the examples below
#' for more information on how to write effective functions.
#'
#' @section Running JavaScript code on page load:
#' If there is any JavaScript code that you want to run immediately when the page loads
#' rather than having to call it from the server, you can place it inside a
#' \code{shinyjs.init} function. The function \code{shinyjs.init}
#' will automatically be called when the Shiny app's HTML is initialized. A common
#' use for this is when registering event handlers or initializing JavaScript objects,
#' as these usually just need to run once when the page loads.
#'
#' For example, the following example uses \code{shinyjs.init} to register an event
#' handler so that every keypress will print its corresponding key code:
#'
#' \preformatted{
#' jscode <- "
#' shinyjs.init = function() {
#' $(document).keypress(function(e) { alert('Key pressed: ' + e.which); });
#' }"
#' shinyApp(
#' ui = fluidPage(
#' useShinyjs(),
#' extendShinyjs(text = jscode),
#' "Press any key"
#' ),
#' server = function(input, output) {}
#' )
#' }
#'
#' @section Passing arguments from R to JavaScript:
#' Any \code{shinyjs} function that is called will pass a single array-like
#' parameter to its corresponding JavaScript function. If the function in R was
#' called with unnamed arguments, then it will pass an Array of the arguments;
#' if the R arguments are named then it will pass an Object with key-value pairs.
#'
#' For example, calling \code{js$foo("bar", 5)} in R will call \code{shinyjs.foo(["bar", 5])}
#' in JS, while calling \code{js$foo(num = 5, id = "bar")} in R will call
#' \code{shinyjs.foo({num : 5, id : "bar"})} in JS. This means that the
#' \code{shinyjs.foo} function needs to be able to deal with both types of
#' parameters.
#'
#' To assist in normalizing the parameters, \code{shinyjs} provides a
#' \code{shinyjs.getParams()} function which serves two purposes. First of all,
#' it ensures that all arguments are named (even if the R function was called
#' without names). Secondly, it allows you to define default values for arguments.
#'
#' Here is an example of a JS function that changes the background colour of an
#' element and uses \code{shinyjs.getParams()}.
#'
#' \preformatted{
#' shinyjs.backgroundCol = function(params) {
#' var defaultParams = {
#' id : null,
#' col : "red"
#' };
#' params = shinyjs.getParams(params, defaultParams);
#'
#' var el = $("#" + params.id);
#' el.css("background-color", params.col);
#' }
#' }
#'
#' Note the \code{defaultParams} object that was defined and the call to
#' \code{shinyjs.getParams}. It ensures that calling \code{js$backgroundCol("test", "blue")}
#' and \code{js$backgroundCol(id = "test", col = "blue")} and
#' \code{js$backgroundCol(col = "blue", id = "test")} are all equivalent, and
#' that if the colour parameter is not provided then "red" will be the default.
#'
#' All the functions provided in \code{shinyjs} make use of \code{shinyjs.getParams},
#' and it is highly recommended to always use it in your functions as well.
#' Notice that the order of the arguments in \code{defaultParams} in the
#' JavaScript function matches the order of the arguments when calling the
#' function in R with unnamed arguments.
#'
#' See the examples below for a shiny app that uses this JS function.
#' @return Scripts that \code{shinyjs} requires in order to run your JavaScript
#' functions as if they were R code.
#' @note You still need to call \code{useShinyjs()} as usual, and the call to
#' \code{useShinyjs()} must come before the call to \code{extendShinyjs()}.
#' @note The \code{V8} package is strongly recommended if you use this function.
#' @note If you are deploying your app to shinyapps.io and are using \code{extendShinyjs()},
#' then you need to let shinyapps.io know that the \code{V8} package is required.
#' The easiest way to do this is by simply including \code{library(V8)} somewhere.
#' This is an issue with shinyapps.io that might be resolved by them in the future --
#' see \href{https://github.com/daattali/shinyjs/issues/20}{here} for more details.
#' @seealso \code{\link[shinyjs]{runExample}}
#' @examples
#' \dontrun{
#' Example 1:
#' Change the page background to a certain colour when a button is clicked.
#'
#' jsCode <- "shinyjs.pageCol = function(params){$('body').css('background', params);}"
#'
#' shinyApp(
#' ui = fluidPage(
#' useShinyjs(),
#' extendShinyjs(text = jsCode),
#' selectInput("col", "Colour:",
#' c("white", "yellow", "red", "blue", "purple"))
#' ),
#' server = function(input, output) {
#' observeEvent(input$col, {
#' js$pageCol(input$col)
#' })
#' }
#' )
#'
#' # If you do not have `V8` package installed, you will need to add another
#' # argument to the `extendShinyjs()` function:
#' # extendShinyjs(text = jsCode, functions = c("pageCol"))
#'
#' ==============
#'
#' Example 2:
#' Change the background colour of an element, using "red" as default
#'
#' jsCode <- '
#' shinyjs.backgroundCol = function(params) {
#' var defaultParams = {
#' id : null,
#' col : "red"
#' };
#' params = shinyjs.getParams(params, defaultParams);
#'
#' var el = $("#" + params.id);
#' el.css("background-color", params.col);
#' }'
#'
#' shinyApp(
#' ui = fluidPage(
#' useShinyjs(),
#' extendShinyjs(text = jsCode),
#' p(id = "name", "My name is Dean"),
#' p(id = "sport", "I like soccer"),
#' selectInput("col", "Colour:",
#' c("white", "yellow", "red", "blue", "purple")),
#' textInput("selector", "Element", "sport"),
#' actionButton("btn", "Go")
#' ),
#' server = function(input, output) {
#' observeEvent(input$btn, {
#' js$backgroundCol(input$selector, input$col)
#' })
#' }
#' )
#'
#' ==============
#'
#' Example 3:
#' Create an `increment` function that increments the number inside an HTML
#' tag (increment by 1 by default, with an optional parameter). Use a separate
#' file instead of providing the JS code in a string.
#'
#' Create a JavaScript file "myfuncs.js":
#' shinyjs.increment = function(params) {
#' var defaultParams = {
#' id : null,
#' num : 1
#' };
#' params = shinyjs.getParams(params, defaultParams);
#'
#' var el = $("#" + params.id);
#' el.text(parseInt(el.text()) + params.num);
#' }
#'
#' And a shiny app that uses the custom function we just defined. Note how
#' the arguments can be either passed as named or unnamed, and how default
#' values are set if no value is given to a parameter.
#'
#' library(shiny)
#' shinyApp(
#' ui = fluidPage(
#' useShinyjs(),
#' extendShinyjs("myfuncs.js"),
#' p(id = "number", 0),
#' actionButton("add", "js$increment('number')"),
#' actionButton("add5", "js$increment('number', 5)"),
#' actionButton("add10", "js$increment(num = 10, id = 'number')")
#' ),
#' server = function(input, output) {
#' observeEvent(input$add, {
#' js$increment('number')
#' })
#' observeEvent(input$add5, {
#' js$increment('number', 5)
#' })
#' observeEvent(input$add10, {
#' js$increment(num = 10, id = 'number')
#' })
#' }
#' )
#' }
#' @export
extendShinyjs <- function(script, text, functions) {
if (missing(script) && missing(text)) {
errMsg("Either `script` or `text` need to be provided.")
}
# if V8 is not installed, the user must provide the JS function names
if (!requireNamespace("V8", quietly = TRUE)) {
if (missing(functions)) {
errMsg(paste0("In order to use the `extendShinyjs()` function, you must either ",
"use the `functions` argument, or install the `V8` package ",
"with `install.packages(\"V8\")`."))
}
jsFuncs <- functions
}
# if V8 is installed (preferable method), parse the input for JS functions
else {
# create a js context with a `shinyjs` object that user-defined functions
# can populate
ct <- V8::new_context(NULL, FALSE, FALSE)
ct$assign("shinyjs", c())
# read functions from a script
if (!missing(script)) {
if (!file.exists(script)) {
errMsg(sprintf("Could not find JavaScript file `%s`.", script))
}
tryCatch({
ct$source(script)
}, error = function(err) {
errMsg(sprintf("Error parsing the JavaScript file: %s.", err$message))
})
}
# read functions from in-line text
if (!missing(text)) {
tryCatch({
ct$eval(text)
}, error = function(err) {
errMsg(sprintf("Error parsing the JavaScript code provided.", err$message))
})
}
# find out what functions the user defined
jsFuncs <- ct$get(V8::JS("Object.keys(shinyjs)"))
if (length(jsFuncs) == 0) {
errMsg(paste0("Could not find any shinyjs functions in the JavaScript file. ",
"Did you remember to prepend every function's name with `shinyjs.`?"))
}
}
# add all the given functions to the shinyjs namespace so that they can be
# called as if they were regular shinyjs functions
lapply(jsFuncs, function(x) {
assign(x, jsFunc, js)
})
# Add the script as a resource
if (!missing(script)) {
if (!file.exists(script)) {
errMsg(sprintf("Could not find JavaScript file `%s`.", script))
}
shiny::addResourcePath("shinyjs-extend", dirname(script))
script <- file.path("shinyjs-extend", basename(script))
}
# set up the message handlers for all functions
setupJS(jsFuncs, script, text)
}
#' Call user-defined JavaScript functions from R
#' @seealso \code{\link[shinyjs]{extendShinyjs}}
#' @export
#' @keywords internal
js <- new.env()