diff --git a/.changeset/purple-eagles-enjoy.md b/.changeset/purple-eagles-enjoy.md new file mode 100644 index 0000000000..318f85a946 --- /dev/null +++ b/.changeset/purple-eagles-enjoy.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: use symbols for encapsulated event delegation diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/OnDirective.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/OnDirective.js index 0ee3b0fb10..011d851023 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/OnDirective.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/OnDirective.js @@ -34,5 +34,5 @@ export function OnDirective(node, context) { node.modifiers.includes('passive') || (node.modifiers.includes('nonpassive') ? false : undefined); - return build_event(node.name, context.state.node, handler, capture, passive); + return build_event(context, node.name, handler, capture, passive, false); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/events.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/events.js index 29edc316ca..9485a33b3d 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/events.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/events.js @@ -27,55 +27,58 @@ export function visit_event_attribute(node, context) { let handler = build_event_handler(tag.expression, tag.metadata.expression, context); if (node.metadata.delegated) { - if (!context.state.events.has(event_name)) { - context.state.events.add(event_name); - } + context.state.events.add(event_name); + } - context.state.init.push( - b.stmt( - b.assignment( - '=', - b.member(context.state.node, b.id('__' + event_name, node.name_loc)), - handler - ) - ) - ); - } else { - const statement = b.stmt( - build_event( - event_name, - context.state.node, - handler, - capture, - is_passive_event(event_name) ? true : undefined - ) - ); + const statement = b.stmt( + build_event( + context, + event_name, + handler, + capture, + is_passive_event(event_name) ? true : undefined, + node.metadata.delegated + ) + ); - const type = /** @type {AST.SvelteNode} */ (context.path.at(-1)).type; + const type = /** @type {AST.SvelteNode} */ (context.path.at(-1)).type; - if (type === 'SvelteDocument' || type === 'SvelteWindow' || type === 'SvelteBody') { - // These nodes are above the component tree, and its events should run parent first - context.state.init.push(statement); - } else { - context.state.after_update.push(statement); - } + if (type === 'SvelteDocument' || type === 'SvelteWindow' || type === 'SvelteBody') { + // These nodes are above the component tree, and its events should run parent first + context.state.init.push(statement); + } else { + context.state.after_update.push(statement); } } /** * Creates a `$.event(...)` call for non-delegated event handlers + * @param {ComponentContext} context * @param {string} event_name - * @param {Expression} node * @param {Expression} handler * @param {boolean} capture * @param {boolean | undefined} passive + * @param {boolean | undefined} delegated */ -export function build_event(event_name, node, handler, capture, passive) { +export function build_event(context, event_name, handler, capture, passive, delegated) { + let fn = handler; + + if (dev && handler.type === 'ArrowFunctionExpression') { + // create a named function for better debugging + const name = context.state.scope.generate(event_name); + + fn = b.function( + b.id(name), + handler.params, + handler.body.type === 'BlockStatement' ? handler.body : b.block([b.return(handler.body)]) + ); + } + return b.call( - '$.event', + delegated ? '$.delegated' : '$.event', b.literal(event_name), - node, - handler, + context.state.node, + fn, capture && b.true, passive === undefined ? undefined : b.literal(passive) ); diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index 7b55c6dc4b..a15fc48596 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -2,7 +2,7 @@ import { DEV } from 'esm-env'; import { hydrating, set_hydrating } from '../hydration.js'; import { get_descriptors, get_prototype_of } from '../../../shared/utils.js'; -import { create_event, delegate } from './events.js'; +import { create_event, delegate, delegated, event, event_symbol } from './events.js'; import { add_form_reset_listener, autofocus } from './misc.js'; import * as w from '../../warnings.js'; import { IS_XHTML, LOADING_ATTR_SYMBOL } from '#client/constants'; @@ -384,14 +384,14 @@ function set_attributes( const opts = {}; const event_handle_key = '$$' + key; let event_name = key.slice(2); - var delegated = can_delegate_event(event_name); + var is_delegated = can_delegate_event(event_name); if (is_capture_event(event_name)) { event_name = event_name.slice(0, -7); opts.capture = true; } - if (!delegated && prev_value) { + if (!is_delegated && prev_value) { // Listening to same event but different handler -> our handle function below takes care of this // If we were to remove and add listeners in this case, it could happen that the event is "swallowed" // (the browser seems to not know yet that a new one exists now) and doesn't reach the handler @@ -402,25 +402,19 @@ function set_attributes( current[event_handle_key] = null; } - if (value != null) { - if (!delegated) { - /** - * @this {any} - * @param {Event} evt - */ - function handle(evt) { - current[key].call(this, evt); - } - - current[event_handle_key] = create_event(event_name, element, handle, opts); - } else { - // @ts-ignore - element[`__${event_name}`] = value; - delegate([event_name]); + if (is_delegated) { + delegated(event_name, element, value); + delegate([event_name]); + } else if (value != null) { + /** + * @this {any} + * @param {Event} evt + */ + function handle(evt) { + current[key].call(this, evt); } - } else if (delegated) { - // @ts-ignore - element[`__${event_name}`] = undefined; + + current[event_handle_key] = create_event(event_name, element, handle, opts); } } else if (key === 'style') { // avoid using the setter diff --git a/packages/svelte/src/internal/client/dom/elements/events.js b/packages/svelte/src/internal/client/dom/elements/events.js index 4c64c8364a..041698eb9d 100644 --- a/packages/svelte/src/internal/client/dom/elements/events.js +++ b/packages/svelte/src/internal/client/dom/elements/events.js @@ -11,6 +11,9 @@ import { set_active_reaction } from '../../runtime.js'; import { without_reactive_context } from './bindings/shared.js'; +import { can_delegate_event } from '../../../../utils.js'; + +export const event_symbol = Symbol('events'); /** @type {Set} */ export const all_registered_events = new Set(); @@ -127,6 +130,17 @@ export function event(event_name, dom, handler, capture, passive) { } } +/** + * @param {string} event_name + * @param {Element} element + * @param {EventListener} [handler] + * @returns {void} + */ +export function delegated(event_name, element, handler) { + // @ts-expect-error + (element[event_symbol] ??= {})[event_name] = handler; +} + /** * @param {Array} events * @returns {void} @@ -249,7 +263,7 @@ export function handle_event_propagation(event) { try { // @ts-expect-error - var delegated = current_target['__' + event_name]; + var delegated = current_target[event_symbol]?.[event_name]; if ( delegated != null && diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index 7fcaf77dc5..fedabd6ac1 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -40,7 +40,7 @@ export { STYLE } from './dom/elements/attributes.js'; export { set_class } from './dom/elements/class.js'; -export { apply, event, delegate, replay_events } from './dom/elements/events.js'; +export { apply, event, delegated, delegate, replay_events } from './dom/elements/events.js'; export { autofocus, remove_textarea_child } from './dom/elements/misc.js'; export { customizable_select, selectedcontent } from './dom/elements/customizable-select.js'; export { set_style } from './dom/elements/style.js'; diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-derived-2/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-derived-2/_config.js index 3742382759..3471a4df0c 100644 --- a/packages/svelte/tests/runtime-runes/samples/inspect-derived-2/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/inspect-derived-2/_config.js @@ -30,7 +30,7 @@ export default test({ }, derived: [1] }, - 'at HTMLButtonElement.Main.button.__click' + 'at HTMLButtonElement.click' ]); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-multiple/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-multiple/_config.js index 6886f5e53e..c46576de22 100644 --- a/packages/svelte/tests/runtime-runes/samples/inspect-multiple/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/inspect-multiple/_config.js @@ -17,10 +17,10 @@ export default test({ 0, 1, 0, - 'at HTMLButtonElement.', + 'at HTMLButtonElement.click', 1, 1, - 'at HTMLButtonElement.' + 'at HTMLButtonElement.click_1' ]); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-nested-state/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-nested-state/_config.js index 34cd74d780..9f012c0f29 100644 --- a/packages/svelte/tests/runtime-runes/samples/inspect-nested-state/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/inspect-nested-state/_config.js @@ -16,7 +16,7 @@ export default test({ [{ count: 0 }], { x: { count: 1 } }, [{ count: 1 }], - 'at HTMLButtonElement.' + 'at HTMLButtonElement.click' ]); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-new-property/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-new-property/_config.js index 43d217977e..36a9298fa1 100644 --- a/packages/svelte/tests/runtime-runes/samples/inspect-new-property/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/inspect-new-property/_config.js @@ -15,9 +15,9 @@ export default test({ {}, [], { x: 'hello' }, - 'at HTMLButtonElement.Main.button.__click', + 'at HTMLButtonElement.click', ['hello'], - 'at HTMLButtonElement.Main.button.__click' + 'at HTMLButtonElement.click' ]); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-recursive/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-recursive/_config.js index 8bf67159f5..199411ed84 100644 --- a/packages/svelte/tests/runtime-runes/samples/inspect-recursive/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/inspect-recursive/_config.js @@ -15,9 +15,9 @@ export default test({ assert.deepEqual(normalise_inspect_logs(logs), [ [], [{}], - 'at HTMLButtonElement.Main.button.__click', + 'at HTMLButtonElement.click', [{}, {}], - 'at HTMLButtonElement.Main.button.__click' + 'at HTMLButtonElement.click' ]); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/inspect/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect/_config.js index c05c4b15c4..117edc3ab4 100644 --- a/packages/svelte/tests/runtime-runes/samples/inspect/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/inspect/_config.js @@ -12,6 +12,6 @@ export default test({ b2.click(); await Promise.resolve(); - assert.deepEqual(normalise_inspect_logs(logs), [0, 1, 'at HTMLButtonElement.']); + assert.deepEqual(normalise_inspect_logs(logs), [0, 1, 'at HTMLButtonElement.click']); } }); diff --git a/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/client/index.svelte.js index 52820c1652..e1b87d6280 100644 --- a/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/client/index.svelte.js @@ -13,9 +13,6 @@ export default function Await_block_scope($$anchor) { var fragment = root(); var button = $.first_child(fragment); - - button.__click = increment; - var text = $.child(button); $.reset(button); @@ -31,6 +28,7 @@ export default function Await_block_scope($$anchor) { $.set_text(text_1, ` ${counter.count ?? ''}`); }); + $.delegated('click', button, increment); $.append($$anchor, fragment); } diff --git a/packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/_expected/client/index.svelte.js index ae28419b95..43f2eadc09 100644 --- a/packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/_expected/client/index.svelte.js @@ -13,11 +13,11 @@ export default function Delegated_locally_declared_shadowed($$anchor) { $.set_attribute(button, 'data-index', index); - button.__click = (e) => { + $.delegated('click', button, (e) => { const index = Number(e.currentTarget.dataset.index); console.log(index); - }; + }); $.append($$anchor, button); }); diff --git a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js index 7025c788be..5281a86582 100644 --- a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js @@ -16,9 +16,6 @@ export default function Nullish_coallescence_omittance($$anchor) { b.textContent = '123'; var button = $.sibling(b, 2); - - button.__click = () => $.update(count); - var text = $.child(button); $.reset(button); @@ -27,6 +24,7 @@ export default function Nullish_coallescence_omittance($$anchor) { h1_1.textContent = 'Hello, world'; $.template_effect(() => $.set_text(text, `Count is ${$.get(count) ?? ''}`)); + $.delegated('click', button, () => $.update(count)); $.append($$anchor, fragment); } diff --git a/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/client/index.svelte.js index 30691231f4..f3272f0414 100644 --- a/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/client/index.svelte.js @@ -25,9 +25,9 @@ export default function State_proxy_literal($$anchor) { var button = $.sibling(input_1, 2); - button.__click = reset; $.bind_value(input, () => $.get(str), ($$value) => $.set(str, $$value)); $.bind_value(input_1, () => $.get(tpl), ($$value) => $.set(tpl, $$value)); + $.delegated('click', button, reset); $.append($$anchor, fragment); }