-
I have a Widget that will start polling for data, using setInterval, I hope I can clearInterval when this widget is no longer used. But I found no such life cycle hook that I can clearInterval with, in https://github.com/Jermolene/TiddlyWiki5/blob/3094e062366830bdecfb91e3d852667fa951dc50/core/modules/widgets/widget.js#L24 So is there a way can do some clean-up when the widget is no longer used? |
Beta Was this translation helpful? Give feedback.
Replies: 4 comments 8 replies
-
Hi @linonetwo widgets are volatile, created and deleted as required to keep the display aligned with the store. Widgets are not intended for long term, async actions. Polling for data is best accomplished by posting a message event that is handled by a global custom startup handler. |
Beta Was this translation helpful? Give feedback.
-
@Jermolene I saw there is a I found I can do cleanup and unmount of react code in this method, but it sometimes is not called. When react is not properly unmounted, there can be bugs... I know, How to inform widget when edit and close? And when list or reveal changed? Seems there should have been a call to removeChildDomNodes to remove the node... Oh I see in the comment:
The parent widget (in this case is the storyriver's list widget, maybe) will directly remove child dom node from itself's childlist, and won't inform child widget, and just wait for GC. I'm going to PR a change to Made #6699 After this, we can do: class SomeReactWidget extends Widget {
removeChildDomNodes() {
super.removeChildDomNodes();
this.reactRoot?.unmount?.();
} |
Beta Was this translation helpful? Give feedback.
-
I was investigating custom html elements implemented via web components and I noticed they have lifecycle methods. One of which will automatically get called upon dom node destruction. It seems to me like a generic destroy-notify web component could be implemented. Anything like react widgets which need notification can be inserted as children of the destroy-notify dom node and a callback could be registered with that dom node. The callback can call the react cleanup. Here is a small example web component showing what I mean on the share site. The web component implemenation looks like this: class DestroyNotifierElement extends HTMLElement {
connectedCallback () {
this.innerHTML = 'hello, world!'
}
disconnectedCallback() {
alert('disconnected from the DOM');
}
}
customElements.define('destroy-notifier', DestroyNotifierElement); Imagine instead of calling alert, the disconnectCallback method calls the necessary react cleanup. This is how the custom component is invoked
At that share site link if you close the "use destroy-notifier web component" tiddler, then the alert will popup because the This works because it is standard web component behavior. Nothing special needs to be done by tiddlywiki. The browser automatically calls the cleanup method. This approach can be used without any changes to the Tiddlywiki core. |
Beta Was this translation helpful? Give feedback.
-
Update: use https://www.npmjs.com/package/@seznam/visibility-observer , which only add 6.09KB to each plugin. Usage import { widget as Widget } from '$:/core/modules/widgets/widget.js';
import { IChangedTiddlers, ITiddlerFields } from 'tiddlywiki';
import { observe, unobserve } from '@seznam/visibility-observer';
class CommandPaletteWidget extends Widget {
render(parent: Element, nextSibling: Element) {
this.parentDomNode = parent;
this.computeAttributes();
this.execute();
const containerElement = $tw.utils.domMaker('nav', {
class: 'tw-commandpalette-container',
});
parent.insertBefore(containerElement, nextSibling);
this.domNodes.push(containerElement);
observe(containerElement, this.onVisibilityChange.bind(this));
}
onVisibilityChange(
visibilityEntry: IntersectionObserverEntry & {
target: HTMLElement;
},
) {
if (!visibilityEntry.isIntersecting) {
this.destroy();
unobserve(visibilityEntry.target, this.onVisibilityChange.bind(this));
}
}
destroy() {
$tw.wiki.deleteTiddler('$:/state/commandpalette/default/opened');
this.modalCount = 0;
Modal.prototype.adjustPageClass.call(this);
}
}
declare let exports: {
['command-palette']: typeof CommandPaletteWidget;
};
exports['command-palette'] = CommandPaletteWidget; It is smaller than wessberg/connection-observer , and does not require a widget-loader. I found a more standard way is to use Before I try to write it myself, I search the npm and found tons of packages https://www.npmjs.com/search?q=removed%20element And I finally choose to use https://github.com/wessberg/connection-observer which have TS typing, and only add 41.8kB to the file. (3,706 k-3,664.2 k) The usage is like: import { widget as Widget } from '$:/core/modules/widgets/widget.js';
import type { Calendar } from '@fullcalendar/core';
import { ConnectionObserver } from '@wessberg/connection-observer';
import type { IChangedTiddlers } from 'tiddlywiki';
class CalendarWidget extends Widget {
#calendar?: Calendar;
#containerElement?: HTMLDivElement;
#mountElement?: HTMLDivElement;
connectionObserver = new ConnectionObserver(entries => {
for (const { connected } of entries) {
if (!connected) {
this.destroy();
this.connectionObserver?.disconnect?.();
}
}
});
/**
* Lifecycle method: Render this widget into the DOM
*/
render(parent: Element, _nextSibling: Element | null): void {
this.parentDomNode = parent;
this.computeAttributes();
this.execute();
if (this.#containerElement === undefined || this.#mountElement === undefined) {
this.connectionObserver.observe(this.parentDomNode);
//...
destroy() {
this.#calendar?.destroy();
}
//...
} Make sure to observer on parent's dom node that already existed on the dom tree Another example: import { ConnectionObserver } from '@wessberg/connection-observer';
import { IChangedTiddlers } from 'tiddlywiki';
import { getWasmContext, WasmContext } from './game/wasm/game';
import './index.css';
import { BasicGamificationEventTypes, IGamificationEvent } from 'src/tw-gamification/event-generator/GamificationEventTypes';
import { GameWidget } from 'src/tw-gamification/game-wiki-adaptor/GameWidgetType';
class ScpFoundationSiteDirectorGameWidget extends GameWidget {
wasmContext?: WasmContext;
gameInitialized = false;
connectionObserver?: ConnectionObserver;
refresh(_changedTiddlers: IChangedTiddlers) {
// noting should trigger game refresh (reloading), because it is self-contained. Game state change is triggered by calling method on wasm.
return false;
}
render(parent: Element, nextSibling: Element) {
this.parentDomNode = parent;
this.execute();
const canvasElement = $tw.utils.domMaker('canvas', {
class: 'tw-gamification-bevy-canvas scp-foundation-site-director',
});
const containerElement = $tw.utils.domMaker('div', {
class: 'tw-gamification-bevy-container',
children: [canvasElement],
});
this.connectionObserver = new ConnectionObserver(entries => {
// For each entry, print the connection state as well as the target node to the console
for (const { connected, target } of entries) {
console.log('target:', target);
console.log('connected:', connected);
// connected will be false when it first time created and not appended to parent DOM
if (!connected && this.gameInitialized) {
this.destroy();
this.connectionObserver?.disconnect?.();
}
}
});
// Observe 'someElement' for connectedness
this.connectionObserver.observe(canvasElement);
nextSibling === null ? parent.append(containerElement) : nextSibling.before(containerElement);
this.domNodes.push(containerElement);
// TODO: load assets from asset sub-plugin, and push list and item to game by call rust function
void this.startGame();
// TODO: handle destroy using https://github.com/Jermolene/TiddlyWiki5/discussions/5945#discussioncomment-8173023
}
private async startGame() {
this.setLoading(true);
await this.initializeGameCanvas();
if (this.gameInitialized) {
this.popGamificationEvents();
}
}
destroy(): void {
// DEBUG: console
console.log(`destroy`);
this.wasmContext?.stopGame?.();
this.wasmContext = undefined;
this.gameInitialized = false;
}
// ... To avoid ReferenceError: Element is not defined, you need to make sure widget only register on browser side: title: $:/plugins/linonetwo/scp-foundation-site-director/widget-loader.js
type: application/javascript
module-type: widget /**
* Fix ReferenceError: Element is not defined
* @url https://github.com/wessberg/connection-observer/issues/8
*/
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
(function whiteboardWidgetIIFE() {
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if (!$tw.browser) {
return;
}
// separate the widget from the exports here, so we can skip the require of react code if `!$tw.browser`. Those ts code will error if loaded in the nodejs side.
const components = require('$:/plugins/linonetwo/scp-foundation-site-director/game-widget.js');
const { ScpFoundationSiteDirectorGameWidget } = components;
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
exports.ScpFoundationSiteDirectorGameWidget = ScpFoundationSiteDirectorGameWidget;
})(); and actual widget use this meta title: $:/plugins/linonetwo/scp-foundation-site-director/game-widget.ts
type: application/javascript
module-type: library
hide-body: yes (based on https://github.com/tiddly-gittly/Modern.TiddlyDev ) |
Beta Was this translation helpful? Give feedback.
Update: use https://www.npmjs.com/package/@seznam/visibility-observer , which only add 6.09KB to each plugin.
Usage