forked from DivadNojnarg/outstanding-shiny-ui
-
Notifications
You must be signed in to change notification settings - Fork 0
/
htmltools-overview.Rmd
1520 lines (1224 loc) · 47.9 KB
/
htmltools-overview.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
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# Manipulate HTML tags from R with {htmltools} {#htmltools-overview}
`{htmltools}` [@R-htmltools] is a R package designed to:
- Generate HTML __tags__ from R.
- Handle web __dependencies__ (see Chapter \@ref(htmltools-dependencies)).
Historically, `{htmltools}` was extracted out of `{shiny}` [@R-shiny] to be able to extend it, that is, develop custom HTML tags, import extra dependencies from the web. That's why both packages have many common functions!
The ultimate goal of `{htmltools}` is to __manipulate__, __combine__ and __rearrange__ tags in order to create flexible and rich HTML structures from R. Would you believe that the below example heavily relies on `{htmltools}` (Figure \@ref(fig:shinyRPG))?
```{r shinyRPG, echo=FALSE, fig.cap='{shinyRPG} was built with {htmltools}.', out.width='25%', fig.align = 'center'}
knitr::include_graphics("images/htmltools/shinyRPG.png")
```
If you want to try out this example, below is the showcase code:
```{r, eval=FALSE}
remotes::install_github("RinteRface/shinyRPG")
library(shinyRPG)
shinyRPGDemo()
```
## Writing HTML Tags from R
To install `{htmltools}`, we run:
```{r, eval=FALSE}
# CRAN
install.packages("htmltools")
# development version
remotes::install_github("rstudio/htmltools")
```
`{htmltools}` provides the necessary functions to write HTML tags that were introduced in Chapter \@ref(web-intro-html). In R, it is even more convenient than raw HTML since there is no opening/closing tag, a simple function call instead:
```{r}
library(htmltools)
tag <- div("Hello World")
tag
```
Inside the function call, all named elements become __attributes__, whereas unnamed elements become __children__. In some cases, tags may have empty attributes like `<input disabled>`. In that case,
the corresponding R code is `input(disabled = NA)`.
::: {.noteblock data-latex=""}
Pro tip: Since tag functions produce Shiny tags, that is, HTML elements, calling `tag` inside a document will render the tag instead of printing its code. Sometimes, particularly in this book, you want to see the code output. In that case, use the code below.
You may find another example [here](https://github.com/rstudio/htmltools/blob/6a03c3f35fbe6bfd0f91ba0607808a2b9127c5e5/vignettes/tagQuery.Rmd#L16).
:::
```{r, eval=FALSE}
# Render the tag instead of printing its code
library(knitr)
library(htmltools)
registerS3method(
"knit_print", "shiny.tag",
getS3method("print", "shiny.tag")
)
registerS3method(
"knit_print", "shiny.tag.list",
getS3method("print", "shiny.tag.list")
)
```
## Notations
If you type `htmltools::tags$` in the R console, you should be suggested the most common available HTML tags, thereby making it fairly easy to switch between HTML and R, as shown Figure \@ref(fig:htmltools-tags).
```{r htmltools-tags, echo=FALSE, fig.cap='htmltools tags builder.', out.width='100%'}
knitr::include_graphics("images/htmltools/htmltools-tags.png")
```
For convenience, the most commonly used tags like `p`, `h1`, `h2`, `h3`, `h4`, `h5`, `h6`, `a`, `br`, `div`, `span`, `pre`, `code`, `img`, `strong`, `em`, `hr`, ...
are accessible by a simple function call like:
```{r, eval=FALSE}
# good
h1("This is a title")
# correct but not necessary
tags$h1("This is a title")
```
Therefore, whether to use `tags$<TAG_NAME` or `<TAG_NAME>` depends if the tag is exported by default. Since `nav` is not exported, we write:
```{r, eval=FALSE}
# correct
tags$nav("This is the navigation")
# fail
try(nav("This is the navigation"))
```
When building custom templates, you will be writing a lot of tags. It might seem
too much work to always write `tags$<TAG_NAME>`. There exists a function called `withTags()`, which allows you to get rid of all `tags$`. Hence, the whole code is much easier to write and read:
```{r, eval=FALSE}
# Better
withTags(
nav(div(), ul(li(), li()))
)
# instead of
tags$nav(div(), tags$ul(tags$li(), tags$li()))
```
If you had to gather multiple tags together, choose `tagList()` over `list()`, although the HTML output is the same:
```{r}
# good
tag_list_1 <- tagList(
p("Some text"),
div("Content")
)
str(tag_list_1)
tag_list_1
# correct but not optimal
tag_list_2 <- list(
p("Some text"),
div("Content")
)
str(tag_list_2)
tag_list_2
```
The first has the `shiny.tag.list` class in addition to `list`. You may see it as a detail, but this has noticeable consequences. For instance, `tag_list_1` prints as HTML content, whereas `tag_list_2` prints as a list. If we try to apply `as.character()` on both elements, we obtain very different outputs:
```{r}
# tag_list_1
as.character(tag_list_1)
```
<!-- This is to trim the output too large for pdf -->
```{r, eval=FALSE}
# tag_list_2
as.character(tag_list_2)
```
```{r, echo=FALSE}
if (knitr::is_html_output()) {
as.character(tag_list_2)
}
```
```{r, echo=FALSE, results='asis'}
code <- '[1] "list(name = \"p\", attribs = list(),
children = list(\"Some text\"))"
[2] "list(name = \"div\", attribs = list(),
children = list(\"Content\"))"'
exclude_from_html(code)
```
Besides, packages like `{golem}` [@R-golem] allow us to test if an R object is a tag list. In this case, using a simple list would cause the test to fail.
## Adding new tags
You may define extra HTML tags with the `tag()` function:
```{r}
customTag <- tag(
"test",
list(class = "test", p("Custom Tag"))
)
str(customTag)
```
```{r, echo=FALSE}
customTag
```
Good practice is to check whether the created tag is in line with the HTML validation rules. If you want to check a web page, particularly a Shiny-generated HTML page, W3C has an online validation [tool](https://validator.w3.org/). Be careful, as not following this rule will cause the HTML code to be invalid. By default, Shiny complies with all the recommendations, but we suggest you be careful with any exotic template.
## Alternative way to write tags
`{htmltools}` comes with the `HTML()` function that you can feed with raw HTML. It prevents HTML escaping on the provided content, which is convenient, for instance, when using formatting tags inside a string:
```{r}
div("Hello <u>World</u>")
div(HTML("Hello <u>World</u>"))
```
Below, both codes give exactly the same output:
```{r, eval = FALSE}
HTML("<div>Blabla</div>")
div("Blabla")
```
Internally, their classes are different, which has consequences:
```{r}
class(HTML("<div>Blabla</div>"))
class(div("Blabla"))
```
Doing so, you will not be able to use tag-related functions, as in the next parts.
::: {.importantblock data-latex=""}
We strongly recommend using R as much as possible and avoid mixing HTML with R.
:::
Interestingly, if you want to convert raw HTML to R code, there is a Shiny App developed by Alan
Dipert from RStudio, namely [html2R](https://github.com/alandipert/html2r/tree/4217b5430e2bfc3af0d841cbefcd94bc1aadbcdf), shown Figure \@ref(fig:htmltools-html2R). Non-standard attributes (like `data-toggle`) are not correctly processed, but there are [solutions](https://github.com/alandipert/html2r/issues/2). This will save you precious time! A more recent approach is developed in Chapter \@ref(workflow-charpente) and has been used internally to develop some of the RinteRface [templates](https://github.com/RinteRface).
```{r htmltools-html2R, echo=FALSE, fig.cap='Illustration of the html2R app.', out.width='100%'}
knitr::include_graphics("images/htmltools/htmltools-html2R.png")
```
By converting HTML to R functions, it's easy to parameterize the generated tag and reuse it later in the code. It also allows you to maintain a single code base (only R), which is much simpler in the long run if the code has to be reviewed by people non-familiar with HTML.
## Playing with tags
Before becoming an `{htmltools}` wizard, let's learn the Shiny tags fundamentals.
### Tags structure
A __shiny tag__ is defined by:
- A __name__ such as `span`, `div`, `h1` ..., accessed with `tag$name`.
- Some __attributes__, which can be accessed with `tag$attribs`.
- __Children__, which can be accessed with `tag$children`.
- A __class__, namely `shiny.tag`.
For instance, the tag below has the following structure:
```{r}
# create the tag
myTag <- div(
class = "divclass",
id = "first",
h1("My first child!"),
span(class = "child", id = "baby", "Crying")
)
# access its name
# myTag$name
# access its attributes (id and class)
# myTag$attribs
# access children (returns a list of 2 elements)
# myTag$children
# access its class
str(myTag)
```
and the output is:
```{r, echo=FALSE}
myTag
```
How to modify the class of the second child?
```{r}
second_children <- myTag$children[[2]]
second_children$attribs$class <- "adult"
myTag
# This is not working ...
```
Why is this not working? By assigning `myTag$children[[2]]` to second_children, `second_children$attribs$class <- "adult"` modifies the class of the copy and not the original object. Thus we do:
```{r}
myTag$children[[2]]$attribs$class <- "adult"
myTag
```
### Practical examples: shinyRPG {#htmltools-shinyRPG}
Below we give concrete example on how to customize tags in real life. There exists a
nice RPG HTML template, that is, [rpgui](http://ronenness.github.io/RPGUI/). It provides
the necessary elements to get started developing nice RPG-looking user interfaces, as depicted by Figure \@ref(fig:rpgui-select).
```{r rpgui-select, echo=FALSE, fig.cap='rpgui select input with two displays: dropdown or list.', out.width='75%', fig.align='center'}
knitr::include_graphics("images/htmltools/rpgui-select.png")
```
In the following, we consider the select input, which does not have exactly the same
structure as the original Shiny tag. However, it is convenient to reuse the Shiny functions
to limit our amount of work. We therefore start to write our custom input:
```{r}
rpgSelect <- function(inputId, label, choices, selected = NULL,
multiple = FALSE, size = NULL) {
shiny::selectInput(
inputId,
label,
choices,
selected,
multiple,
selectize = FALSE,
width = NULL,
size
)
}
```
According to the rpgui documentation, a select tag is composed of the following HTML elements:
```{r, echo=FALSE, results='asis'}
html_code <- '<select class="rpgui-dropdown">
<option value="option1">option1</option>
<option value="option2">option2</option>
...
</select>'
code_chunk_custom(html_code, "html")
```
Adding a label tag on top of the slider, this is what we would like to get:
```{r, echo=FALSE, results='asis'}
html_code <- '<div>
<label id="variable-label" for="variable">Variable:</label>
<select
id="variable"
class="rpgui-dropdown">
<option value="cyl" selected>Cylinders</option>
<option value="am">Transmission</option>
<option value="gear">Gears</option>
</select>
</div>'
code_chunk_custom(html_code, "html")
```
We compare with our own `rpgSelect()` function:
```{r, eval=FALSE}
rpgSelect(
"variable",
"Variable:",
c("Cylinders" = "cyl",
"Transmission" = "am",
"Gears" = "gear")
)
```
<!-- This is to trim the output too large for pdf -->
```{r, echo=FALSE}
if (knitr::is_html_output()) {
rpgSelect(
"variable",
"Variable:",
c("Cylinders" = "cyl",
"Transmission" = "am",
"Gears" = "gear")
)
}
```
```{r, echo=FALSE, results='asis'}
code <- '<div class="form-group shiny-input-container">
<label class="control-label" id="variable-label"
for="variable">Variable:
</label>
<div>
<select id="variable" class="form-control">
<option value="cyl" selected>Cylinders</option>
<option value="am">Transmission</option>
<option value="gear">Gears</option>
</select>
</div>
</div>'
exclude_from_html(code)
```
As shown in the above output, this is not exactly matching:
- The outer div should not have any class.
- The label should not have any class.
- The input tag is wrapped inside a div container. It should not.
- The input tag should have the `rpgui-dropdown` or `rpgui-list` class,
depending on the size value.
To fix the first problem we target the outer tag (`selectTag`), that is, the tag returned by our `rpgSelect()` function. The second row cleans the label class. The third row removes the extra outer div and only keeps its children, corresponding to the input tag. The last instruction ensures we set the appropriate class, depending on the size value:
```{r, eval=FALSE}
# Modify tag
selectTag$attribs$class <- NULL
# Clean extra label class
selectTag$children[[1]]$attribs$class <- NULL
# Remove extra outer div
selectTag$children[[2]] <- selectTag$children[[2]]$children[[1]]
# Add good class for rppgui binding
selectTag$children[[2]]$attribs$class <- if (is.null(size)) {
"rpgui-dropdown"
} else {
"rpgui-list"
}
```
The complete code is given [here](https://github.com/RinteRface/shinyRPG/blob/a1fe30761ffd6469f28a0f92107d9613e9eccbe7/R/inputs.R#L180), which yields:
```{r, echo=FALSE}
rpgSelect <- function(inputId, label, choices, selected = NULL,
multiple = FALSE, size = NULL) {
selectTag <- shiny::selectInput(
inputId,
label,
choices,
selected,
multiple,
selectize = FALSE,
width = NULL,
size
)
# Modify tag
selectTag$attribs$class <- NULL
# Clean extra label class
selectTag$children[[1]]$attribs$class <- NULL
# Remove extra outer div
selectTag$children[[2]] <- selectTag$children[[2]]$children[[1]]
# Add good class for rppgui binding
selectTag$children[[2]]$attribs$class <- if (is.null(size)) {
"rpgui-dropdown"
} else {
"rpgui-list"
}
selectTag
}
```
```{r, eval=FALSE}
rpgSelect(
"variable",
"Variable:",
c("Cylinders" = "cyl",
"Transmission" = "am",
"Gears" = "gear")
)
```
<!-- This is to trim the output too large for pdf -->
```{r, echo=FALSE}
if (knitr::is_html_output()) {
rpgSelect(
"variable",
"Variable:",
c("Cylinders" = "cyl",
"Transmission" = "am",
"Gears" = "gear")
)
}
```
```{r, echo=FALSE, results='asis'}
code <- '<div>
<label id="variable-label" for="variable">Variable:</label>
<select id="variable" class="rpgui-dropdown">
<option value="cyl" selected>Cylinders</option>
<option value="am">Transmission</option>
<option value="gear">Gears</option>
</select>
</div>'
exclude_from_html(code)
```
### Useful functions for tags
As shown in the previous shinyRPG example, adding a class and an id to a tag may be done with:
```{r, eval=FALSE}
tag$attribs$class <- "class"
tag$attribs$id <- "id"
```
These are two lines of code, and believe me, for complex examples, it might be much worse, thereby significantly impairing code readability.
Fortunately, `{htmltools}` provides powerful functions to overcome this issue, the most significant being reviewed below.
#### Add attributes
`tagAppendAttributes()` adds a new attribute to the current tag. For instance, assuming we created a `div` without any id attribute:
```{r}
myTag <- div("A tag")
myTag <- tagAppendAttributes(myTag, id = "myid")
myTag
```
You can pass as many attributes as you want, including __non-standard__ attributes such as `data-toggle` (see Bootstrap 3 [tabs](https://www.w3schools.com/bootstrap/bootstrap_ref_js_collapse.asp) for instance):
```{r}
myTag <- tagAppendAttributes(
myTag,
`data-toggle` = "tabs",
class = "myclass"
)
myTag
```
As a reminder, even though correct, the classic approach would require two steps:
```{r, eval=FALSE}
myTag$attribs[["data-toggle"]] <- "newValue"
myTag$attribs$class <- "newClass"
myTag
```
<!-- This is to trim the output too large for pdf -->
```{r, echo=FALSE}
if (knitr::is_html_output()) {
myTag$attribs[["data-toggle"]] <- "newValue"
myTag$attribs$class <- "newClass"
myTag
}
```
```{r, echo=FALSE, results='asis'}
code <- '<div id="myid" data-toggle="newValue"
class="newClass">A tag
</div>'
exclude_from_html(code)
```
#### Check if tag has specific attribute
`tagHasAttribute()` checks if a tag has a specific attribute:
```{r}
# I want to know if div has a class
myTag <- div(class = "myclass")
tagHasAttribute(myTag, "class")
```
In practice, this function is suitable for testing tag elements as shown in Chapter \@ref(custom-templates-testing).
#### Get all attributes
`tagGetAttribute()` gets the targeted attribute's value, if it exists, otherwise NULL:
```{r}
myTag <- div(class = "test")
# returns the class
tagGetAttribute(myTag, "class")
# returns NULL
tagGetAttribute(myTag, "id")
```
#### Set child/children
`tagSetChildren()` creates children for a given tag. For instance:
```{r}
myTag <- div(
class = "parent",
id = "father",
"Father!"
)
child <- span("Daughter")
myTag <- tagSetChildren(myTag, child)
myTag
```
::: {.warningblock data-latex=""}
`tagSetChildren()` __removes__ all existing children. Below we see another set of functions to add children while conserving existing ones.
:::
#### Add child or children
`tagAppendChild()` and `tagAppendChildren()` add other tags to an existing tag.
Whereas `tagAppendChild()` only takes one tag, you can pass a list of tags to `tagAppendChildren()`.
```{r}
myTag <- div(class = "parent", "A tag", "Child 1")
otherTag <- span("Child 2")
myTag <- tagAppendChild(myTag, otherTag)
myTag
```
#### Build your own functions
You might wonder why there is no `tagRemoveChild()` or `tagRemoveAttributes()`.
Let's look at the `tagAppendChild()`:
```{r, eval = FALSE}
tagAppendChild <- function (tag, child, .cssSelector = NULL) {
if (!is.null(.cssSelector)) {
return(
tagAppendChildren(
tag,
child,
.cssSelector = .cssSelector
)
)
}
tag$children[[length(tag$children) + 1]] <- child
tag
}
```
Below we write the `tagRemoveChild()`, where tag is the target and n is the position to remove in the list of children:
```{r}
myTag <- div(class = "parent", span("Hey!"))
# we create the tagRemoveChild function
tagRemoveChild <- function(tag, n) {
# check if the list is empty
if (length(tag$children) == 0) {
stop(paste(tag$name, "does not have any children!"))
}
tag$children[n] <- NULL
tag
}
myTag <- tagRemoveChild(myTag, 1)
myTag
```
When defining the `tagRemoveChild()`, we choose `[` instead of `[[` to allow to select multiple list elements. Also notice that the function raises an error if the provided tag does not have children.
The `tagAppendChild()` is not able to insert at a specific position. We could draft the `tagInsertChild()` building on top of the base R `append` function:
```{r}
tagInsertChild <- function(tag, child, position) {
tag$children <- append(tag$children, list(child), position - 1)
tag
}
res1 <- tagInsertChild(p(span("hello")), a(), 1)
res2 <- tagInsertChild(p(span("hello")), a(), 2)
res1
res2
```
::: {.noteblock data-latex=""}
As of `{htmltools}` 0.5.2, there is a new `tagInsertChildren()` [function](https://rstudio.github.io/htmltools/reference/tagAppendChild.html).
:::
### Other functions
The [golem](https://github.com/ThinkR-open/golem/blob/dev/inst/utils/golem_utils_ui.R) package written by [thinkr](https://thinkr.fr) contains neat functions to edit your tags.
Particularly, the `tagRemoveAttributes()`:
```{r}
tagRemoveAttributes <- function(tag, ...) {
attrs <- as.character(list(...))
for (i in seq_along(attrs)) {
tag$attribs[[ attrs[i] ]] <- NULL
}
tag
}
```
```{r}
myTag <- div(class = "test", id = "coucou", "Hello")
myTag <- tagRemoveAttributes(myTag, "class", "id")
myTag
```
### Conditionally set attributes
Sometimes, you only want to set attributes under specific conditions.
```{r}
my_button <- function(color = NULL) {
tags$button(
style = paste("color:", color),
p("Hello")
)
}
```
Calling `my_button()` would give:
```{r, echo=FALSE}
my_button()
```
This example will not fail but having `style="color: "` is not clean. We may use conditions:
```{r}
my_button <- function(color = NULL) {
tags$button(
style = if (!is.null(color)) paste("color:", color),
p("Hello")
)
}
```
Below, we call `my_button("blue")` and `my_button()`:
```{r}
my_button("blue")
my_button()
```
In this example, style won't be available if color is not specified.
### Using %>%
While doing a lot of manipulation for a tag, if you don't need to create intermediate
objects, it is a good idea to use `%>%` from [magrittr](https://magrittr.tidyverse.org):
```{r, eval = FALSE}
myTag <- div(class = "cl", h1("Hello")) %>%
tagAppendAttributes(id = "myid") %>%
tagAppendChild(p("some extra text here!"))
myTag
```
This is overall easier to follow and read.
### Programmatically create children elements {#htmltools-programmatic-children}
Assume you want to create a tag with five children inside:
```{r, eval = FALSE}
myTag <- div(
span(1),
span(2),
span(3),
span(4),
span(5)
)
myTag
```
The structure is correct, but imagine if you had to create 1000 `span()` or a fancier tag. The previous approach is not consistent with the __DRY__ programming concept. `lapply()` function will be useful here (or the purrr `map()` family):
```{r, eval = FALSE}
# base R
div(lapply(1:5, function(i) span(i)))
# purrr + %>%
map(1:5, function(i) span(i)) %>% div()
```
```{r, echo=FALSE}
div(lapply(1:5, function(i) span(i)))
```
## Modern {htmltools} {#htmltools-modern}
::: {.importantblock data-latex=""}
This section requires basic CSS knowledge, particularly CSS selectors. Please read Chapter \@ref(beautify-css) before going further.
:::
As of `{htmltools}` 0.5.2, the new `tagQuery()` function makes manipulating Shiny tags a real pleasure, in addition to being more efficient. If you know and like __jQuery__ (Chapter \@ref(intro-jquery)), the API is really similar. If you don't know jQuery yet, no problem, we'll see it later in the book!
As a preliminary example, we want to modify the third `span` element from the above example in section \@ref(htmltools-programmatic-children):
```{r}
spans <- div(div(p(), lapply(1:5, function(i) span(i))))
spans$children[[1]]$children[[2]][[3]]$attribs$class <- "test"
spans
```
Below is the new `{htmltools}` approach, which leverages `tagQuery()`:
```{r}
spans <- div(div(p(), lapply(1:5, function(i) span(i))))
spans <- tagQuery(spans)$
find("span")$
filter(function(x, i) i == 3)$
addAttrs("class" = "amazing-tag")$
allTags()
spans
```
As you may notice, the first approach may lead to __poorly__ written code as soon as the
tag structure gets more complex. You may easily end up with things like `tag$children[[1]]$children[[2]]$children[[1]]$attribs$class`, which is nearly impossible to maintain.
The second approach is much more human __readable__, even though not necessarily shorter in this example.
The biggest advantage is that is does not always depend on the overall tag structure. As an exercise, you may wrap the `span` elements inside another `div` parent:
```{r}
spans <- div(div(p(), div(lapply(1:5, function(i) span(i)))))
spans <- tagQuery(spans)$
find("span")$
filter(function(x, i) i == 3)$
addAttrs("class" = "amazing-tag")$
allTags()
spans
```
The above code still works, while the previous one would require being updated.
Another reason to prefer the new `tagQuery()` API is the substantial performance [gains](https://rstudio.github.io/htmltools/articles/tagQuery.html#performance-1). Interestingly, under the hood, most if not all older `{htmltools}` functions like `tagAppendChildren()` or `tagAppendAttributes()` call the `tagQuery()` API when `.cssSelector` is provided. In practice,
while we can achieve multiple modifications at once with a single `tagQuery()` call, it requires a combination of multiple `tagAppendChildren()`/`tagAppendAttributes()` to reach the same result, thereby leading to less performance.
Are you ready to become a tag witcher [^tag-witcher]?
^[tag-witcher]: I hope you'll realize that it is easier than killing weird monsters, drinking dangerous potions made of deadly mutagens and not taking any bath for weeks...but one never knows!].
### Basics
`tagQuery()` accepts a tag or list of tags as input and returns a data structure containing:
- `$allTags()`: all tags.
- `$selectedTags()`: selected tags, default to `$allTags()`.
As an example:
```{r}
tag_query <- tagQuery(div(p()))
class(tag_query)
tag_query
```
As shown above, the returned result is not a Shiny tag. Instead, it is a [R6](https://r6.r-lib.org/articles/Introduction.html) class having methods to handle those tags.
### Query tags
Below is a table summarizing all available query methods. Note that at the time of writing, `tagQuery()` only supports simple CSS selectors. For instance, `data-...` selectors are not covered, as well as `,`, `+` and `~`. However, we'll see below there are many options to work around.
| Method | Description | Example |
|:----------:|:-------------:|
| children | Get all the direct descendants of each selected tag |
| find | Get all descendants of each selected tag |
| parent | Get the direct ancestors of each selected tag |
| parents | Get all parents of each selected tag |
| siblings | Get all siblings of each selected tag
| filter | Subset selected tags with CSS selectors or R function |
| resetSelected | Reset set of selected tags to the root tag |
According to Figure \@ref(fig:htmltools-query-tags), while `$children()` selects only direct descendants, `$find()` is slightly more powerful and drills down to any level deeper. `$filter()` is convenient to subset selected tags, for instance, depending on a specific attribute. A dedicated section (\@ref(htmtools-chain-queries)) covers `$resetSelected()`, which essentially resets the current selection to the root tag. Whereas `$parent()` allows going up step by step, returning each time the direct ancestor, `$parents()` returns all ancestors. If you need to be even more specific, `$closest(cssSelector)` goes up until it finds the matching `cssSelector`. Note that, if `cssSelector = NULL`, `$closest()` is equivalent to call `$selectedTags()`.
```{r htmltools-query-tags, echo=FALSE, fig.cap='tagQuery API: overview of query methods.', out.width='100%', fig.align='center'}
knitr::include_graphics("images/htmltools/htmltools-query-tags.png")
```
Let's consider an example consisting of a [tabset](https://mastering-shiny.org/action-layout.html#tabsets) panel with three tabs. Those menu items are one of the most challenging elements to handle when building a custom Shiny template and the new `tagQuery()` literally make it a breeze to handle. A detailed case study is available section \@ref(tabler-navbar-navigation).
```{r, eval=FALSE}
temp_tabs <- lapply(1:3, function(i) {
tabPanel(i, paste("Tab", i))
})
tabs <- bs4Dash::tabsetPanel(.list = temp_tabs)
tabs
```
```{r, include=FALSE}
temp_tabs <- lapply(1:3, function(i) {
tabPanel(i, paste("Tab", i))
})
tabs <- bs4Dash::tabsetPanel(.list = temp_tabs)
tabs
```
<!-- This is to trim the output too large for pdf -->
```{r, echo=FALSE}
if (knitr::is_html_output()) {
temp_tabs <- lapply(1:3, function(i) {
tabPanel(i, paste("Tab", i))
})
tabs <- bs4Dash::tabsetPanel(.list = temp_tabs)
tabs
}
```
```{r, echo=FALSE, results='asis'}
code <- '<div class="tabbable">
<ul class="nav nav-tabs" data-tabsetid="5315">
<li class="nav-item">
<a href="#" data-toggle="tab" data-value="1"
class="nav-link active" data-target="#tab-5315-1">1
</a>
</li>
<li class="nav-item">
<a href="#" data-toggle="tab" data-value="2"
class="nav-link" data-target="#tab-5315-2">2
</a>
</li>
<li class="nav-item">
<a href="#" data-toggle="tab" data-value="3"
class="nav-link" data-target="#tab-5315-3">3
</a>
</li>
</ul>
<div class="tab-content" data-tabsetid="5315">
<div class="tab-pane active" data-value="1"
id="tab-5315-1">Tab 1
</div>
<div class="tab-pane" data-value="2"
id="tab-5315-2">Tab 2
</div>
<div class="tab-pane" data-value="3"
id="tab-5315-3">Tab 3
</div>
</div>
</div>'
exclude_from_html(code)
```
How would you select the third tab content element?
```{r, eval=FALSE}
tagQuery(tabs)$
find("div.tab-pane")$ # div element with tab-pane class
filter(function(x, i) tagGetAttribute(x, "data-value") == 3)
```
```{r, echo=FALSE}
if (knitr::is_html_output()) {
tagQuery(tabs)$
find("div.tab-pane")$ # div element with tab-pane class
filter(function(x, i) tagGetAttribute(x, "data-value") == 3)$
selectedTags()
}
```
```{r, echo=FALSE, results='asis'}
code <- '[[1]]
<div class="tab-pane" data-value="3" id="tab-8147-3">Tab 3
</div>'
exclude_from_html(code)
```
Note that we provided an anonymous R function to `$filter()`, where x is the tag and i the index, allowing us to drill down to the third tab, which has `data-value = 3`.
As an exercise, I give you two minutes to find the classic `{htmltools}` equivalent. If you don't manage, it means the new `tagQuery()` system is rather convenient.
### Modify tags
As shown in the preliminary example, the main interest of querying tags is to ultimately modify them. `tagQuery()` exposes methods to modify __attributes__, __children__ of the query selection.
#### Playing with attributes
As shown Figure \@ref(fig:htmltools-modify-attributes), there are currently two main methods to alter tag attributes, namely `$addAttrs()` (equivalent of `tagAppendAttributes`) and `$removeAttrs()`, even though more specific methods exists, for instance `$addClass()`, `$removeClass()` and `$toggleClass()`.
| Method | Description | Example |
|:----------:|:-------------:|
| addAttrs | Add any number of attributes to each selected tag |
| removeAttrs | Remove any number of attributes to each selected tag |
| hasAttrs | Check if the selected tag has the specified attribute(s) |
| addClass | Add any number of new classes to each selected tag |
| removeClass | Remove any number of classes to each selected tag |
| hasClass | Check if the selected tag has the specified classe(s) |
```{r htmltools-modify-attributes, echo=FALSE, fig.cap='tagQuery API: modify tag attributes.', out.width='100%'}
knitr::include_graphics("images/htmltools/htmltools-modify-attributes.png")
```
Bootstrap 4 allows us to apply a fade [transition](https://getbootstrap.com/docs/4.6/components/navs/#fade-effect) between tabs, provided that those tabs have the `fade` class. Below is how to seamlessly do it with `tagQuery()`:
```{r, eval=FALSE}
tagQuery(tabs)$
find(".tab-pane")$
addClass("fade")
```
```{r, echo=FALSE}
if (knitr::is_html_output()) {
tagQuery(tabs)$
find(".tab-pane")$
addClass("fade")$
selectedTags()
}
```
```{r, echo=FALSE, results='asis'}
code <- '[[1]]
<div class="tab-pane active fade" data-value="1"
id="tab-4283-1">Tab 1
</div>
[[2]]
<div class="tab-pane fade" data-value="2"
id="tab-4283-2">Tab 2
</div>
[[3]]
<div class="tab-pane fade" data-value="3"
id="tab-4283-3">Tab 3
</div>'
exclude_from_html(code)
```
#### Altering tag/children/siblings
Below are listed some methods to alter the current tag or its children, as depicted Figure \@ref(fig:htmltools-alter-tags).
| Method | Description | Example |
|:----------:|:-------------:|
| append | Insert content after the children of each selected tag |
| prepend | Insert content before the children of each selected tag |
| empty | Remove all children from the selected tag |
| remove | Remove all selected tags |
| before | Insert content before each selected tag |
| after | Insert content after each selected tag |
| replaceWith | Replace the currently selected tag by the provided tag |
```{r htmltools-alter-tags, echo=FALSE, fig.cap='tagQuery API: alter tags children.', out.width='100%'}
knitr::include_graphics("images/htmltools/htmltools-alter-tags.png")
```
Going back to our previous tabs example, we would like to include an icon before each tab title. We leverage the `$prepend()` method, after selecting the `a` elements part of the tab navigation:
```{r, eval=FALSE}
# Add extra item to tabs at the end
new_tabs <- tagQuery(tabs)$
find("a")$
prepend(icon("flag"))