diff --git a/src/manager.ts b/src/manager.ts index 69ed7d0..db723c2 100644 --- a/src/manager.ts +++ b/src/manager.ts @@ -18,11 +18,13 @@ import { WidgetModel, WidgetView, IClassicComm } from '@jupyter-widgets/base'; import * as base from '@jupyter-widgets/base'; import * as controls from '@jupyter-widgets/controls'; -import {ManagerBase} from '@jupyter-widgets/base-manager'; +import { ManagerBase } from '@jupyter-widgets/base-manager'; import { JSONObject } from '@lumino/coreutils'; -import {Loader} from './amd'; +import { Widget } from '@lumino/widgets'; + +import { Loader } from './amd'; import { IComm, IWidgetManager, WidgetEnvironment } from './api'; -import {swizzle} from './swizzle'; +import { swizzle } from './swizzle'; export class Manager extends ManagerBase implements IWidgetManager { private readonly models = new Map>(); @@ -135,13 +137,9 @@ export class Manager extends ManagerBase implements IWidgetManager { conflate: () => false, }); - container.appendChild(view.el); - - view.luminoWidget.processMessage({ - type: 'after-attach', - isConflatable: false, - conflate: () => false, - }); + const lifecycleAdapter = new LuminoLifecycleAdapter(view.luminoWidget); + lifecycleAdapter.appendChild(view.el); + container.appendChild(lifecycleAdapter); } } @@ -221,4 +219,44 @@ class ClassicComm implements IClassicComm { get comm_id() { return this.id; } +} + +/** + * Custom element to provide Lumino lifecycle events driven by native DOM + * events. + */ +class LuminoLifecycleAdapter extends HTMLElement { + constructor(private readonly widget?: Widget) { + super(); + } + connectedCallback() { + if (this.widget) { + this.widget.processMessage({ + type: 'after-attach', + isConflatable: false, + conflate: () => false, + }); + } + } + disconnectedCallback() { + if (this.widget) { + // We don't have a native event for before-detach, so just fire before + // the after-detach. + this.widget.processMessage({ + type: 'before-detach', + isConflatable: false, + conflate: () => false, + }); + this.widget.processMessage({ + type: 'after-detach', + isConflatable: false, + conflate: () => false, + }) + } + } +} +try { + window.customElements.define('colab-lumino-adapter', LuminoLifecycleAdapter); +} catch (error: unknown) { + // May have already been defined. } \ No newline at end of file diff --git a/test/manager.spec.js b/test/manager.spec.js index 7e9c447..2efb555 100644 --- a/test/manager.spec.js +++ b/test/manager.spec.js @@ -84,4 +84,56 @@ describe('widget manager', () => { await new Promise((resolve)=> setTimeout(resolve, 100)); window.onerror = oldHandler; }); + + it('has proper lifecycle events', async () => { + const provider = new FakeState({ + '123': { + state: { + "_view_module": "custom-widget", + "_view_name": "View", + }, + "model_module": "custom-widget", + "model_name": "Model", + } + }); + const manager = createWidgetManager(provider); + let modelClass; + let viewClass; + + manager.loader.define('custom-widget', ['@jupyter-widgets/base'], (base) => { + class Model extends base.DOMWidgetModel { + constructor(...args) { + super(...args); + } + } + class View extends base.DOMWidgetView { + constructor(...args) { + super(...args); + this.hasBeenDisplayed = false; + this.displayed.then(() => { + this.hasBeenDisplayed = true; + }); + } + } + modelClass = Model; + viewClass = View; + + return { + Model, + View, + } + }); + + container.remove(); + + await manager.render('123', container); + const model = await manager.get_model('123'); + expect(model).toBeInstanceOf(modelClass); + const view = await Object.values(model.views)[0]; + expect(view).toBeInstanceOf(viewClass); + expect(view.hasBeenDisplayed).toBe(false); + document.body.appendChild(container); + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(view.hasBeenDisplayed).toBe(true); + }); }); \ No newline at end of file