-
Notifications
You must be signed in to change notification settings - Fork 58
/
shiny-input-lifecycle.Rmd
197 lines (158 loc) · 9.12 KB
/
shiny-input-lifecycle.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
# Shiny inputs lifecycles {#shiny-input-lifecycle}
In the following, we provide an integrated view of the Shiny input system by summarizing all
mechanisms seen since Chapter \@ref(shiny-intro).
## App initialization
When a Shiny app starts, Shiny runs `initShiny` on the client. This JS [function](https://github.com/rstudio/shiny/blob/60db1e02b03d8e6fb146c9bb1bbfbce269231add/srcjs/init_shiny.js) has three main tasks:
- __Bind__ all inputs and outputs.
- __Initialize__ all inputs (if necessary) with `initializeInputs`.
- Initialize the client __websocket__ connection mentioned in Chapter \@ref(shiny-intro) and send __initial values__ to the server.
Most input bindings are, in principle, bundled in the `{shiny}` package. Some may be user-defined like in `{shinyMobile}` or even in a simple Shiny app. In any case, they are all contained in a __binding registry__, namely `inputBindings` built on top the following class:
```{r, echo=FALSE, results='asis'}
js_code <- "var BindingRegistry = function() {
this.bindings = [];
this.bindingNames = {};
}"
code_chunk_custom(js_code, "js")
```
This class has a method to `register` a binding. This method is executed when calling `Shiny.inputBindings.register(myBinding, 'reference');`, which appends the newly created binding to the bindings array.
When Shiny starts, it has to find all defined bindings with the `getBindings` method.
Once done, for each binding, `find` is triggered. If no corresponding element is found in the DOM, nothing is done. For each found input, the following methods are triggered:
- `getId` returns the input id. This ensures the uniqueness and is critical.
- `getType` optionally handles any `registerInputHandler` defined by the user on the R side. A detailed example is shown in section \@ref(custom-data-format).
- `getValue` gets the initial input value.
- `subscribe` registers event listeners driving the input behavior.
The data attribute `shiny-input-binding` is then added. This allows Shiny to access the input binding methods from the client, as shown in section \@ref(update-binding-client). The `shiny-bound-input` class is added, the corresponding input is appended to the `boundInputs` object (listing all bound inputs) and `shiny:bound` triggered on the client. As a side note, if you recall the `Shiny.unbinAll()` method from sections \@ref(input-binding) and \@ref(edit-input-binding), it triggers the `shiny:unbound` event for all inputs as well as removes them from the `boundInputs` registry.
Once done, Shiny stores all initial values in a variable `initialInput`, also containing all [client data](https://shiny.rstudio.com/articles/client-data.html) and passes them to the `Shinyapp.connect` method. As shown in Chapter \@ref(shiny-intro), the latter opens the client __websocket__ connection, raises the `shiny:connected` event and sends all values to the server (R). A few time after, `shiny:sessioninitialized` is triggered.
```{r init-shiny-recap, echo=FALSE, fig.cap='What Shiny does client side on initialization.', out.width='50%', fig.align='center'}
knitr::include_graphics("images/survival-kit/init-shiny-recap.png")
```
In Chapter \@ref(shiny-intro), we briefly described the `Shiny` JavaScript object. As an exercise, let's explore what the `Shiny.shinyApp` object contains. The definition is located in the shinyapps.js [script](https://github.com/rstudio/shiny/blob/60db1e02b03d8e6fb146c9bb1bbfbce269231add/srcjs/shinyapp.js).
```{r, echo=FALSE, results='asis'}
js_code <- "var ShinyApp = function() {
this.$socket = null;
// Cached input values
this.$inputValues = {};
// Input values at initialization (and reconnect)
this.$initialInput = {};
// Output bindings
this.$bindings = {};
// Cached values/errors
this.$values = {};
this.$errors = {};
// Conditional bindings
// (show/hide element based on expression)
this.$conditionals = {};
this.$pendingMessages = [];
this.$activeRequests = {};
this.$nextRequestId = 0;
this.$allowReconnect = false;
};"
code_chunk_custom(js_code, "js")
```
It creates several properties; some of them are easy to guess like `inputValues` or `initialInput`. Let's run the example below and open the HTML inspector. Notice that the `sliderInput` is set to 500 at `t0` (initialization):
<!-- NOT deploy: not informative just for test -->
```{r, eval=FALSE}
ui <- fluidPage(
sliderInput(
"obs",
"Number of observations:",
min = 0,
max = 1000,
value = 500
),
plotOutput("distPlot")
)
server <- function(input, output, session) {
output$distPlot <- renderPlot({
hist(rnorm(input$obs))
})
}
shinyApp(ui, server)
```
Figure \@ref(fig:shiny-initial-inputs) shows how to access Shiny's initial input value with `Shiny.shinyapp.$initialInput.obs`. After changing the slider position, its value is given by `Shiny.shinyapp.$inputValues.obs`. `$initialInput` and `$inputValues` contain many more elements, however we are only interested in the slider function in this example.
```{r shiny-initial-inputs, echo=FALSE, fig.cap='Explore initial input values.', out.width='100%'}
knitr::include_graphics("images/survival-kit/shiny-init-input.png")
```
## Update input {#update-input-lifecycle}
Below we try to explain what are the mechanisms to update an input from the server on the client. As stated previously, it all starts with an `update<name>Input` function call, which actually sends a message through the current __session__. This message is received by the client __websocket__ message manager:
```{r, echo=FALSE, results='asis'}
js_code <- "socket.onmessage = function(e) {
self.dispatchMessage(e.data);
};"
code_chunk_custom(js_code, "js")
```
which sends the message to the appropriate [handler](https://github.com/rstudio/shiny/blob/60db1e02b03d8e6fb146c9bb1bbfbce269231add/srcjs/shinyapp.js#L552), that is `inputMessages`:
```{r, echo=FALSE, results='asis'}
js_code <- "addMessageHandler('inputMessages', function(message) {
// inputMessages should be an array
for (var i = 0; i < message.length; i++) {
var $obj = $('.shiny-bound-input#' + $escape(message[i].id));
var inputBinding = $obj.data('shiny-input-binding');
// Dispatch the message to the appropriate input object
if ($obj.length > 0) {
var el = $obj[0];
var evt = jQuery.Event('shiny:updateinput');
evt.message = message[i].message;
evt.binding = inputBinding;
$(el).trigger(evt);
if (!evt.isDefaultPrevented())
inputBinding.receiveMessage(el, evt.message);
}
}
});"
code_chunk_custom(js_code, "js")
```
In short, it gets the `inputId` and accesses the corresponding input binding. Then it triggers the `shiny:updateinput` [event](https://shiny.rstudio.com/articles/js-events.html) and calles the input binding `receiveMessage` method. This fires `setValue` and `subscribe`. The way `subscribe` works is not really well covered in the official [documentation](https://shiny.rstudio.com/articles/building-inputs.html).
The `callback` function is actually defined during the initialization [process](https://github.com/rstudio/shiny/blob/60db1e02b03d8e6fb146c9bb1bbfbce269231add/srcjs/init_shiny.js#L128):
```{r, echo=FALSE, results='asis'}
js_code <- "function valueChangeCallback(binding, el, allowDeferred) {
var id = binding.getId(el);
if (id) {
var value = binding.getValue(el);
var type = binding.getType(el);
if (type)
id = id + ':' + type;
let opts = {
priority: allowDeferred ? 'deferred' : 'immediate',
binding: binding,
el: el
};
inputs.setInput(id, value, opts);
}
}"
code_chunk_custom(js_code, "js")
```
`valueChangeCallback` ultimately calls `inputs.setInput(id, value, opts)`. The latter involves a rather complex chain of [reactions](https://github.com/rstudio/shiny/blob/60db1e02b03d8e6fb146c9bb1bbfbce269231add/srcjs/input_rate.js) (which is not described here). It is important to understand that the client does not send input values one by one, but by batch:
```{r, echo=FALSE, results='asis'}
code_chunk(OSUICode::get_example("inputs-lifecycle/event-message"), "r")
```
Overall, the result is stored in a queue, namely `pendingData`, and sent to the server with `shinyapp.sendInput`:
```{r, echo=FALSE, results='asis'}
js_code <- "this.sendInput = function(values) {
var msg = JSON.stringify({
method: 'update',
data: values
});
this.$sendMsg(msg);
$.extend(this.$inputValues, values);
// ....; Extra code removed
}"
code_chunk_custom(js_code, "js")
```
The message has an `update` tag and is sent through the client __websocket__, only if the connection is opened. If not, it is added to the list of pending messages.
```{r, echo=FALSE, results='asis'}
js_code <- "this.$sendMsg = function(msg) {
if (!this.$socket.readyState) {
this.$pendingMessages.push(msg);
}
else {
this.$socket.send(msg);
}
};"
code_chunk_custom(js_code, "js")
```
Finally, current `inputValues` are updated. On the server side, the new value is received
by the server __websocket__ message handler, that is `ws$onMessage(message)`.
```{r update-input-recap, echo=FALSE, fig.cap='What Shiny does upon input update.', out.width='50%', fig.align='center'}
knitr::include_graphics("images/survival-kit/update-input-recap.png")
```