forked from DivadNojnarg/outstanding-shiny-ui
-
Notifications
You must be signed in to change notification settings - Fork 0
/
custom-templates-interactivity.Rmd
668 lines (543 loc) · 25.2 KB
/
custom-templates-interactivity.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
# Adding more interactivity {#custom-templates-interactivity}
In this part, we are going to bring even more life to the template element.
We first see how to enhance an existing static HTML component, through a simple progress bar example.
Then we explore more complex elements involving specific Shiny patterns. By the end of the chapter, you will be able to implement components like the one showed in Figure \@ref(fig:tabler-progress-overview).
```{r tabler-progress-overview, echo=FALSE, fig.cap='Stacked progress bar inside a card.', out.width='100%'}
knitr::include_graphics("images/practice/tabler-progress-overview.png")
```
All the JavaScript handlers described below are gathered in an HTML dependency, as well as an input binding(s):
```{r, eval=FALSE}
# contains bindings and other JS code
tabler_custom_js <- htmlDependency(
name = "tabler-custom-js",
version = "1.0.7",
src = "tabler",
package = "OSUICode",
script = c(
"tabler-update-navbar/navbarMenuBinding.js",
"handlers/tabler_progress_handler.js",
"handlers/tabler_toast_handler.js",
"handlers/tabler_dropdown_handler.js",
"handlers/tabler_insert_tab_handler.js"
)
)
```
## Custom progress bars
__Progress bars__ are a good way to display metrics related to a progress, for instance tracking the number of remaining tasks for a project. In general, those elements are static HTML. Hence, it would be interesting to update the current value from the server side. Since it is not an proper input element, implementing an input binding is inappropriate, and we decide to proceed with a custom handler. We first create the `tabler_progress()` tag, which is mainly composed of:
- `style` gives the current progress value. This is the main element.
- `min` and `max` are bounds, in general between 0 and 100.
- `id` ensures the progress bar uniqueness, thereby avoiding conflicts.
- `aria-...` parameters are [accessibility](https://developer.mozilla.org/fr/docs/Web/Accessibility/ARIA) elements.
```{r, eval=FALSE}
tabler_progress <- function(id = NULL, value) {
div(
class = "progress",
div(
id = id,
class = "progress-bar",
style = paste0("width: ", value, "%"),
role = "progressbar",
`aria-valuenow` = as.character(value),
`aria-valuemin` = "0",
`aria-valuemax` = "100",
span(class = "sr-only", paste0(value,"% complete"))
)
)
}
```
The next element is the `update_tabler_progress()` function, which sends two elements from R to JS.
::: {.noteblock data-latex=""}
How to handle custom messages in Shiny modules? Well, it is pretty straightforward. We wrap any __id__ with the module __namespace__ given by `session$ns()` before sending it to JS. You may even do it by default since `session$ns()` returns `""`.
:::
- The progress __id__ wrapped in `session$ns`.
- The new __value__.
```{r, eval=FALSE}
update_tabler_progress <- function(
id,
value,
session = shiny::getDefaultReactiveDomain()
) {
message <- list(id = session$ns(id), value = value)
session$sendCustomMessage(
type = "update-progress",
message
)
}
```
On the JS side, we leverage the well-known `Shiny.addCustomMessageHandler`. As mentioned in Chapter \@ref(survival-kit-javascript), `sendCustomMessage` and `addCustomMessageHandler` are connected by the __type__ parameter. This is crucial! Moreover, as the sent message is a R list, it becomes an JSON, meaning that elements must be accessed with a `.` in JS:
```{r, echo=FALSE, results='asis'}
js_code <- "$(function () {
Shiny.addCustomMessageHandler(
'update-progress', function(message) {
$('#' + message.id).css('width', message.value +'%');
});
});"
code_chunk_custom(js_code, "js")
```
We finally test these components in a simple app, whose output is depicted in Figure \@ref(fig:tabler-progress):
```{r, echo=FALSE, results='asis'}
code_chunk(OSUICode::get_example("tabler/update-progress"), "r")
```
```{r tabler-progress, echo=FALSE, fig.cap='Progress bar component updated by a slider.', out.width='100%'}
knitr::include_graphics("images/practice/tabler-progress.png")
```
Is there a way to directly update the progress from the client that would avoid to exchange data between R and JS, thereby saving some time?
The idea is to get rid of the classic `session$sendCustomMessage` and `Shiny.addCustomMessageHandler` method. We could directly create a function that inserts a script in the UI taking a __trigger__ and __target__ as main parameters. This function would have to be inserted multiple times if multiple triggers had to update the same target. The JS logic is slightly different:
- We have to wait for Shiny to be connected so that the JS `Shiny` object is ready.
- We recover the trigger element with any JS/jQuery method.
- We leverage the noUiSlider [API](https://refreshless.com/nouislider/) to listen to any update in the range. It's fine because the slider instance has already been initialized in the shinyWidget [input binding](https://github.com/dreamRs/shinyWidgets/blob/55ec7eab9bcf18ceefee2e3714328fd09cfa6e06/inst/assets/nouislider/nouislider-bindings.js#L100). This would not work if we were not waiting for Shiny to be connected (you may try)! Notice the use of `this.get()` in the event listener, which avoids repeating `slider.noUiSlider`
- We modify the `width` CSS property of the target as in the previous example.
```{r, echo=FALSE, results='asis'}
js_code <- "$(document).on('shiny:connected', function(event) {
let slider = document.getElementById('triggerId');
slider.noUiSlider.on('update', function(event) {
$('#targetId').css('width', this.get() + '%');
});
});"
code_chunk_custom(js_code, "js")
```
To insert this script in the app UI, we create `update_tabler_progress2` that just wraps it inside a `tags$script`.
```{r, eval=FALSE}
update_tabler_progress2 <- function(trigger, target) {
tags$script(
paste0(
"$(document).on('shiny:connected', function(event) {
let slider = document.getElementById('", trigger, "');
slider.noUiSlider.on('update', function(event) {
$('#", target, "').css('width', this.get() + '%');
});
});
"
)
)
}
```
```{r, echo=FALSE, results='asis'}
code_chunk(OSUICode::get_example("tabler/update-progress-2"), "r")
```
**Question:** Run the above example in an external web browser, then stop the app from RStudio. Try to move both sliders. What happens for the progress bars? Compare with a classic `update` function. How could you explain this? Answer: as we are not sending a message through the websocket with `session$sendCustomMessage`, the progress bar still updates even though the websocket connection is closed.
Overall this way is a bit more complex. Yet, assuming an app with data manipulation, tons of inputs and visualizations, everything that can be done from the client (web browser) is less work for the R server part and a better end-user experience! Building outstanding Shiny apps is not only designing amazing user interfaces, it's also about __optimization__ and speed as mentioned by Colin Fay et al. in their [book](https://engineering-shiny.org/optimjs.html) [@thinkrShiny].
## User feedback: toasts
__Toasts__ are components to send discrete user feedback, contrary to modals which open in the middle of the page. Toasts may open on all sides of the window and are similar to the Shiny __notifications__ (see [here](https://shiny.rstudio.com/reference/shiny/0.14/showNotification.html)). The Tabler toast component is built on top of [Bootstrap 4](https://getbootstrap.com/docs/4.3/components/toasts/). Therefore, we rely on this documentation.
### Toast skeleton
The skeleton is the HTML structure of the toast:
```{r, echo=FALSE, results='asis'}
html_code <- '<div class="toast show" role="alert" aria-live="assertive"
aria-atomic="true" data-autohide="false" data-toggle="toast">
<div class="toast-header">
<span class="avatar mr-2"
style="background-image: url(...)"></span>
<strong class="mr-auto">Mallory Hulme</strong>
<small>11 mins ago</small>
<button type="button" class="ml-2 close"
data-dismiss="toast" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="toast-body">
Hello, world! This is a toast message.
</div>
</div>'
code_chunk_custom(html_code, "html")
```
Toasts are mainly composed of a header and a body. There might be a close button in case the toast does not hide itself after a period of time. If multiple toasts appear one after each others, they are stacked, the latest being at the bottom of the stack. The position is controlled with the style attribute like `style="position: absolute; top: 0; right: 0;"` for a top-right placement. Accessibility parameters like `aria-live` are detailed [here](https://getbootstrap.com/docs/4.3/components/toasts/#accessibility).
### The toast API
Toasts have a JS API to control their behavior, for instance `$('<toast_selector>').toast(option)`, where option is a __JSON__ with the following fields:
- __animation__ applies a CSS fade transition to the toast and is TRUE by default.
- __autohide__ automatically hides the toast (TRUE by default).
- __delay__ is the delay to hide the toast (500 ms).
There are three methods: __hide__, __show__ and __dispose__ (dispose ensures the toast does not appear anymore). Finally, we may fine-tune the toast behavior with four events: `show.bs.toast`, `shown.bs.toast`, `hide.bs.toast`, `hidden.bs.toast` (like for tabs).
### R implementation
We first create the toast skeleton in a `tabler_toast()` function. We assume our toast will eventually hide automatically, so we may remove the delete button as well as the `data-autohide="false` attribute. All parameters are optional except the toast id, which is required to toggle the toast:
```{r, eval=FALSE}
tabler_toast <- function(id, title = NULL, subtitle = NULL,
..., img = NULL) {
# SEE BELOW
}
```
The first part is the toast header, which is a direct translation of the above HTML structure. Notice how we handle optional parameters with `if (!is.null(<PARAM>))` so that the tag is not included if no value is given:
```{r, eval=FALSE}
toast_header <- div(
class = "toast-header",
if (!is.null(img)) {
span(
class = "avatar mr-2",
style = sprintf("background-image: url(%s)", img)
)
},
if (!is.null(title)) strong(class = "mr-2", title),
if (!is.null(subtitle)) tags$small(subtitle)
)
```
The body is the simplest part, it receives the main content in `...`:
```{r, eval=FALSE}
toast_body <- div(class = "toast-body", ...)
```
The wrapper is the external toast tag that will received both body and header elements, owing to `tagAppendChildren()`:
```{r, eval=FALSE}
toast_wrapper <- div(
id = id,
class = "toast",
role = "alert",
style = "position: absolute; top: 0; right: 0;",
`aria-live` = "assertive",
`aria-atomic` = "true",
`data-toggle` = "toast"
)
tagAppendChildren(toast_wrapper, toast_header, toast_body)
```
The whole code may be found in the `{OSUICode}` package (see https://github.com/DivadNojnarg/outstanding-shiny-ui-code/blob/101248eabda2bd2682c73c2998dbe7d53cb5eb78/R/tabler.R#L820).
We create the `show_tabler_toast()` function. Since the toast automatically hides, it does not make sense to create the hide function, as well as the dispose:
```{r, eval=FALSE}
show_tabler_toast <- function(
id,
options = NULL,
session = getDefaultReactiveDomain()
) {
message <- dropNulls(
list(
id = session$ns(id),
options = options
)
)
session$sendCustomMessage(type = "tabler-toast", message)
}
```
Let's design the corresponding JS handler. We first configure the toast and show it. Notice how we chained jQuery methods (see Chapter \@ref(survival-kit-javascript)). We optionally add an __event listener__ to capture the `hidden.bs.toast` event, so that we may trigger an action when the toast is closed. The `input$id` is used for that purpose in combination with the `Shiny.setInputValue`. Notice the extra parameter `{priority: 'event'}`: basically, once the toast is closed, `input$id` is always `TRUE`, thereby breaking the __reactivity__. Adding this extra parameter __forces__ the evaluation of the input, although constant over time.
```{r, echo=FALSE, results='asis'}
js_code <- "$(function() {
Shiny.addCustomMessageHandler(
'tabler-toast', function(message) {
$(`#${message.id}`)
.toast(message.options)
.toast('show');
// add custom Shiny input to listen to the toast state
$(`#${message.id}`).one('hidden.bs.toast', function() {
Shiny.setInputValue(
message.id,
true,
{priority: 'event'}
);
});
});
});"
code_chunk_custom(js_code, "js")
```
```{r, echo=FALSE, results='asis'}
code_chunk(OSUICode::get_example("tabler/toast"), "r")
```
The result of shown in Figure \@ref(fig:tabler-toast).
```{r tabler-toast, echo=FALSE, fig.cap='Tabler toast element.', out.width='100%'}
knitr::include_graphics("images/practice/tabler-toast.png")
```
### Exercise
Based on the Tabler [documentation](https://preview-dev.tabler.io/docs/tooltips.html), add the `tabler_tooltip()` function. Hint: you may also check the corresponding Bootstrap 4 help.
## Transform an element in a custom action button
As seen in Chapter \@ref(custom-templates-inputs), any `<button>`, `<a>` element holding the `action-button` class may eventually become an action button. The Tabler template has dropdown menus in the navbar, and we would like to transform those dropdown items in action buttons. The `tabler_dropdown()` function takes the following parameters:
- __id__ is required by the `show_tabler_dropdown()` (see below) function, which opens the menu.
- __title__ is the dropdown menu name.
- __subtitle__ is optional text.
- __img__ is an optional image.
- __...__ hosts the `tabler_dropdown_item()` (see below).
```{r, eval=FALSE}
tabler_dropdown <- function(..., id = NULL, title,
subtitle = NULL, img = NULL) {
# SEE BELOW
}
```
We proceed step by step. The image tag is:
```{r, eval=FALSE}
img_tag <- if (!is.null(img)) {
span(
class = "avatar",
style = sprintf("background-image: url(%s)", img)
)
}
```
The title tag accepts both `title` and `subtitle` parameters. It is given by:
```{r, eval=FALSE}
title_tag <- div(
class = "d-none d-xl-block pl-2",
div(title),
if (!is.null(subtitle)) {
div(class = "mt-1 small text-muted", subtitle)
}
)
```
The link tag contains both image tag and title tag:
```{r, eval=FALSE}
link_tag <- tagAppendChildren(
a(
href = "#",
id = id,
class = "nav-link d-flex lh-1 text-reset p-0",
`data-toggle` = "dropdown",
`aria-expanded` = "false"
),
img_tag,
title_tag
)
```
The dropdown tag receives the main content:
```{r, eval=FALSE}
dropdown_tag <- div(
class = "dropdown-menu dropdown-menu-right",
`aria-labelledby` = id,
...
)
```
Both link tag and dropdown tag are wrapped in an external tag:
```{r, eval=FALSE}
tagAppendChildren(
div(class = "nav-item dropdown"),
link_tag,
dropdown_tag
)
```
The whole code is located [here](https://github.com/DivadNojnarg/outstanding-shiny-ui-code/blob/101248eabda2bd2682c73c2998dbe7d53cb5eb78/R/tabler.R#L926).
To convert a dropdown item in an action button, we add the `action-button` class as well as the `id` parameter to recover the corresponding input id.
```{r, eval=FALSE}
tabler_dropdown_item <- function(..., id = NULL) {
a(
id = id,
class = "dropdown-item action-button",
href = "#",
...
)
}
```
We finally create the `show_tabler_dropdown()`, as well as the corresponding Shiny message handler.
```{r, eval=FALSE}
show_tabler_dropdown <- function(
id,
session = getDefaultReactiveDomain()
) {
session$sendCustomMessage(
type = "show-dropdown",
message = session$ns(id)
)
}
```
To show the dropdown, we use the __dropdown__ method, which is linked to the `data-toggle="dropdown"` of `tabler_dropdown()`.
```{r, echo=FALSE, results='asis'}
js_code <- "$(function() {
Shiny.addCustomMessageHandler(
'show-dropdown', function(message) {
$(`#${message}`).dropdown('show');
});
});"
code_chunk_custom(js_code, "js")
```
Let's play with it (Figure \@ref(fig:tabler-dropdown)).
```{r, echo=FALSE, results='asis'}
code_chunk(OSUICode::get_example("tabler/dropdown"), "r")
```
```{r tabler-dropdown, echo=FALSE, fig.cap='Tabler dropdown element.', out.width='100%'}
knitr::include_graphics("images/practice/tabler-dropdown.png")
```
## Tab events
Do you remember about the navbar element and the __tabsetpanel__ system of Chapter \@ref(custom-templates-skeleton)? __Navs__ allow users to organize any app into several tabs, acting like a multi-page application. This is a powerful tool for Shiny since it is currently not straightforward to create [multi-pages](https://community.rstudio.com/t/shiny-app-composed-of-many-many-pages/7698) Shiny apps like anyone would do in a standard website. Navs rely on the Bootstrap 4 API but we only use a few JS functions.
### Insert/remove tabs in tabsetpanel {#insert-tabs}
How about dynamically inserting/removing tabs from a `tabler_navbar()`? We chose this example since it involves extra technical details about Shiny, especially process extra dependencies at render time.
How do we proceed? If you recall about the `tabler_navbar_menu_item()` and `tabler_tab_item()` coupling, inserting a tab implies inserting the trigger in the navigation menu as well as the content in the dashboard body. Therefore, we need to know the structure of what we insert. Below is a reminder:
- `tabler_navbar_menu_item()` are `<li>` elements.
- `tabler_tab_item()` is a `<div>` element with specific classes.
```{r, echo=FALSE, results='asis'}
html_code <- '<li class="nav-item">
<a class="nav-link" href="#ww" data-toggle="pill"
data-value="ww" role="tab">
<span class="nav-link-icon d-md-none d-lg-inline-block">
</span>
<span class="nav-link-title">ww</span>
</a>
</li>
<div role="tabpanel" class="tab-pane fade container-fluid"
id="ww">
</div>'
code_chunk_custom(html_code, "html")
```
We design the `insert_tabler_tab()` function similar to the Shiny `insertTab()`.
```{r, eval=FALSE}
insert_tabler_tab <- function(
inputId,
tab,
target,
position = c("before", "after"),
select = FALSE,
session = getDefaultReactiveDomain()
) {
# SEE BELOW
}
```
To handle shiny modules, we wrap the __inputId__ in the session namespace __session$ns__.
```{r, eval=FALSE}
inputId <- session$ns(inputId)
```
We recover the provided new tab position, leveraging `match.arg()`:
```{r, eval=FALSE}
position <- match.arg(position)
```
We create the menu item element based on the new tab and the above HTML structure:
```{r, eval=FALSE}
navbar_menu_item <- tags$li(
class = "nav-item",
a(
class = "nav-link",
href = "#",
`data-target` = paste0("#", session$ns(tab$attribs$id)),
`data-toggle` = "pill",
`data-value` = tab$attribs$id,
role = "tab",
tab$attribs$id
)
)
```
Since JS does not understand Shiny tags, we have to convert both tab and the nav link to character:
```{r, eval=FALSE}
tab <- as.character(tab)
navbar_menu_item <- as.character(navbar_menu_item)
```
We finally, build our message as a list, whose `NULL` elements are handled by `dropNulls()` and send it to JS with `session$sendCustomMessage`.
```{r, eval=FALSE}
message <- dropNulls(
list(
inputId = inputId,
content = tab,
link = navbar_menu_item,
target = target,
position = position,
select = select
)
)
session$sendCustomMessage(type = "insert-tab-1", message)
```
On the JS side, we capture the R message (list) in two elements:
- `$divTag` contains the tab __content__.
- `$liTag` contains the tab link, i.e. the __navigation__ part.
Depending on the position parameter, we use the `insertAfter()` and `insertBefore()` jQuery methods. Finally, if the newly inserted tab has to be selected, we activate the corresponding tab element with `$(tablink).tab('show')`.
```{r, echo=FALSE, results='asis'}
js_code <- "$(function() {
Shiny.addCustomMessageHandler(
'insert-tab-1', function(message) {
// define div and li targets
let $divTag = $(message.content);
let $liTag = $(message.link);
let targetId = '#' + message.target;
if (message.position === 'after') {
$divTag.insertAfter($(targetId));
$liTag.insertAfter(
$('[data-target=\"' + targetId + '\"]')
.parent()
);
} else if (message.position === 'before') {
$divTag.insertBefore($(targetId));
$liTag.insertBefore(
$('[data-target=\"' + targetId + '\"]')
.parent()
);
}
if (message.select) {
// trigger a click on corresponding the new tab button.
let newTabId = $divTag.attr('id');
$('#' +
message.inputId +
' a[data-target=\"#' +
newTabId +'\"]').tab('show');
}
});
});"
code_chunk_custom(js_code, "js")
```
```{r, echo=FALSE, results='asis'}
code_chunk(OSUICode::get_example("tabler/insert-tab-1", view_code = FALSE), "r")
```
If the tab is well inserted, we notice that the slider and the plot are not properly shown, as illustrated in Figure \@ref(fig:tabler-insert-tab-fail).
```{r tabler-insert-tab-fail, echo=FALSE, fig.cap='The newly inserted tab fails to render its content.', out.width='100%'}
knitr::include_graphics("images/practice/tabler-insert-tab-fail.png")
```
How could we explain that? It is a __dependency__ issue: the slider input relies on a specific JS [library](http://ionden.com/a/plugins/ion.rangeSlider/), namely ionRangesSlider, as depicted in Figure \@ref(fig:slider-deps). In our previous example, if you open the HTML inspector, the dependency is not included in the page.
```{r slider-deps, echo=FALSE, fig.cap='Inspect slider input dependencies.', out.width='100%'}
knitr::include_graphics("images/practice/slider-deps.png")
```
Even stranger, when we use `renderUI()` to conditionally render the slider, the dependency is only included when the go button is pressed. How does Shiny include them?
```{r, echo=FALSE, results='asis'}
code_chunk(OSUICode::get_example("tabler/renderUI"), "r")
```
Let's look at `renderUI()`:
```{r, eval=FALSE}
renderUI <- function (
expr,
env = parent.frame(),
quoted = FALSE,
outputArgs = list()
) {
installExprFunction(expr, "func", env, quoted)
createRenderFunction(func, function(result, shinysession,
name, ...) {
if (is.null(result) || length(result) == 0)
return(NULL)
processDeps(result, shinysession)
}, uiOutput, outputArgs)
}
```
The last line returned is `processDeps(result, shinysession)`. As stated in section \@ref(insert-ui), this function is responsible to handle dependencies during run time. `shiny:::processDeps` (R side) works with `Shiny.renderContent` (JS side), as already mentioned in part 15.3. The latter takes a tag element as well as an object containing its HTML code and dependencies, for instance:
```{r, echo=FALSE, results='asis'}
js_code <- "exports.renderContent($tag[0], {
html: $tag.html(),
deps: message.tag.deps
}); "
code_chunk_custom(js_code, "js")
```
Note that, as it is bad practice to rely on other packages's internal functions, we'll rely on `htmltools::renderTags` instead of `shiny:::processDeps`, being very similar.
In the following, we modify the `insert_tabler_tab()` to include the dependencies processing step. This actually requires changing only three lines of code (the last one consists of pointing to the new JS handler):
```{r, eval=FALSE}
insert_tabler_tab_2 <- function(
inputId,
tab,
target,
position = c("before", "after"),
select = FALSE,
session = getDefaultReactiveDomain()
) {
# ... Unchanged
content <- htmltools::renderTags(tab),
link <- htmltools::renderTags(navbar_menu_item),
session$sendCustomMessage("insert-tab-2", message)
}
```
We then apply the `Shiny.renderContent` method to the tab content and navigation item. Below, we only show what changed compared to the first handler defined earlier:
```{r, echo=FALSE, results='asis'}
js_code <- "$(function() {
Shiny.addCustomMessageHandler('insert-tab-2',
function(message) {
// message.content and message.link are objects
// with 2 fields: html (tag) and deps (dependencies)
// We only need HTML for now.
let $divTag = $(message.content.html);
let $liTag = $(message.link.html);
// Unchanged
// Render input/output in newly added tab.
// It takes the possible deps and add them to the tag.
Shiny.renderContent(
$liTag[0],
{html: $liTag.html(), deps: message.link.dependencies}
);
Shiny.renderContent(
$divTag[0],
{html: $divTag.html(), deps: message.content.dependencies}
);
// Trigger show: unchanged
});
});"
code_chunk_custom(js_code, "js")
```
We check if our approach works as expected.
```{r, echo=FALSE, results='asis'}
code_chunk(OSUICode::get_example("tabler/insert-tab-2", view_code = FALSE), "r")
```
```{r process-deps, echo=FALSE, fig.cap='htmltools::renderTags and Shiny.renderContent in action.', out.width='100%'}
knitr::include_graphics("images/practice/process-deps.png")
```
Et voila! As shown in Figure \@ref(fig:process-deps), everything is properly displayed.
### Exercise
Taking inspiration from the `insert_tabler_tab()` function, write the `remove_tabler_tab()` function.