svelte
Version:
Cybernetically enhanced web apps
79 lines (70 loc) • 2.91 kB
JavaScript
/** @import { ComponentContext, Effect } from '#client' */
import { DESTROYING, STATE_SYMBOL } from '#client/constants';
import { component_context } from '../../../context.js';
import { effect, render_effect } from '../../../reactivity/effects.js';
import { active_effect, untrack } from '../../../runtime.js';
/**
* @param {any} bound_value
* @param {Element} element_or_component
* @returns {boolean}
*/
function is_bound_this(bound_value, element_or_component) {
return (
bound_value === element_or_component || bound_value?.[STATE_SYMBOL] === element_or_component
);
}
/**
* @param {any} element_or_component
* @param {(value: unknown, ...parts: unknown[]) => void} update
* @param {(...parts: unknown[]) => unknown} get_value
* @param {() => unknown[]} [get_parts] Set if the this binding is used inside an each block,
* returns all the parts of the each block context that are used in the expression
* @returns {void}
*/
export function bind_this(element_or_component = {}, update, get_value, get_parts) {
var component_effect = /** @type {ComponentContext} */ (component_context).r;
var parent = /** @type {Effect} */ (active_effect);
effect(() => {
/** @type {unknown[]} */
var old_parts;
/** @type {unknown[]} */
var parts;
render_effect(() => {
old_parts = parts;
// We only track changes to the parts, not the value itself to avoid unnecessary reruns.
parts = get_parts?.() || [];
untrack(() => {
if (element_or_component !== get_value(...parts)) {
update(element_or_component, ...parts);
// If this is an effect rerun (cause: each block context changes), then nullify the binding at
// the previous position if it isn't already taken over by a different effect.
if (old_parts && is_bound_this(get_value(...old_parts), element_or_component)) {
update(null, ...old_parts);
}
}
});
});
return () => {
// When the bind:this effect is destroyed, we go up the effect parent chain until we find the last parent effect that is destroyed,
// or the effect containing the component bind:this is in (whichever comes first). That way we can time the nulling of the binding
// as close to user/developer expectation as possible.
// TODO Svelte 6: Decide if we want to keep this logic or just always null the binding in the component effect's teardown
// (which would be simpler, but less intuitive in some cases, and breaks the `ondestroy-before-cleanup` test)
let p = parent;
while (p !== component_effect && p.parent !== null && p.parent.f & DESTROYING) {
p = p.parent;
}
const teardown = () => {
if (parts && is_bound_this(get_value(...parts), element_or_component)) {
update(null, ...parts);
}
};
const original_teardown = p.teardown;
p.teardown = () => {
teardown();
original_teardown?.();
};
};
});
return element_or_component;
}