-
Notifications
You must be signed in to change notification settings - Fork 58
/
workflow-charpente.Rmd
796 lines (651 loc) · 31.1 KB
/
workflow-charpente.Rmd
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
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
# Automate new template creation with {charpente} {#workflow-charpente}
```{r workflow-charpente-hex, echo=FALSE, fig.cap='The {charpente} package.', fig.align = 'center', out.width='15%'}
knitr::include_graphics("images/workflow/charpente-hex.png")
```
## Motivations
Translating an HTML template into an R API requires the creation of a package. This is not a good practice to proceed as follows:
```{r, eval=FALSE}
ui <- fluidPage(
useShinydashboard(),
tags$script(
"$(function() {
// JS logic
});
"
),
# R UI elements
)
server <- function(input, output, session) {
# R server logic
}
shinyApp(ui, server)
```
Imagine if we had to repeat the process for more than twenty components. The R package structure provides many advantages like:
- Develop a comprehensive __documentation__ of the underlying API.
- Design __unit tests__ to guarantee code robustness and improve long term plan.
- Relevant file organization, easing collaboration.
## General idea
[{charpente}](https://github.com/RinteRface/charpente) is a game changer for custom template creation and has been widely used to help develop RinteRface packages.
`{charpente}` [@R-charpente] drastically:
- Eases the __import__ of external dependencies.
- Speeds up the HTML to R __conversion__, which is quite frankly a rather boring process,
allowing the focus to be on the features rather than the syntax. This feature builds on top of the `{html2R}` Shiny app by Alan Dipert, already mentioned in Chapter \@ref(htmltools-overview).
- Eases __JS code management__, leveraging [esbuild](https://esbuild.github.io/).
Let's try below with `html_2_R()`:
```{r}
library(charpente)
html_2_R('<div class="divclass" id = "someid"></div>')
```
`html_2_R` has a __prefix__ parameter, which adds a `tags$` prefix if TRUE. It is
TRUE by default which prevents errors with non exported Shiny tags like `nav`, as discussed in \@ref(htmltools-overview).
The second main benefit of `{charpente}` is the dependency management system. We explain the main principles in the below example.
## A case study: `{shinybulma}`
In the following, we'll illustrate `{charpente}`'s workflow, through a case [study](https://github.com/RinteRface/Unleash-Shiny-Exercise-1) involving [Bulma](https://bulma.io/), a more and more popular open source CSS framework for the web. Importantly, there **isn't** any JavaScript in the Bulma core code.
We'll see later that the recent [bulma JS](https://bulmajs.tomerbe.co.uk/) provides such an implementation. For now, we only focus on HTML and CSS.
To initialize a `{charpente}` package, we run:
```{r, eval = FALSE}
path <- file.path(tempdir(), "mypkg")
create_charpente(path, license = "mit")
```
This sets up a minimal viable package under version control (__git__) and optionally GitHub remote setup, Figure \@ref(fig:workflow-charpente-pkg).
```{r workflow-charpente-pkg, echo=FALSE, fig.cap='Package structure for {charpente}.', fig.align = 'center', out.width='75%'}
knitr::include_graphics("images/workflow/charpente-pkg.png")
```
By default, the package `DESCRIPTION` `Imports` field has `shiny`, `htmltools` and
`utils`. `{charpente}` is never required to be a dependency of your package since it might
be invasive.
In the `./R` folder, `{charpente}` creates a `mypkg-utils.R` script containing:
- Tools to facilitate __HTML dependency__ management like `add_dependencies` (see corresponding section below).
- Some __validation__ functions mentioned in Chapter \@ref(custom-templates-testing).
Finally, you may see some exotic folders and files like `srcjs`, `package.json`, `package-lock.json` and `node_modules`. Fear not, we describe them later in section \@ref(organize-your-js). Overall, they are here to support JS code management.
### Build the HTML dependency
The interested reader may have a look at the **Getting started [guide](https://bulma.io/documentation/overview/start/)**,
so as to know more about how to get Bulma. In short, to install Bulma dependencies, there are several ways:
- The __content delivery network__ method (CDN), which consists of getting dependencies from a dedicated server.
Files are not stored locally, which may be a problem if someone does not have internet.
- The local method consists of downloading the production files (minified CSS).
- Using [npm](https://www.npmjs.com/) (JS package repository but far less controlled than CRAN), which installs Bulma sources as well as production files. It means you can modify
sources at anytime, which is not recommended since it would be hard to
maintain.
In our case, we show the two first methods, the third being out of the scope of this book.
As shown previously in Chapter \@ref(custom-templates-dependencies),
we could build the Bulma dependency as follows:
```{r, eval=FALSE}
library(htmltools)
bulma_deps <- htmlDependency(
name = ...,
version = ...,
src = c(href = ...),
stylesheet = ...
)
add_bulma_deps <- function(tag) {
tagList(..., bulma_deps)
}
```
but this already takes too much time. This is where `{charpente}` comes into play.
Specifically, the `create_dependency()` function automatically points to the specified dependency by just providing its name. This means you have to know what you are looking for. Best practice is to look at the [jsdelivr](https://www.jsdelivr.com/) website (`{charpente}` is built on top of jsdelivr) and find the correct repository, as shown Figure \@ref(fig:workflow-jsdelivr-bulma). `create_dependency()` will also create the `add_<DEP_NAME>_deps` function in a `<DEP_NAME>--dependencies.R` script and open it.
`charpente_options(local = FALSE)` allows you to fine-tune the behavior. If local is FALSE,
`{charpente}` points to the CDN without downloading any files. However, your end users will require
an internet connection to be able to access static assets required by the Shiny app.
Therefore, package developers should prefer the option `local = TRUE` to ensure dependencies are always accessible.
Extra parameters like __tag__ control the downloaded version since HTML templates
may have several flavors. It is always good to be able to test multiple versions and select
the best option.
```{r workflow-jsdelivr-bulma, echo=FALSE, fig.cap='jsdelivr result for Bulma.', fig.align = 'center', out.width='100%'}
knitr::include_graphics("images/workflow/jsdelivr-bulma.png")
```
Once satisfied, we simply run the below code to get the latest version, or a specific
version if __tag__ is used:
```{r, eval=FALSE}
# CDN method
create_dependency(
"bulma",
options = charpente_options(local = FALSE)
)
create_dependency(
"bulma",
tag = "0.7.0",
options = charpente_options(local = FALSE)
)
# local method (default)
create_dependency("bulma")
```
Moreover, `create_dependency()` is able to filter all files, through the `charpente_options()`:
- __minified__ targets all files with `.min`, if TRUE.
- __bundle__ targets all files containing `.bundle`, if TRUE.
- __lite__ targets files with `lite` keyword, if TRUE.
- __rtl__ target all files with `.rtl`, if TRUE. rtl design stands for right to left
and is common in some countries, for instance.
You may imagine that `charpente_options()` targets `.min` files by default. If you don't find
any script, you probably have to change options. For instance, some templates
like Bootstrap and Framework7 have `bundle.min` files (`charpente_options(bunlde = TRUE)`), whereas Bulma doesn't.
We can test our new dependency:
```{r, eval=FALSE}
devtools::load_all()
findDependencies(add_bulma_deps(div()))
```
which works like a charm. If you chose the local option, you also get an `inst/bulma-<BULMA-VERSION>` folder
with all relevant files sorted by type. The `bulma-dependencies.R` script contains the
newly created `add_bulma_deps` function, either pointing to the CDN or the local
files, depending on the chosen strategy:
```{r, eval=FALSE}
# local dependency script output
#' bulma dependencies utils
#'
#' @description This function attaches bulma dependencies
#' to the given tag
#'
#' @param tag Element to attach the dependencies.
#'
#' @importFrom htmltools tagList htmlDependency
#' @export
add_bulma_deps <- function(tag) {
bulma_deps <- htmlDependency(
name = "bulma",
version = "0.9.3",
src = c(file = "bulma-0.9.3"),
stylesheet = "css/bulma.min.css",
package = "mypkg",
)
tagList(tag, bulma_deps)
}
```
```{r, eval=FALSE}
# CDN dependencies
#' bulma dependencies utils
#'
#' @description This function attaches bulma
#' dependencies to the given tag
#'
#' @param tag Element to attach the dependencies.
#'
#' @importFrom htmltools tagList htmlDependency
#' @export
add_bulma_deps <- function(tag) {
bulma_deps <- htmlDependency(
name = "bulma",
version = "0.9.3",
src = c(href = "https://cdn.jsdelivr.net/npm/bulma@0.9.3/"),
stylesheet = "css/bulma.min.css"
)
tagList(tag, bulma_deps)
}
```
`{charpente}` sets the roxygen skeleton so that you don't have to worry about function imports.
### Set up the minimal page template
According to the Bulma [documentation](https://bulma.io/documentation/overview/start/#starter-template), the starter page template is:
```{r, echo=FALSE, results='asis'}
html_code <- '<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,
initial-scale=1">
<title>Hello Bulma!</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/
/bulma.min.css">
</head>
<body>
<section class="section">
<div class="container">
<h1 class="title">
Hello World
</h1>
<p class="subtitle">
My first website with <strong>Bulma</strong>!
</p>
</div>
</section>
</body>
</html>'
code_chunk_custom(html_code, "html")
```
Adding some `{charpente}` magic with `html_2_R()`, we set the path parameter to `/html` to get the entire template. We, replace `...` with the appropriate content (see above). Since the copied HTML contains double quotations marks like `<p class="subtitle"></p>`, we put the string in single quotation marks.
```{r, echo=FALSE}
bulma_html_skeleton <- '
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Hello Bulma!</title>
</head>
<body>
<section class="section">
<div class="container">
<h1 class="title">
Hello World
</h1>
<p class="subtitle">
My first website with <strong>Bulma</strong>!
</p>
</div>
</section>
</body>
</html>'
```
```{r}
# bulma_html_skeleton is the above HTML code
html_2_R(bulma_html_skeleton, path = "/html")
```
::: {.noteblock data-latex=""}
At run time, Shiny adds `html` around the UI, thereby making it not necessary to include.
We don't need the `link(rel = "stylesheet", href = "https://cdn.jsdelivr.net/npm/bulma@0.9.3/css/bulma.min.css")`
since `add_bulma_deps` does already attach the dependencies to the page. We removed `tags$section` content, as it may correspond to another Bulma component, like `bulma_section()`.
:::
The `prefix` parameter defaults to TRUE, so that we don't have to worry about whether tags functions are exported
by Shiny (see Chapter \@ref(htmltools-overview)).
The `bulma_page()` function is defined below, considering a simplified version of the above HTML to R translation, which we save in the `./R` package folder:
```{r}
bulma_page <- function(..., title = NULL) {
tagList(
tags$head(
tags$meta(charset = "utf-8"),
tags$meta(
name = "viewport",
content = "width=device-width, initial-scale=1"
),
tags$title(title)
),
add_bulma_deps(tags$body(...))
)
}
```
With some practice, going from step one to the Bulma page templates literally takes three minutes,
while it would have taken much more time by hand. At any time, you may replace
the dependency with another version. Be careful, since `{charpente}` does not make snapshots of old versions.
### Exercise: add bulmaJS
As stated in the above, Bulma only contains CSS code. It means we need
to either develop custom JS code to add interactivity or rely on any third-party existing API. [bulma JS](https://bulmajs.tomerbe.co.uk/) is one of these.
::: {.importantblock data-latex=""}
This example has been written and tested with bulmaJS (https://www.jsdelivr.com/package/npm/@vizuaalog/bulmajs?version=0.12.1) `0.12.1`.
Some features may change in future releases.
:::
1. Using `{charpente}`, create a Bulma js dependency. We point to [vizuaalog/bulmajs](https://bulmajs.tomerbe.co.uk/) since some bulmajs already exist and is not what we want. Run the following code in the R console.
```{r bulmajs-structure, eval=FALSE}
get_dependency_assets("@vizuaalog/bulmajs")
```
```{r, echo=FALSE}
if (knitr::is_html_output()) {
get_dependency_assets("@vizuaalog/bulmajs")
}
```
```{r, echo=FALSE, results='asis'}
code <- 'ℹ Trying with https://data.jsdelivr.com/v1/package/
npm/@vizuaalog/bulmajs
✓ Success!
────────────────────────────────────────────────────────
$url
[1] "https://cdn.jsdelivr.net/npm/@vizuaalog/
bulmajs@0.12.1/dist/"
$files
name hash
1 alert.js 0Tq89d1U9WqE...=
2 bulma.js vbERfMn7TdJ3...=
3 dropdown.js a4jYH26F8++6...=
4 file.js auA7tFsecFic...=
5 message.js xIxQKW6ezuEu...=
6 modal.js hBvcaTjLTgUE...=
7 navbar.js kZ1bvDA2eaAt...=
8 notification.js DLFq8emqUPpF...=
9 panel.js NSnEBEPnog3o...=
10 panelTabs.js YYhbFo+xIExS...=
11 tabs.js /DBuPkvCsQJ6...=
$hasSubfolders
[1] FALSE'
exclude_from_html(code)
```
As shown in the code output, bulmajs does not have minified JS and CSS files. Therefore, to install the dependency, we have to specify the `charpente_options(minified = FALSE)`:
```{r bulmajs-deps, eval=FALSE}
opts <- charpente_options(minified = FALSE)
create_dependency(..., options = opts)
```
Notice how many files are added to the dependency. Below, we only work with **notifications**:
- Only keep `notification.js` and remove all the unnecessary files
- Only keep `bulma.js` that gather all plugins in one script.
The best practice is usually to **keep only what we need** since some scripts may be **heavy to load**.
2. Run `devtools::load_all()`. Modify the below code to test the newly created dependency. Hint: toggle the HTML inspector to check all appropriate dependencies are there.
```{r bulmajs-deps-test, eval=FALSE}
# Where should the html dependency be added?
ui <- bulma_page()
server <- function(input, output, session) {}
shinyApp(ui, server)
```
### Add custom JS
Notifications are always useful to send user feedback. Shiny has a notification system through `shiny::showNotification`. Like Shiny, Bulma [notifications](https://bulmajs.tomerbe.co.uk/docs/0.11/2-core-components/notification/) are entirely built from JS (no need for any HTML code).
The API works as follows:
- `Bulma(target).notification(config)` creates the notification based on a JSON option list (config).
`target` expects a jQuery selector.
- `show` toggles the newly instantiated notification
In other words the following code attaches the notification to the `body`:
```{r, echo=FALSE, results='asis'}
js_code <- "Bulma('body').notification({
body: 'Example notification',
color: 'info'
}).show();"
code_chunk_custom(js_code, "js")
```
In the following, we design the R interface and JS handler (which is no more than an event listener).
`{charpente}` has a function that creates both pieces, namely `create_custom_handler()`:
```{r, eval=FALSE}
create_custom_handler("notification")
```
We obtain the `notification-handler.R` script:
```{r, eval=FALSE}
send_notification_message <- function(
id = NULL,
options = NULL,
session = shiny::getDefaultReactiveDomain()
) {
message <- list(
# your logic
)
session$sendCustomMessage(type = "notification", message)
}
```
and the corresponding JavaScript piece in `notification.js`,
derived from the `golem::add_js_handler` function:
```{r, echo=FALSE, results='asis'}
js_code <- "$(function() {
Shiny.addCustomMessageHandler(
'notification', function(message) {
});
});"
code_chunk_custom(js_code, "js")
```
By default, the JS file is created in the `./srcjs` directory. This is a special directory where we store all JavaScript files that depend on the package author. For instance, bulmaJS is an external dependency and is very unlikely to be edited by the package author. For that reason, it remains in the `./inst` folder like all other external dependencies.
### Add custom input/output bindings
In section \@ref(secondary-inputs), we created better `{shinydashboard}` boxes that one
may programmatically collapse, close, and restore. Until now, there was no way to
setup an input binding skeleton and you had to copy and paste the same code each time. `{charpente}` has a `create_input_binding()` and `create_output_binding()` (functions that
you can also find in the development version of `{golem}`). Contrary to the custom handler case,
`create_input_binding()` only generates the JavaScript piece since the R part is highly variable from
one input to another. To get a plug-and-play box input binding, we call:
```{r, eval=FALSE}
create_input_binding("boxBinding")
```
which gives the `input-boxBinding.js` script in the `./srcjs` folder:
```{r, echo=FALSE, results='asis'}
js_code <- "var boxBinding = new Shiny.InputBinding();
$.extend(boxBinding, {
find: function(scope) {
// JS logic $(scope).find('whatever')
},
getValue: function(el) {
// JS code to get value
},
setValue: function(el, value) {
// JS code to set value
},
receiveMessage: function(el, data) {
// this.setValue(el, data);
},
subscribe: function(el, callback) {
$(el).on('click.boxBinding', function(e) {
callback();
});
},
unsubscribe: function(el) {
$(el).off('.boxBinding');
}
});
Shiny.inputBindings.register(boxBinding, 'shiny.whatever');"
code_chunk_custom(js_code, "js")
```
This function has multiple options:
- __initialized__ is FALSE by default. If TRUE, it adds an `initialized` method
to the binding.
- __dev__ adds some `console.log` elements whenever relevant to help in the debugging
process.
- __event__ is a list containing events related to the binding. By default, it generates
a `click` event without any rate policy. To add extra events we do
`list(name = c("click", "whatever"), rate_policy = c(FALSE, TRUE))`.
Similarly, the `create_output_binding()` function creates a ready to use output binding JS script,
in the `./srcjs` folder (`create_output_binding("menuOutput")`):
```{r, echo=FALSE, results='asis'}
js_code <- "var menuOutput = new Shiny.OutputBinding();
$.extend(menuOutput, {
find: function(scope) {
// JS logic $(scope).find('whatever')
},
renderValue: function(el, data) {
// JS logic
}
});
Shiny.outputBindings.register(menuOutput, 'shiny.whatever');"
code_chunk_custom(js_code, "js")
```
### Organize your JS code {#organize-your-js}
This naturally leads us to this part, which is about JS code organization. Shiny developers
may have a lot of custom JS scripts, and it is generally a bad idea to put them all
under `./inst`. Instead, we store them in `./srcjs`, as already stated above. `{charpente}` has a function providing
a tool to bundle the JS code for production or development, that is `build_js()`:
- It compresses, mangles all JS files and concatenate them in one minified file called `mypkg.min.js`. If __mode__ is `dev`, the files are not minified.
- In production mode (__mode__ is `prod`, by default), it additionally generates source maps.
::: {.noteblock data-latex=""}
esbuild concatenates files by the order provided in the `./srcjs/main.js` entry point, automatically generated by `create_charpente()`. The configuration is provided by `{charpente}` in the `package.json` file.
:::
The script `mypkg.min.js` is not human readable but the generated source map
allows us to reconstruct the original code, whose location is under the web browser `./srcjs` folder, like all Shiny JS files. From there, we can access any mapped script and start the debugging process like setting break points.
::: {.noteblock data-latex=""}
In production, the variable names, functions, are mangled. For instance, a variable
`config` could be called `t` in the minified file.
:::
Additionally, `build_js()` creates the `mypkg-dependencies.R` file containing the HTML
dependency pointing to the newly generated JS file (below for production):
```{r, eval=FALSE}
#' mypkg dependencies utils
#'
#' @description This function attaches mypkg dependencies
#' to the given tag
#'
#' @param tag Element to attach the dependencies.
#'
#' @importFrom utils packageVersion
#' @importFrom htmltools tagList htmlDependency
#' @export
add_mypkg_deps <- function(tag) {
mypkg_deps <- htmlDependency(
name = "mypkg",
version = packageVersion("mypkg"),
src = c(file = "mypkg-0.0.0.9000"),
script = "js/mypkg.min.js",
package = "mypkg",
)
tagList(tag, mypkg_deps)
}
```
Switching between `prod` and `dev` automatically updates the `mypkg-dependencies.R` JS files.
Finally, under the hood, `create_js()`, `create_input_binding()`, `create_output_binding()` and `create_custom_handler()` add a reference to the newly created script in the `main.js` entry point, which may look like:
```{r, echo=FALSE, results='asis'}
js_code <- "// Gather all files to import here
import './init.js'
import './widgets.js'
import './test.js'"
code_chunk_custom(js_code, "js")
```
A real-life example will be shown in the section \@ref(mobile-modularizeJS) and the result showcased [here](https://github.com/DivadNojnarg/outstanding-shiny-ui-code/tree/fd0ba28881869c5c605dd0e5b9f7a8f420bbedb4/srcjs).
__export__ and __import__ must be called from the top level of a script. For instance,
they cannot live inside the `$( document ).ready(function(...)});`, as this would trigger a build error.
`{charpente}` currently does not provide a similar process for CSS, as this is still work in progress. Other tools exist like [`{packer}`](https://github.com/JohnCoene/packer) by [John Coene](https://john-coene.com/), which leverages [webpack](https://webpack.js.org/) to handle JS code.
### Combine multiple dependencies
`add_dependencies()` allows you to select any dependencies available in the `./R` folder,
provided that they follow the convention `<depName>_dependencies.R`
(which is always the case if you use `{charpente}`).
For instance `add_dependencies(div(), deps = c("bulma", "bulmajs"))` adds Bulma (first) and
bulmajs dependencies to a `div` tag. You may change the order as you see fit, as most
of the time, the order matters. We update `bulma_page()` to benefit from the new dependencies:
```{r, eval=FALSE}
bulma_page <- function(..., title = NULL) {
tagList(
tags$head(
tags$meta(charset = "utf-8"),
tags$meta(
name = "viewport",
content = "width=device-width, initial-scale=1"
),
tags$title(title)
),
add_dependencies(
tags$body(...),
deps = c("bulma", "mypkg")
)
)
}
```
As mentioned above, `add_dependencies()` belongs to the `mypkg-utils.R` script
so that you don't have to import `{charpente}` in the `DESCRIPTION` `Imports` field.
### Other `{charpente}` helpers
Let's finish this section by listing other useful `{charpente}` tools.
We know `create_dependency()` to install an external dependency.
As shown earlier, this code installs Bulma dependencies:
```{r, eval=FALSE}
create_dependency("bulma")
```
However, we don't necessarily know all package versions and may need
`bulma 0.9.3` or `bulma 0.7.0`. `get_dependency_versions()` allows to look for
all existing versions:
```{r}
get_dependency_versions("bulma")
get_dependency_versions("bulma", latest = TRUE)
```
Specifying `latest = TRUE` ensures recovering the very last __stable__ version
(it excludes alpha/beta versions).
You may explore also the dependency files with `get_dependency_assets()`, even for
a specific version with __tag__:
```{r}
get_dependency_assets("bulma")
```
This is helpful to further fine-tune `charpente_options()`, as stated previously. It is indeed possible
that you don't want bundles, minified, lite or rtl versions of scripts. Internally, `create_dependency()`
relies on `get_dependency_assets()`.
`get_installed_dependency()` allows you to __inspect__ which dependencies are installed. It only works
if the dependencies were created locally, that is `charpente_options(local = TRUE)`.
Finally, we may ask how to __update__ a given dependency. `update_dependency()` does this, provided that
the dependency is installed locally. By default, it installs the latest version of the targeted dependency.
It gives a diagnosis comparing the current installed version
with the latest available version, yielding three possible cases. Dependencies are __up to date__
and `update_dependency("bulma")` yields:
```{r, echo=FALSE, results='asis'}
tmp_code <- '#> ℹ Trying https://data.jsdelivr.com/v1/package/npm/bulma
#> ✓ Success!
#> ───────────────────────────────────────────────────────
#> Error in update_dependency("bulma") : Versions are identical'
code_chunk_custom(tmp_code)
```
The installed dependencies are __outdated__ (we have 0.7.0 with `create_dependency("bulma", tag = "0.7.0", options = charpente_options(local = TRUE))`), the function shows the targeted version as well as
the last one:
```{r, echo=FALSE, results='asis'}
tmp_code <- '#> ℹ Trying https://data.jsdelivr.com/v1/package/npm/bulma
#> ✓ Success!
#> ───────────────────────────────────────────────────────
#> ℹ current version: 0.7.0 ||
#> target version: 0.9.3 ||
#> latest version: 0.9.3
#> ! Upgrading bulma to 0.9.3
#> ✓ Directory inst/bulma-0.9.3/css successfully created
#> ! Remove existing file R/bulma-dependencies.R'
code_chunk_custom(tmp_code)
```
The last use case is a __downgrade__, which may be possible if the package maintainer realizes
that the dependency version is too unstable. In the following, we have `bulma-0.9.3` installed
and downgrade to 0.7.0 with `update_dependency("bulma", version_target = "0.7.0")`:
```{r, echo=FALSE, results='asis'}
tmp_code <- '#> ℹ Trying https://data.jsdelivr.com/v1/package/npm/bulma
#> ✓ Success!
#> ───────────────────────────────────────────────────────
#> ℹ current version: 0.9.3 ||
#> target version: 0.7.0 ||
#> latest version: 0.9.3
#> ! Downgrading bulma to 0.7.0
#> ✓ Directory inst/bulma-0.7.0/css successfully created
#> ! Remove existing file R/bulma-dependencies.R'
code_chunk_custom(tmp_code)
```
## Other tips
### Validate JavaScript
We could not finish this chapter without mentioning tools to __validate__ JavaScript code. [JSHint](https://jshint.com), which comes with [{jstools}](https://github.com/dreamRs/jstools). Below is an example of how to check all the [shinyMobile](https://github.com/RinteRface/shinyMobile) JavaScript input bindings at once:
```{r, eval=FALSE}
shinyMobileJS <- system.file(
sprintf(
"shinyMobile-%s/js/shinyMobile.js",
packageVersion("shinyMobile")
),
package = "shinyMobile"
)
# jstools print messages to the console
# We don't want to see them all in the book ...
invisible(capture.output(
temp <- jshint_file(
input = shinyMobileJS,
options = jshint_options(
jquery = TRUE,
globals = list("Shiny", "app")
)
)
))
head(tibble::as_tibble(temp$errors[, c("line", "reason")]))
#> A tibble: 51 x 2
#> line reason
#> <int> <chr>
#> 1 33 'template literal syntax' is only available in ES6.
#> 2 37 'template literal syntax' is only available in ES6.
#> 3 41 'template literal syntax' is only available in ES6.
#> 4 42 'template literal syntax' is only available in ES6.
#> 5 52 'arrow function syntax (=>)' is only available in ES6.
#> 6 64 'arrow function syntax (=>)' is only available in ES6.
```
You may fine-tune the `jshint_file` behavior with the [`jshint_options`](https://jshint.com/docs/options/). You will be often tempted to call `eval` in JS code, which will result in a JSHint error. An option called `evil` exists to disable the corresponding test. However, we recommend playing the game, accept those errors and try to fix them instead of cheating with options! An important remark about validation is that it does not check whether your code
does what it should do. It just focuses on checking whether the code could probably execute. To test
the JavaScript logic, please refer to section \@ref(custom-templates-testing-js).
Be extremely careful: if we consider the example mentioned in section \@ref(broken-shiny-app-debug), the
following code is valid JavaScript:
```{r, echo=FALSE, results='asis'}
js_code <- "const sendNotif = (message, type, duration) => {
Shiny.notification.show({
html: `<strong>${message}</strong>`,
type: type,
duration: duration
});
};
sendNotif('Hello')"
code_chunk_custom(js_code, "js")
```
and will pass the validation step without any error:
```{r}
jshint(
"const sendNotif = (message, type, duration) => {
Shiny.notification.show({
html: `<strong>${message}</strong>`,
type: type,
duration: duration
});
};
sendNotif('Hello');
",
options = jshint_options(
esversion = 6,
jquery = TRUE,
globals = list("Shiny", "app")
)
)$errors
```
Yet the code won't work since `Shiny.notification` does not exist.
### Test JS code
`{charpente}` provides a starting point to __test__ the JS code with `test_js()`, by leveraging the [mocha](https://mochajs.org/) library. All tests are assumed to be contained within the `srcjs/test` folder. Inside, we find `test_basic.js`, which was created upon package initialization, as a boilerplate:
```{r, echo=FALSE, results='asis'}
js_code <- "describe('Basic test', () => {
it('should not fail', (done) => {
done();
});
});"
code_chunk_custom(js_code, "js")
```
This test starts with a `describe` function, similar to the `{testthat}` `context()` function, where you provide the general idea behind the test. `it` is equivalent to `test_that()`, where we describe what specific feature is being tested. Inside, we write a series of instructions, some of them failing, others passing. Naturally, `mocha` works better with other assertions libraries like [expect.js](https://github.com/Automattic/expect.js) or [should.js](http://shouldjs.github.io/), whose details are out of the scope of this book.
### Beautify JS code
If you work with the RStudio IDE, your JS code may sometimes be messy with bad indentation.
`{jstools}` also provides a function and addin to fix the problem. `prettier_js(code)` and
`prettier_file(input = "path/to/file.js", output = "path/to/reformated.js")` does this.
I often use the `Prettier` addin, which is way faster than typing the function call (Figure \@ref(fig:workflow-pretty-js)).
```{r workflow-pretty-js, echo=FALSE, fig.cap='Better JS formatting.', fig.align = 'center', out.width='50%'}
knitr::include_graphics("images/workflow/pretty-js.png")
```