@@ -9,6 +9,7 @@ import type { ErrorsMessageValue } from 'rstudio-shiny/srcts/types/src/shiny/shi
9
9
******************************************************************************/
10
10
11
11
class OutputManager extends HTMLManager {
12
+ orphaned_models : string [ ] ;
12
13
// In a soon-to-be-released version of @jupyter-widgets/html-manager,
13
14
// display_view()'s first "dummy" argument will be removed... this shim simply
14
15
// makes it so that our manager can work with either version
@@ -91,20 +92,31 @@ class IPyWidgetOutput extends Shiny.OutputBinding {
91
92
if ( fill ) el . classList . add ( "forward-fill-potential" ) ;
92
93
93
94
// At this time point, we should've already handled an 'open' message, and so
94
- // the model should be ready to use
95
+ // the model should already be registered
95
96
const model = await manager . get_model ( data . model_id ) ;
96
97
if ( ! model ) {
97
98
throw new Error ( `No model found for id ${ data . model_id } ` ) ;
98
99
}
99
100
101
+ // Create a view and display it
100
102
const view = await manager . create_view ( model , { } ) ;
101
103
await manager . display_view ( view , { el : el } ) ;
102
104
103
- // Don't allow more than one .lmWidget container, which can happen
104
- // when the view is displayed more than once
105
- // TODO: It's probably better to get view(s) from m.views and .remove() them
106
- while ( el . childNodes . length > 1 ) {
107
- el . removeChild ( el . childNodes [ 0 ] ) ;
105
+ // If the model was orphaned, close it (and thus, the view as well) now
106
+ if ( manager . orphaned_models . length > 0 ) {
107
+ for ( const model_id of manager . orphaned_models ) {
108
+ const model = await manager . get_model ( model_id ) ;
109
+ if ( model ) {
110
+ // Closing the model removes the previous view
111
+ await model . close ( ) ;
112
+ // .close() isn't enough to remove manager's reference to it,
113
+ // and apparently the only way to remove it is through the `comm:close` event
114
+ // https://github.com/jupyter-widgets/ipywidgets/blob/303cae4/packages/base-manager/src/manager-base.ts#L330-L337
115
+ // https://github.com/jupyter-widgets/ipywidgets/blob/303cae4/packages/base/src/widget.ts#L251-L253
116
+ model . trigger ( "comm:close" ) ;
117
+ }
118
+ }
119
+ manager . orphaned_models = [ ] ;
108
120
}
109
121
110
122
// The ipywidgets container (.lmWidget)
@@ -189,21 +201,34 @@ Shiny.addCustomMessageHandler("shinywidgets_comm_open", (msg_txt) => {
189
201
// Basically out version of https://github.com/jupyterlab/jupyterlab/blob/d33de15/packages/services/src/kernel/default.ts#L1200-L1215
190
202
Shiny . addCustomMessageHandler ( "shinywidgets_comm_msg" , ( msg_txt ) => {
191
203
const msg = jsonParse ( msg_txt ) ;
192
- manager . get_model ( msg . content . comm_id ) . then ( m => {
204
+ const id = msg . content . comm_id ;
205
+ const model = manager . get_model ( id ) ;
206
+ if ( ! model ) {
207
+ console . error ( `Couldn't handle message for model ${ id } because it doesn't exist.` ) ;
208
+ return ;
209
+ }
210
+ model . then ( m => {
193
211
// @ts -ignore for some reason IClassicComm doesn't have this method, but we do
194
212
m . comm . handle_msg ( msg ) ;
195
213
} ) ;
196
214
} ) ;
197
215
198
- // TODO: test that this actually works
216
+
217
+ // When `widget.close()` happens server-side, don't `.close()` client-side until the
218
+ // next render. This is because we currently trigger a close _during_ the server-side
219
+ // render, and thus, immediately closing the model removes the view before a new one is
220
+ // ready.
221
+ manager . orphaned_models = [ ] ;
199
222
Shiny . addCustomMessageHandler ( "shinywidgets_comm_close" , ( msg_txt ) => {
200
223
const msg = jsonParse ( msg_txt ) ;
201
- manager . get_model ( msg . content . comm_id ) . then ( m => {
202
- // @ts -ignore for some reason IClassicComm doesn't have this method, but we do
203
- m . comm . handle_close ( msg )
204
- } ) ;
224
+ manager . orphaned_models . push ( msg . content . comm_id ) ;
205
225
} ) ;
206
226
227
+ // At least currently, all widgets must be created within a session scope, so we can
228
+ // clear the state (i.e., .close() all the widget models) when the session ends
229
+ $ ( document ) . on ( "shiny:disconnected" , ( ) => {
230
+ manager . clear_state ( ) ;
231
+ } ) ;
207
232
208
233
// Our version of https://github.com/jupyter-widgets/widget-cookiecutter/blob/9694718/%7B%7Bcookiecutter.github_project_name%7D%7D/js/lib/extension.js#L8
209
234
function setBaseURL ( x : string = '' ) {
0 commit comments