-
Notifications
You must be signed in to change notification settings - Fork 58
/
custom-templates-skeleton.Rmd
547 lines (425 loc) · 23.9 KB
/
custom-templates-skeleton.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
# Create template elements {#custom-templates-skeleton}
The list of all available tabler __layouts__ is quite impressive (horizontal, vertical, compressed, right to left, dark, ...). In the next steps, we will focus on the __dark-compressed__ template, leaving the reader to try other templates as an exercise.
## Identify template elements
We are quite lucky since there is nothing fancy about the Tabler layout. As usual, let's inspect the `layout-condensed-dark.html` (located `/demo` [folder](https://github.com/tabler/tabler/blob/14d0c001436b85d2a4533d63680d209affdf774b/demo/layout-condensed-dark.html)) in Figure \@ref(fig:tabler-layout-intro).
```{r tabler-layout-intro, echo=FALSE, fig.cap='Tabler condensed layout.', out.width='77%', fig.align='center'}
knitr::include_graphics("images/practice/tabler-layout-intro.png")
```
There are two main components:
- the __header__ containing the brand logo, the navigation and dropdown.
- the __content__ containing the dashboard body as well as the footer.
::: {.warningblock data-latex=""}
The dashboard body does not mean `<body>` tag.
:::
That is it for now.
## Design the page layout
### The page wrapper
Do you remember the structure of a basic HTML page seen in section \@ref(web-intro-html)? Well, if not, here is a reminder.
```{r, echo=FALSE, results='asis'}
html_code <- '<!DOCTYPE HTML>
<html lang="en">
<head>
<!-- head content here -->
<title>A title</title>
</head>
<body>
<!-- body content here -->
</body>
</html>'
code_chunk_custom(html_code, "html")
```
We actually don't need to include the `<html>` tag since Shiny does it on the fly, as described earlier in section \@ref(build-shiny-ui). Below we construct a list of tags with `tagList()`, including the __head__ and the __body__. In the head, we have the `meta` tag, which has multiple purposes:
- Describe the [encoding](https://www.w3schools.com/html/html_charset.asp), which briefly controls what character can be displayed on the page. __UTF-8__ is a safe choice as it covers almost all existing characters.
- How to display the app on different devices. For instance the __viewport__ meta tag handles the responsive web design. `width=device-width`, allows the page width to vary depending on the user device. `initial-scale=1` handles the initial page zoom.
- Set the __favicon__, which is an icon representing the website icon, that is the one you may see on the right side of the searchbar. Try [Twitter](https://twitter.com/home) for instance.
- ...
The page __title__ and favicon may be changed by the developer, so they may be included as function parameters. If you remember, there should also be CSS in the head but they are missing. Actually, the insertion of dependencies is achieved by our very own `add_tabler_deps()` function defined in Chapter \@ref(custom-templates-dependencies). Tabler comes with two main __themes__, namely white and dark, which may be applied through the `<body>` class attribute (respectively, `antialiased theme-dark` and `antialiased`). The __...__ parameter contains other template elements like the header and the dashboard body, which remain to be designed. As shown in Figure \@ref(fig:tabler-dark), the Tabler dashboard template may contain a __navigation__ bar as well as a __footer__. As they are not mandatory, we don't create dedicated parameters and pass all elements in the `...` slot:
```{r}
tabler_page <- function(..., dark = TRUE, title = NULL,
favicon = NULL){
# head
head_tag <- tags$head(
tags$meta(charset = "utf-8"),
tags$meta(
name = "viewport",
content = "
width=device-width,
initial-scale=1,
viewport-fit=cover"
),
# ... Elements omitted for space reasons
tags$link(
rel = "shortcut icon",
href = favicon,
type="image/x-icon"
)
)
# body
body_tag <- add_tabler_deps(
tags$body(
tags$div(
class = paste0("antialiased ", if (dark) "theme-dark"),
style = "display: block;",
tags$div(class = "page", ...)
)
)
)
tagList(head_tag, body_tag)
}
```
The whole code maybe found in the `{OSUICode}` side [package](https://github.com/DivadNojnarg/outstanding-shiny-ui-code/blob/b040a24e576f5d190825be0433edac288bbfbc26/R/tabler.R#L69).
Below we quickly test if a Tabler element renders well, to confirm whether our setup is adequate. To do this, we include a card element taken from the demo HTML page, using `HTML()`.
::: {.importantblock data-latex=""}
Let's be clear: this is only for __testing purposes__. In production, you should avoid this as much as possible because of __security__ issues and the __bad readability__ of the code.
:::
This also checks that our basic Shiny input/output system works as expected with a `textInput()` linked to a `textOutput()` to provide the card title:
```{r, echo=FALSE, results='asis'}
code_chunk(OSUICode::get_example("tabler/test-template"), "r")
```
OK, our card and the shiny element work like a charm, which is a good start. Now we may focus on the aesthetics.
### The body content
In this part, we translate the dashboard __body__ HTML code to R. As a reminder, the [html2r](https://alandipert.shinyapps.io/html2r/) by [Alan Dipert](https://github.com/alandipert) substantially speeds up the conversion process. You copy the code in the HTML text area, click on convert and get the R Shiny output. We create a function called `tabler_body()`. The __...__ parameter holds all the dashboard body elements, and the __footer__ is dedicated for the future `tabler_footer()` function.
```{r}
tabler_body <- function(..., footer = NULL) {
div(
class = "content",
div(class = "container-xl", ...),
tags$footer(class = "footer footer-transparent", footer)
)
}
```
Let's test it with the previous example.
```{r, eval=FALSE}
ui <- tabler_page(tabler_body(h1("Hello World")))
server <- function(input, output) {}
shinyApp(ui, server)
```
Way better!
### The footer
The footer is composed of left and right containers.
We decide to create parameters left and right in which the user may pass any elements:
```{r}
tabler_footer <- function(left = NULL, right = NULL) {
div(
class = "container",
div(
class = "row text-center align-items-center
flex-row-reverse",
div(class = "col-lg-auto ml-lg-auto", right),
div(class = "col-12 col-lg-auto mt-3 mt-lg-0", left)
)
)
}
```
All the class __attributes__ are taken from the original HTML template.
If you are already familiar with Bootstrap 4, you may easily customize the style. The main container leverages the __flexbox__ model, shown in section \@ref(beautify-css-flexbox). In short, `row` means that all elements are aligned on a row; `text-center` and `align-items-center` handle the text and content centering. `flex-row-reverse` displays elements in a reversed order.
Notice also that a `row` element contains columns created with the `col` class.
As above, let's check our brand-new element (Figure \@ref(fig:tabler-basic)).
```{r, echo=FALSE, results='asis'}
code_chunk(OSUICode::get_example("tabler/basic-template"), "r")
```
```{r tabler-basic, echo=FALSE, fig.cap='Tabler basic structure with content and footer.', out.width='100%'}
knitr::include_graphics("images/practice/tabler-basic.png")
```
### The navbar (or header)
This function is called `tabler_header()`. In the Tabler template, the __header__ has the
`navbar navbar-expand-md navbar-light` classes. We don't need the `navbar-light` class
since we are only interested in the dark theme. As shown in Figure \@ref(fig:tabler-header), the header is composed of four elements:
- The navbar __toggler__ is only visible when we reduce the screen width, like on mobile devices.
- The __brand__ image
- The __navigation__ menu.
- The __dropdown__ menus (this is not mandatory).
```{r tabler-header, echo=FALSE, fig.cap='Tabler header structure.', out.width='100%'}
knitr::include_graphics("images/practice/tabler-header.png")
```
You may have a look at the [Bootstrap 4](https://getbootstrap.com/docs/4.0/components/navbar/) documentation for extra configuration and layout.
Each of these elements will be considered a parameter to the `tabler_navbar()` function, except the navbar toggler, which is a default element and must not be removed:
```{r, eval=FALSE}
tabler_navbar <- function(..., brand_url = NULL,
brand_image = NULL, nav_menu,
nav_right = NULL) {
# SEE BELOW
}
```
Morever, we only show the brand element when it is provided. The __...__
parameter is a slot for extra elements (between the menu and dropdowns).
In the following, we start by creating the main container, that is
`header_tag` and its unique child `container_tag`:
```{r, eval=FALSE}
header_tag <- tags$header(class = "navbar navbar-expand-md")
container_tag <- tags$div(class = "container-xl")
```
The latter has four children: `toggler_tag`, `brand_tag`, `dropdown_tag` and `navmenu_tag`. `toggler_tag` is only visible on small screen devices or when the browser window's width is reduced. It consists of a button that has two important attributes `data-toggle` and `data-target`. They are part of the Bootstrap 4 template and briefly mean that the button will toggle a collapsible element having the `navbar-menu` unique id. The toggle icon is provided in a simple `span` element:
```{r, eval=FALSE}
# toggler for small devices (must not be removed)
toggler_tag <- tags$button(
class = "navbar-toggler",
type = "button",
`data-toggle` = "collapse",
`data-target` = "#navbar-menu",
span(class = "navbar-toggler-icon")
)
```
The `navmenu_tag` is the `toggler_tag` target, linked by the `id` and the `collapse` class. It is a container leveraging Flexbox, that will host the not yet defined `nav_menu` elements. In the following code, you probably notice some outstanding classes like `mr-md-4`, `py-2`. It corresponds to the Bootstrap 4 spacing [system](https://getbootstrap.com/docs/4.0/utilities/spacing/). Overall, `m` stands for margin while `p` means padding. `x`, `y`, `t`, `b`, `l` and `r` set the direction. The spacing value is an integer whose value ranges between 0 and 5 (or set to `auto`). Keep in mind the following rule `{sides}-{breakpoint}-{size}`, where `breakpoint` may be one of `sm`, `md`, `lg` and `xl`. If you remember the CSS media queries section \@ref(beautify-css-responsive), this is the same principle: `pl-md-4` will apply a padding on the left side for all devices with a screen [width](https://getbootstrap.com/docs/4.0/layout/grid/#grid-options) of at least 768px (`md`), thereby excluding small and extra-small devices (`sm`, `xs`).
```{r, eval=FALSE}
navmenu_tag <- div(
class = "collapse navbar-collapse",
id = "navbar-menu",
div(
class = "d-flex flex-column flex-md-row flex-fill
align-items-stretch align-items-md-center",
nav_menu
),
if (length(list(...)) > 0) {
div(
class = "ml-md-auto pl-md-4 py-2 py-md-0 mr-md-4
order-first order-md-last flex-grow-1 flex-md-grow-0",
...
)
}
)
```
The `brand_tag` is an optional image with `navbar-brand` main class:
```{r, eval=FALSE}
# brand elements
brand_tag <- if (!is.null(brand_url) ||
!is.null(brand_image)) {
a(
href = if (!is.null(brand_url)) {
brand_url
} else {
"#"
},
class = "navbar-brand navbar-brand-autodark
d-none-navbar-horizontal pr-0 pr-md-3",
if(!is.null(brand_image)) {
img(
src = brand_image,
alt = "brand Image",
class = "navbar-brand-image"
)
}
)
}
```
`dropdown_tag`:
```{r, eval=FALSE}
dropdown_tag <- if (!is.null(nav_right)) {
div(class = "navbar-nav flex-row order-md-last", nav_right)
}
```
Remember that `container_tag` has to contain the four previously defined children tags. In this situations, `{htmltools}` functions like `tagAppendChild()` and `tagAppendChildren()` are game changers to better organize the code and make it more __maintainable__.
```{r, eval=FALSE}
# ... other tags defined above
container_tag <- tagAppendChildren(
container_tag,
toggler_tag,
brand_tag,
dropdown_tag,
navmenu_tag
)
# Final navbar wrapper
tagAppendChild(header_tag, container_tag)
```
Users never know in advance how extra features will be added to that component. Hence being cautious at the very beginning is crucial! The `tabler_navbar()` full code is given [here](https://github.com/DivadNojnarg/outstanding-shiny-ui-code/blob/b040a24e576f5d190825be0433edac288bbfbc26/R/tabler.R#L236).
The __navbar menu__ is the main navbar component. The __...__ parameter is a slot for the __menu items__.
Compared to the original Tabler dashboard template where there is only the `navbar-nav` class, we have to add at least, the `nav` class to make sure items are correctly activated/inactivated. The `nav-pills` class is to select pills instead of basic tabs (see [here](https://getbootstrap.com/docs/4.0/components/navs/)), which is nothing more than a cosmetic consideration. Notice the `ul` tag that will contain `li` elements, that is the navbar items:
```{r, eval=FALSE}
tabler_navbar_menu <- function(...) {
tags$ul(class = "nav nav-pills navbar-nav", ...)
}
```
Besides, each navbar menu item could be either a simple button or contain multiple menu sub-items.
For now, we only focus on simple items.
#### Navbar navigation {#tabler-navbar-navigation}
The navbar is crucial since it drives the template navigation. We would like to associate each item to a separate page in the body content. This would allow us to navigate to a new page each time we change an item. In brief, it is very similar to the Shiny `tabsetPanel()` function.
In HTML, menu items are `<a>` tags (links) with a given `href` attribute pointing to a specific page located in the server files. With Shiny, as applications are single page by design, we can't split our content into multiple pages. The strategy here is to create a __tabbed navigation__, to mimic __multi-pages layout__.
Let's see how the tab navigation works. In the menu list, all items must have:
- A __data-toggle__ attribute set to `tab` or `pill`.
- A __href__ or __data-target__ attribute holding a unique __id__, being mandatory since it points the menu item to the corresponding body content.
::: {.importantblock data-latex=""}
Importantly, `href` navigation appears to be broken on shinyapps.io, RStudio Connect (actually all rstudio products relying on __workers__ to spread the user load across multiple R processes).
Therefore, we'll choose the `data-target` attribute.
:::
On the body side, tab panels are contained in a __tabset__ panel (simple div container), have a `role` attribute set to `tabpanel` and an __id__ corresponding to the __data-target__ passed in the menu item. The exact match between __id__ and __data-target__ is mandatory, as shown in Figure \@ref(fig:tabler-tabset).
```{r tabler-tabset, echo=FALSE, fig.cap='Tabler tabset panel: main principle.', out.width='100%'}
knitr::include_graphics("images/practice/tabler-tabset.png")
```
Below, we propose a possible implementation of a menu item, as well as the corresponding body tab panel. The text parameter corresponds to the nav item text displayed in the menu. We also added an optional icon and the ability to select the item at start:
```{r, eval=FALSE}
tabler_navbar_menu_item <- function(text, tabName, icon = NULL,
selected = FALSE) {
item_cl <- paste0("nav-link", if (selected) " active")
tags$li(
class = "nav-item",
a(
class = item_cl,
`data-target` = paste0("#", tabName),
`data-toggle` = "pill",
`data-value` = tabName,
role = "tab",
span(class = "nav-link-icon d-md-none
d-lg-inline-block", icon),
span(class = "nav-link-title", text)
)
)
}
```
We also decided to add a fade transition effect between tabs, as per Bootstrap 4 documentation, which consists of the `fade` extra class:
```{r, eval=FALSE}
tabler_tab_items <- function(...) {
div(class = "tab-content", ...)
}
tabler_tab_item <- function(tabName = NULL, ...) {
div(
role = "tabpanel",
class = "tab-pane fade container-fluid",
id = tabName,
...
)
}
```
What about testing this in a Shiny app?
```{r, echo=FALSE, results='asis'}
code_chunk(OSUICode::get_example("tabler/navbar"), "r")
```
At this point you might argue that we did not even validate the template elements. For instance, going back to the `tabler_navbar_menu_item` function, we find the following possible issues:
- What happens if the user provides an invalid tabName,
i.e. a text that is not valid for jQuery like `tab&?++`?
- What happens if the user accidentally activates two tabs at start?
We see later in Chapter \@ref(custom-templates-testing) how to validate those parameters.
#### Fine-tune tabs behavior
Quite good, isn't it? You notice however that even if the first tab is selected by default, its content is not shown. To fix this, we apply our jQuery skills. According to the Bootstrap documentation, we must trigger the __show__ event on the active tab at start, as well as add the classes `show` and `active` to the associated tab panel in the dashboard body. We therefore target the nav item that has the active class and if no item is found, we select the first item by default and activate its body content.
```{r, echo=FALSE, results='asis'}
js_code <- "$(function() {
// this makes sure to trigger the show event on
// the active tab at start
let activeTab = $('#navbar-menu .nav-link.active');
// if multiple items are found
if (activeTab.length > 0) {
let tabId = $(activeTab).attr('data-value');
$(activeTab).tab('show');
$(`#${tabId}`).addClass('show active');
} else {
$('#navbar-menu .nav-link')
.first()
.tab('show');
}
});"
code_chunk_custom(js_code, "js")
```
This script is included in the the below app `www` folder. We see in Chapter \@ref(custom-templates-inputs) that custom input binding may perfectly handle this situation and are actually preferred.
```{r, echo=FALSE, results='asis'}
code_chunk(OSUICode::get_example("tabler/navbar-bis", view_code = FALSE), "r")
```
The result is shown in Figure \@ref(fig:tabler-nav). We'd also suggest including at least one input/output per tab, to test whether everything works properly.
```{r tabler-nav, echo=FALSE, fig.cap='Tabler template with navbar.', out.width='100%'}
knitr::include_graphics("images/practice/tabler-nav.png")
```
Looks like we are done for the main template elements. Actually, wouldn't it be better to include, at least, card containers?
### Card containers
Card are a central piece of template as they may contain visualizations, metrics and much more, generally enhancing content visibility. Thus, this is not a hazard why I choose this component and fortunately, Tabler offers a large choice of __card containers__.
#### Classic card
What we call a classic card is like the [shinydashboard](https://rstudio.github.io/shinydashboard/structure.html) `box()` container. The card structure has key elements:
- A __width__ to control the space taken by the card in the Bootstrap [grid](https://getbootstrap.com/docs/4.0/layout/grid/).
- A __title__, in general in the header (Tabler does always not follow this rule and header is optional).
- A __body__ where the main content is.
- Style elements like color __statuses__.
- A __footer__ (optional, Tabler does not include this).
A comprehensive list of all Tabler card features may be found [here](https://preview-dev.tabler.io/docs/cards.html). To be faster, we copy the following HTML code in the [html2R](https://github.com/alandipert/html2r) Shiny app to convert it to Shiny tags:
```{r, echo=FALSE, results='asis'}
html_code <- '<div class="col-md-6">
<div class="card">
<div class="card-status-top bg-danger"></div>
<div class="card-body">
<h3 class="card-title">Title</h3>
<p>Some Text.</p>
</div>
</div>
</div>'
code_chunk_custom(html_code, "html")
```
Below is the result. The next step consists of replacing all content by parameters to the `tabler_card()` function, whenever necessary. For instance, the first `<div>` sets the card width. The Bootstrap grid ranges from 1 to 12, so why not create a width parameter to control the card size. We proceed similarly for the title, status, body content. It seems reasonable to allow title to be `NULL` (if so, the title is not shown), same thing for the status. Regarding the card default width, a value of six also makes sense, which would take half of the row:
```{r}
tabler_card <- function(..., title = NULL, status = NULL,
width = 6, stacked = FALSE,
padding = NULL) {
card_cl <- paste0(
"card",
if (stacked) " card-stacked",
if (!is.null(padding)) paste0(" card-", padding)
)
status_tag <- if (!is.null(status)) {
div(class = paste0("card-status-top bg-", status))
}
body_tag <- div(
class = "card-body",
# we could have a smaller title like h4 or h5...
if (!is.null(title)) {
h3(class = "card-title", title)
},
...
)
main_wrapper <- div(class = paste0("col-md-", width))
card_wrapper <- div(class = card_cl)
card_wrapper <- tagAppendChildren(
card_wrapper, status_tag, body_tag
)
tagAppendChild(main_wrapper, card_wrapper)
}
```
In the meantime, it would be also convenient to be able to display cards in the same row. Let's create the `tabler_row()` function:
```{r}
tabler_row <- function(...) {
div(class = "row row-deck", ...)
}
```
Below, we show an example of the `tabler_card()` function, in combination with the `{apexcharter}` package. The whole code may be printed with `OSUICode::get_example("tabler/card")`.
```{r, echo=FALSE, results='asis'}
code_chunk(OSUICode::get_example("tabler/card", view_code = FALSE), "r")
```
The code output is shown in Figure \@ref(fig:tabler-card).
```{r tabler-card, echo=FALSE, fig.cap='Tabler card component.', out.width='100%'}
knitr::include_graphics("images/practice/tabler-card.png")
```
### Ribbons: card components
Let's finish this part by including a card component,
namely the [ribbon](https://preview-dev.tabler.io/docs/ribbons.html).
```{r, eval=FALSE}
tabler_ribbon <- function(..., position = NULL, color = NULL,
bookmark = FALSE) {
ribbon_cl <- paste0(
"ribbon",
if (!is.null(position)) sprintf(" bg-%s", position),
if (!is.null(color)) sprintf(" bg-%s", color),
if (bookmark) " ribbon-bookmark"
)
div(class = ribbon_cl, ...)
}
```
Integrating the freshly created ribbon component requires modifying the card structure since the ribbon is added after the body tag, and no parameter is associated with this slot. We could also modify the `tabler_card()` function, but `{htmltools}` offers tools to help us. Since the ribbon should be put after the card body, we may think about the `tagAppendChild()` function, introduced in Chapter \@ref(htmltools-overview):
```{r, eval=FALSE}
# add the ribbon to a card
my_card <- tabler_card(title = "Ribbon", status = "info")
my_card$children[[1]] <- tagAppendChild(
my_card$children[[1]],
tabler_ribbon(
icon("info-circle", class = "fa-lg"),
bookmark = TRUE,
color = "red"
)
)
```
Now, we check how it looks in a Shiny app.
```{r, echo=FALSE, results='asis'}
code_chunk(OSUICode::get_example("tabler/ribbon", view_code = FALSE), "r")
```
```{r tabler-ribbon, echo=FALSE, fig.cap='Tabler ribbon component.', out.width='100%'}
knitr::include_graphics("images/practice/tabler-ribbon.png")
```
### Icons
Not mentioned before, but we may include Font Awesome icons provided with Shiny, as well as other libraries.
Moreover, Tabler has a internal svg library located [here](https://preview-dev.tabler.io/icons.html).
## Exercises
1. Have a look at this [page](https://preview-dev.tabler.io/snippets.html). Select two elements and create the corresponding R functions.
2. Leverage the new `{htmltools}` `tagQuery()` API (see section \@ref(htmltools-modern)) to rewrite the `tabler_navbar()` and `tabler_card()` functions.