fix: use symbols for encapsulated event delegation (#17703)

Alternative to #17690. Closes #17057
pull/17707/head
Rich Harris 6 months ago committed by GitHub
parent 7b6755eaf6
commit 220b526d3a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: use symbols for encapsulated event delegation

@ -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);
}

@ -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)
);

@ -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

@ -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<string>} */
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<string>} 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 &&

@ -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';

@ -30,7 +30,7 @@ export default test({
},
derived: [1]
},
'at HTMLButtonElement.Main.button.__click'
'at HTMLButtonElement.click'
]);
}
});

@ -17,10 +17,10 @@ export default test({
0,
1,
0,
'at HTMLButtonElement.<anonymous>',
'at HTMLButtonElement.click',
1,
1,
'at HTMLButtonElement.<anonymous>'
'at HTMLButtonElement.click_1'
]);
}
});

@ -16,7 +16,7 @@ export default test({
[{ count: 0 }],
{ x: { count: 1 } },
[{ count: 1 }],
'at HTMLButtonElement.<anonymous>'
'at HTMLButtonElement.click'
]);
}
});

@ -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'
]);
}
});

@ -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'
]);
}
});

@ -12,6 +12,6 @@ export default test({
b2.click();
await Promise.resolve();
assert.deepEqual(normalise_inspect_logs(logs), [0, 1, 'at HTMLButtonElement.<anonymous>']);
assert.deepEqual(normalise_inspect_logs(logs), [0, 1, 'at HTMLButtonElement.click']);
}
});

@ -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);
}

@ -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);
});

@ -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);
}

@ -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);
}

Loading…
Cancel
Save