-
Notifications
You must be signed in to change notification settings - Fork 58
/
shiny-input-gems.Rmd
162 lines (121 loc) · 7.53 KB
/
shiny-input-gems.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
# Mastering Shiny's events {#shiny-input-gems}
We've already seen a couple of Shiny JS __events__ since the beginning of this book. You may know the `shiny:connected`, meaning that the client and server are properly initialized and all
internal methods/functions are available to the programmer. Below, we add more elements to the list, trying to
give practical examples and see how it can significantly improve your apps. If you ever used the `{waiter}` [@R-waiter] package by John Coene, know that it heavily relies on some Shiny's events (Figure \@ref(fig:waiter-preloader)).
```{r waiter-preloader, echo=FALSE, fig.cap='{waiter} preloader significantly improves the perceived app performance and user experience.', out.width='100%', fig.align='center'}
knitr::include_graphics("images/survival-kit/waiter-preloader.png")
```
## Get the last changed input
### Motivations
We probably all had this question one day: How can I get the __last changed__ input in a Shiny app? There are already some methods like this [one](https://stackoverflow.com/questions/31250587/creating-shiny-reactive-variable-that-indicates-which-widget-was-last-modified):
```{r, echo=FALSE, results='asis'}
code_chunk(OSUICode::get_example("shiny-events/get-last-changed"), "r")
```
Shouldn't this be easier? Could we do that from the client instead, thereby reducing the server load?
### Invoke JS events
`shiny:inputchanged` is the event we are looking for. It is fired each time an input gets a new value. The related event has five properties:
- __name__, the event name.
- __value__, the new value.
- __inputType__, the input type.
- __binding__, the related input binding.
- __el__, the related input DOM element.
You may try below:
```{r, echo=FALSE, results='asis'}
code_chunk(OSUICode::get_example("shiny-events/get-input-changed"), "r")
```
Changing the `textInput()` value fires the event as shown Figure \@ref(fig:input-changed-event).
```{r input-changed-event, echo=FALSE, fig.cap='Inspect the input-changed event in the JS console.', out.width='100%'}
knitr::include_graphics("images/survival-kit/input-changed-event.png")
```
Contrary to what is mentioned in the online [documentation](https://shiny.rstudio.com/articles/js-events.html), __inputType__ does not always have a value. In this case, an alternative, is to access the related input binding and extract its name, as illustrated by Figure \@ref(fig:input-changed-event-zoom) and in the following code:
```{r, echo=FALSE, results='asis'}
js_code <- "$(document).on('shiny:inputchanged', function(event) {
Shiny.setInputValue(
'pleaseStayHome',
{
name: event.name,
value: event.value,
type: event.binding.name.split('.')[1]
}
);
});"
code_chunk_custom(js_code, "js")
```
If you use this code in a custom Shiny template, it is possible that input bindings doesn't have a name, which would thereby make `event.binding.name.split('.')[1]` crash, `event.binding` being undefined.
```{r, echo=FALSE, results='asis'}
code_chunk(OSUICode::get_example("shiny-events/get-input-changed-info"), "r")
```
(ref:input-changed-event-zoom-caption) Extract input-changed event's most relevant elements.
```{r input-changed-event-zoom, echo=FALSE, fig.cap='(ref:input-changed-event-zoom-caption)', out.width='50%', fig.align='center'}
knitr::include_graphics("images/survival-kit/input-changed-event-zoom.png")
```
::: {.warningblock data-latex=""}
For the `textInput()`, the event is also fired when moving the mouse cursor with the keyboard arrows, which is a sort of false positive, since the value isn't changed. However, as `Shiny.setInputValue` only sets a new value when the input value really changed (unless the __priority__ is set to __event__), we avoid this edge case. As an exercise, you may try to add `{priority: 'event'}` to the above code.
:::
`$(document).on('shiny:inputchanged')` is also cancellable, that is we may definitely prevent the input from changing its value, calling `event.preventDefault();`, as depicted in Figure \@ref(fig:input-changed-freeze).
```{r, echo=FALSE, results='asis'}
code_chunk(OSUICode::get_example("shiny-events/freeze-input-change"), "r")
```
```{r input-changed-freeze, echo=FALSE, fig.cap='Cancel input update on the client.', out.width='50%', fig.align='center'}
knitr::include_graphics("images/survival-kit/input-changed-freeze.png")
```
### Practical example
`{shinyMobile}` natively implements this feature that may be accessed with `input$lastInputChanged`.
```{r, echo=FALSE, results='asis'}
code_chunk(OSUICode::get_example("shiny-events/get-last-changed-shinyMobile"), "r")
```
This approach has the advantage of not overloading the server part with complex logic.
### About `{shinylogs}`
The `{shinylogs}` [@R-shinylogs] package developed by [dreamRs](https://github.com/dreamRs/shinylogs) provides this feature with much more advanced options, such as a history of past values, as demonstrated in Figure \@ref(fig:input-changed-shinylogs).
```{r, echo=FALSE, results='asis'}
code_chunk(OSUICode::get_example("shiny-events/get-last-changed-shinylogs"), "r")
```
```{r input-changed-shinylogs, echo=FALSE, fig.cap='{shinylogs} allows real-time input tracking and storage for analytics purposes.', out.width='100%'}
knitr::include_graphics("images/survival-kit/input-changed-shinylogs.png")
```
## Custom overlay screens
If you ever designed corporate production apps, you probably faced this situation where clients wanted
a loading screen whenever a computation occurs or at the start.
To date, one of the most comprehensive alternatives is the `{waiter}` package.
It provides myriad options to significantly enhance the perceived performance of your app.
In the following, we'll focus on the `waiter_preloader()` and `waiter_on_busy()` functions. How does this work?
### Preloader
Under the hood, this feature relies on the `shiny:idle` event. When the app starts, `shiny:idle` is triggered just after `shiny:connected` and `shiny:sessioninitialized`. `shiny:idle` is also called each time a computation cycle is finished, that is each time an input is changed and the related output is re-rendered.
Whenever we call `waiter_preloader()`, an HTML overlay is added in the DOM. Moreover, this extra JS code ensures hiding the waiter when Shiny is ready:
```{r, echo=FALSE, results='asis'}
js_code <- "window.ran = false;
$(document).on('shiny:idle', function(event){
if(!window.ran)
hide_waiter(id = null);
window.ran = true;
});"
code_chunk_custom(js_code, "js")
```
As a security, `window.ran` prevents us from running this code twice. As an example, consider this app with
a slider input and a plot output. We simulated a delay of 3 s to produce the plot.
```{r, echo=FALSE, results='asis'}
code_chunk(OSUICode::get_example("shiny-events/waiter-on-load"), "r")
```
Notice how the waiter correctly handles the plot processing time.
### Load on busy
Similarly, the `waiter_on_busy()` exploits the `shiny:idle` and `shiny:busy` events. Each time an output is invalidated, `shiny:busy` is fired, which triggers the recalculation until the next `shiny:idle` event. The loader is shown as soon as Shiny is busy:
```{r, echo=FALSE, results='asis'}
js_code <- "$(document).on('shiny:busy', function(event) {
show_waiter(
id = null,
html = ...,
color = ...
);
});"
code_chunk_custom(js_code, "js")
```
and is hidden once Shiny is done:
```{r, echo=FALSE, results='asis'}
js_code <- "$(document).on('shiny:idle', function(event) {
hide_waiter(null);
});"
code_chunk_custom(js_code, "js")
```
```{r, echo=FALSE, results='asis'}
code_chunk(OSUICode::get_example("shiny-events/waiter-on-busy"), "r")
```