Open big-camel opened 12 months ago
const RENDERER_TO_VIRTUAL = new WeakMap<Renderer, ReturnType<typeof directive>>();
function virtual(renderer: Renderer) {
const virtualDirective = RENDERER_TO_VIRTUAL.get(renderer);
if(virtualDirective) return virtualDirective;
class VirtualDirective extends AsyncDirective {
...
update(part: ChildPart, args: DirectiveParameters<this>) {
...
// teardownOnRemove(this.cont, part);
...
}
protected disconnected(): void {
if(this.cont) {
this.cont.teardown();
this.cont = undefined;
}
}
}
const v = directive(VirtualDirective);
RENDERER_TO_VIRTUAL.set(renderer, v);
return v;
}
The virtual.test test file also runs with this solution
For the disconnected
function to work in virtual
it needs to be called from an ancestor somewhere in the tree.
For component
this does not happen and you can end up with cases where the cleanup of effects in descendant virtual
does not run.
First component
needs to call disconnect on the template result in its disconnectedCallback.
Watching an actual element is safer ...
For the
disconnected
function to work invirtual
it needs to be called from an ancestor somewhere in the tree. Forcomponent
this does not happen and you can end up with cases where the cleanup of effects in descendantvirtual
does not run. Firstcomponent
needs to call disconnect on the template result in its disconnectedCallback. Watching an actual element is safer ...
My understanding is that the disconnected
method is called by lit-html/async-directive.js
, and the virtual
component functions as a custom directive
. In my test case, the disconnected
callback is always executed correctly.
I am unable to reproduce the scenario you described. Could you please provide a test case for me? Thank you.
I will provide a better example at a some later point but basically:
https://github.com/matthewp/haunted/issues/475#issuecomment-1827963683 On the basis of the above code, here is my component and the runtime result:
Additionally, I am using lit-html@3.1.0
export const VirtualButton = virtual(() => {
useEffect(() => {
console.log('effect')
return () => {
console.log('effect-clear')
}
}, [])
return html`<button>button</button>`
})
const MyApp = () => {
const [count, setCount] = useState(0)
return html`<div>
<div>${count % 2 === 0 ? VirtualButton() : 'None'}</div>
</div>`
}
output
effect
effect-clear
Yes! this works. But does it work if you unmount/detach the my-app element ? from web inspector.
Understood. If we only consider the unmounting of the component
, it is feasible to collect all child nodes' virtual components
using a WeakMap
. Then, we can handle them uniformly in the disconnectedCallback
of the component
. Does this approach seem feasible to you?
If we handle this correctly by
render
in this.renderResult.in component
then we might not need the MutationObserver and teardownOnRemove.
I don't know if we actually need to have a map of parts or schedulers . To me it looks like disconnected
will be called by the AsyncDirective's parent's and in it we can just call cont.teardown().
Additionally we could also create the scheduler once in the constructor like its done in component
.
A good example somewhat similar to virtual
is the example implementation of a ObservableDirective in lit docs.
https://lit.dev/docs/templates/custom-directives/#async-directives
I've written some code, primarily to collect AsyncDirectives
under the component
. Now, when removing the component
, the virtual component
can correctly trigger the disconnected
method.
const virtualDirectives = new WeakMap<HTMLComponent<P>, Set<typeof Directive>>()
const setVirtualDirectives = (values: unknown[]): void => {
for (const value of values) {
if (isDirectiveResult(value)) {
const directive = getDirectiveClass(value)
if (!directive) continue
const directives = virtualDirectives.get(component) || new Set()
directives.add(directive)
virtualDirectives.set(component, directives)
// @ts-ignore
setVirtualDirectives(value.values ?? [])
}
}
}
component scheduler render
scheduler.render = () => {
const result = scheduler.state.run(() => renderer.call(component, component))
if (isTemplateResult(result)) {
setVirtualDirectives(result.values)
} else if (isDirectiveResult(result)) {
setVirtualDirectives([result])
} else if (Array.isArray(result)) {
setVirtualDirectives(result)
}
console.log(result,virtualDirectives)
return result
}
component scheduler teardown
const superTeardown = scheduler.teardown
scheduler.teardown = (): void => {
const directives = virtualDirectives.get(component)
if (directives) {
for (const directive of directives) {
// @ts-ignore
const instance = VIRTUAL_CLASS_TO_INSTANCE.get(directive)
if (instance) {
// @ts-ignore
instance['_$notifyDirectiveConnectionChanged'](false)
}
}
}
superTeardown()
}
Now it meets the unmounting requirement for releasing the virtual component at the root node.
However, in a structure like the one below, where the
useEffect(() => {
// Removing <div> using the DOM
divRef.remove();
}, [])
return html`<div ${ref(divRef)}>${VirtualComponent()}</div>`
I believe there should be a convention here, discouraging the direct use of the DOM to manipulate nodes rendered from HTML templates. What do you think?
I am using this patch to detach on component disconnect:
diff --git a/node_modules/haunted/lib/component.js b/node_modules/haunted/lib/component.js
index fae37bb..702cd4b 100644
--- a/node_modules/haunted/lib/component.js
+++ b/node_modules/haunted/lib/component.js
@@ -3,12 +3,13 @@ const toCamelCase = (val = '') => val.replace(/-+([a-z])?/g, (_, char) => char ?
function makeComponent(render) {
class Scheduler extends BaseScheduler {
frag;
+ renderResult;
constructor(renderer, frag, host) {
super(renderer, (host || frag));
this.frag = frag;
}
commit(result) {
- render(result, this.frag);
+ this.renderResult = render(result, this.frag);
}
}
function component(renderer, baseElementOrOptions, options) {
@@ -31,9 +32,11 @@ function makeComponent(render) {
}
connectedCallback() {
this._scheduler.update();
+ this._scheduler.renderResult?.setConnected(true)
}
disconnectedCallback() {
this._scheduler.teardown();
+ this._scheduler.renderResult?.setConnected(false)
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue === newValue) {
Great! I'll take it with me.
https://github.com/matthewp/haunted/blob/main/src/virtual.ts#L59 Why don't you use the disconnected method here?
Is there more performance consumption with MutationObserver?