Skip to content

Commit 9e91678

Browse files
authored
Chapter 18 (#30)
* button and switch added * starting nav bar second input * completing chapter summary
1 parent 31c3b08 commit 9e91678

File tree

4 files changed

+509
-4
lines changed

4 files changed

+509
-4
lines changed

18_develop-custom-input-widgets.Rmd

Lines changed: 384 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,393 @@
1+
---
2+
output: html_document
3+
editor_options:
4+
chunk_output_type: console
5+
---
16
# Develop custom input widgets
27

38
**Learning objectives:**
49

5-
- THESE ARE NICE TO HAVE BUT NOT ABSOLUTELY NECESSARY
10+
- Integrate new inputs after defining:
611

7-
## SLIDE 1
12+
- The template dependencies
13+
- The page skeleton
14+
- Containers like cards
15+
16+
17+
18+
- Learn how to add
19+
20+
- Tabler action button
21+
- Toggle Switch
22+
- Navbar menu input
23+
24+
## Tabler action button {-}
25+
26+
Tabler has **built-in HTML** buttons with a substantial amount of **custom styles** with really simple element, only needs 2 classes:
27+
28+
- `btn`
29+
- `btn-primary`
30+
31+
```html
32+
<button class="btn btn-primary">Button</button>
33+
```
34+
35+
But as input we expect the next behavior:
36+
37+
- When the app starts, the action button has the value 0
38+
- Each click increments its value by 1
39+
40+
41+
## Tabler action button {-}
42+
43+
As `shiny` does that we can use the binding the behind the `shiny::actionButton`.
44+
45+
`actionButtonInputBinding` shiny input binding which apply for elements with `class = action-button`
46+
47+
```javascript
48+
var actionButtonInputBinding = new InputBinding();
49+
$.extend(actionButtonInputBinding, {
50+
find: function(scope) {
51+
return $(scope).find('.action-button');
52+
},
53+
getValue: function(el) {
54+
return $(el).data('val') || 0;
55+
},
56+
// ....; Extra code removed
57+
});
58+
```
59+
60+
## Tabler action button {-}
61+
62+
Now by checking the source code of `shiny::actionButton` we can that:
63+
64+
- Tabler and `shiny` are using the button tag
65+
- Table don't use the class `btn-default`
66+
67+
```r
68+
actionButton <- function (inputId, label, icon = NULL,
69+
width = NULL, ...) {
70+
71+
value <- restoreInput(id = inputId, default = NULL)
72+
73+
tags$button(
74+
id = inputId,
75+
style = if (!is.null(width)) {
76+
paste0("width: ", validateCssUnit(width), ";")
77+
},
78+
type = "button",
79+
class = "btn btn-default action-button",
80+
`data-val` = value,
81+
list(validateIcon(icon), label), ...
82+
)
83+
}
84+
```
85+
86+
## Tabler action button {-}
87+
88+
By copying the source code into a new function we can create an action button with Tabler style.
89+
90+
```r
91+
tabler_button <- function(inputId, label, status = NULL, icon = NULL, width = NULL, ...) {
92+
93+
# recover any possible bookmarked
94+
value <- restoreInput(id = inputId, default = NULL)
95+
96+
# defining the classes to use
97+
btn_cl <- paste0(
98+
"btn action-button",
99+
if (is.null(status)) {
100+
" btn-primary"
101+
} else {
102+
paste0(" btn-", status)
103+
}
104+
)
105+
106+
# custom right margin
107+
if (!is.null(icon)) icon$attribs$class <- paste0(
108+
icon$attribs$class, " mr-1"
109+
)
110+
111+
# creating the html
112+
tags$button(
113+
id = inputId,
114+
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
115+
type = "button",
116+
class = btn_cl,
117+
`data-val` = value,
118+
list(icon, label), ...
119+
)
120+
}
121+
```
122+
123+
**Let's RUN example 1**
124+
125+
## Toggle Switch {-}
126+
127+
In Tabler we can see the next **switch component** which have the peculiarity of having `type="checkbox"`.
128+
129+
```html
130+
<label class="form-check form-switch">
131+
<input class="form-check-input" type="checkbox" checked>
132+
<span class="form-check-label">Option 1</span>
133+
</label>
134+
```
135+
136+
Just like the checkbox in shiny.
137+
138+
```r
139+
shiny::checkboxInput("test", "Test", TRUE)
140+
```
141+
142+
```html
143+
<div class="form-group shiny-input-container">
144+
<div class="checkbox">
145+
<label>
146+
<input id="test" type="checkbox" class="shiny-input-checkbox" checked="checked"/>
147+
<span>Test</span>
148+
</label>
149+
</div>
150+
</div>
151+
```
152+
153+
## Toggle Switch {-}
154+
155+
Just by exploring `checkboxInputBinding` input binding we can confirm that shiny is using `type = "checkbox"` in the input tag.
156+
157+
```javascript
158+
var checkboxInputBinding = new InputBinding();
159+
$.extend(checkboxInputBinding, {
160+
find: function(scope) {
161+
return $(scope).find('input[type="checkbox"]');
162+
},
163+
// ....; Extra code removed
164+
}
165+
});
166+
167+
inputBindings.register(checkboxInputBinding, 'shiny.checkboxInput');
168+
```
169+
170+
## Toggle Switch {-}
171+
172+
After this confirmation we just need to replicate the Tabler html into a function.
173+
174+
```r
175+
tabler_switch <- function(inputId, label, value = FALSE, width = NULL) {
176+
177+
# Recovers any possible bookmarked
178+
value <- restoreInput(id = inputId, default = value)
179+
180+
# main wrapper creation
181+
input_wrapper <- tags$label(
182+
class = "form-check form-switch",
183+
style = if (!is.null(width)) {
184+
paste0("width: ", validateCssUnit(width), ";")
185+
}
186+
)
187+
188+
# Defining the input tag to be find by the binding
189+
# with form-check-input from Tabler
190+
input_tag <- tags$input(
191+
id = inputId,
192+
type = "checkbox",
193+
class = "form-check-input"
194+
)
195+
196+
# Confirms if the switch needs to be active by default
197+
if (!is.null(value) && value) {
198+
input_tag <- tagAppendAttributes(input_tag, checked = "checked")
199+
}
200+
201+
tagAppendChildren(
202+
input_wrapper,
203+
input_tag,
204+
span(class = "form-check-label", label)
205+
)
206+
}
207+
```
208+
209+
## Toggle Switch {-}
210+
211+
We also can create a update function which just a copy of `shiny::updateCheckboxInput` but more user friendly.
212+
213+
```r
214+
update_tabler_switch <- function (session, inputId, label = NULL, value = NULL) {
215+
message <- dropNulls(list(label = label, value = value))
216+
session$sendInputMessage(inputId, message)
217+
}
218+
```
219+
220+
**Let's RUN example 2**
221+
222+
## Navbar menu input {-}
223+
224+
To **capture the currently selected tab** to subsequently perform actions on the server side, updating the selected tab based on a button click.
225+
226+
1. Add an **id** attribute to `tabler_navbar_menu()`.
227+
228+
```r
229+
tabler_navbar_menu <- function(..., id = NULL) {
230+
tags$ul(
231+
id = id,
232+
class = "nav nav-pills navbar-nav",
233+
...
234+
)
235+
}
236+
```
237+
238+
2. Create the JS `navbarMenuBinding` looking for the `navbar-nav` class in the `find` method.
239+
240+
```javascript
241+
find: function(scope) {
242+
return $(scope).find('.navbar-nav');
243+
}
244+
```
245+
246+
## Navbar menu input {-}
247+
248+
3. Define the `initialize` method.
249+
250+
```javascript
251+
initialize: function(el) {
252+
253+
// Construct the jQuery selector for the container element by its ID
254+
let menuId = '#' + $(el).attr('id');
255+
256+
// Find the currently active tab within the container
257+
let activeTab = $(`${menuId} .nav-link.active`);
258+
259+
// If at least one active tab is found
260+
if (activeTab.length > 0) {
261+
// Get the associated tab content's ID from the 'data-value' attribute
262+
let tabId = $(activeTab).attr('data-value');
263+
264+
// Activate the tab using Bootstrap's tab API
265+
// Remove the .active from other tabs
266+
$(activeTab).tab('show');
267+
268+
// Make sure the corresponding tab content is also visible
269+
$(`#${tabId}`).addClass('show active');
270+
271+
} else {
272+
273+
// If no active tab is found, activate the first tab in the menu
274+
$(`${menuId} .nav-link`)
275+
.first()
276+
.tab('show');
277+
278+
}
279+
}
280+
281+
```
282+
283+
## Navbar menu input {-}
284+
285+
4. Define `getValue` is to return the currently selected tab.
286+
287+
```javascript
288+
getValue: function(el) {
289+
let activeTab = $(el).find('a').filter('nav-link active');
290+
return $(activeTab).attr('data-value');
291+
}
292+
```
293+
294+
```r
295+
tabler_navbar_menu_item <- function(text, tabName,
296+
icon = NULL,
297+
selected = FALSE) {
298+
299+
item_cl <- paste0("nav-link", if(selected) " active")
300+
301+
tags$li(
302+
class = "nav-item",
303+
a(
304+
class = item_cl,
305+
`data-value` = tabName,
306+
# Commented since not relevant
307+
)
308+
)
309+
}
310+
```
311+
312+
## Navbar menu input {-}
313+
314+
5. Define `setValue` to update the active tab based on the Bootstrap method.
315+
316+
```javascript
317+
setValue: function(el, value) {
318+
let hrefVal = '#' + value;
319+
let menuId = $(el).attr('id');
320+
$(`#${menuId} a[data-target="${hrefVal}"]`).tab('show');
321+
}
322+
```
323+
324+
## Navbar menu input {-}
325+
326+
6. Create an update function and the `receiveMessage` method to change the tab.
327+
328+
```r
329+
update_tabler_tab_item <- function(
330+
inputId,
331+
value,
332+
session = getDefaultReactiveDomain()
333+
) {
334+
session$sendInputMessage(inputId, message = value)
335+
}
336+
```
337+
338+
339+
```javascript
340+
receiveMessage: function(el, data) {
341+
this.setValue(el, data);
342+
}
343+
```
344+
## Navbar menu input {-}
345+
346+
7. Define the `subscribe` method to tell Shiny **when to change** the current input value. Here it's the events applied by Bootstrap in order:
347+
348+
- `hide.bs.tab` (on the current active tab).
349+
- `show.bs.tab` (on the to-be-shown tab).
350+
- `hidden.bs.tab` (on the previous active tab).
351+
- `shown.bs.tab` (on the newly-active just-shown tab).
352+
353+
```javascript
354+
subscribe: function(el, callback) {
355+
// important to use shown.bs.tab and not show.bs.tab!
356+
$(el).on('shown.bs.tab.navbarMenuBinding', function(e) {
357+
callback();
358+
});
359+
},
360+
361+
unsubscribe: function(el) {
362+
$(el).off('.navbarMenuBinding');
363+
}
364+
```
365+
366+
367+
## Navbar menu input {-}
368+
369+
8. Include this custom input binding.
370+
371+
```r
372+
tabler_custom_js <- htmlDependency(
373+
name = "tabler-bindings",
374+
version = "1.0.7",
375+
src = "tabler",
376+
package = "OSUICode",
377+
script = "input-bindings/navbarMenuBinding.js"
378+
)
379+
```
380+
381+
```r
382+
add_tabler_deps <- function(tag) {
383+
# below, the order is of critical importance!
384+
deps <- list(bs4_deps, tablers_deps, tabler_custom_js)
385+
attachDependencies(tag, deps, append = TRUE)
386+
}
387+
```
388+
389+
**Let's RUN example 3**
8390

9-
- ADD SLIDES AS SECTIONS (`##`).
10-
- TRY TO KEEP THEM RELATIVELY SLIDE-LIKE; THESE ARE NOTES, NOT THE BOOK ITSELF.
11391

12392
## Meeting Videos
13393

0 commit comments

Comments
 (0)