mirror of https://github.com/sveltejs/svelte
fix: better event handling (#12722)
* simplify * fix/simplify * fix/simplify * start getting a grip of this mess * tidy up * more * more * more * tidy up * make things a bit less weird * tweak * more * more * add once once * consolidate event handling code * some progress. man, this stuff is entangled * more * tidy up * simplify * simplify * more * fix * fix test names * fix a bug * tidy up * changeset * simplify * regenerate * tidy up * tidy up * tidy up * simplify * the module declaration case is already accounted for, above * simplify/document * typo * "hoistable" is a misnomer * hoist non_hoistable, rename * more typos * tweak * regeneratepull/12741/head
parent
e78cfd393e
commit
59ea0b9e13
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'svelte': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix: avoid recreating handlers for component events
|
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'svelte': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix: call correct event handler for properties of non-reactive objects
|
@ -1,11 +1,38 @@
|
|||||||
/** @import { OnDirective } from '#compiler' */
|
/** @import { OnDirective, SvelteNode } from '#compiler' */
|
||||||
/** @import { ComponentContext } from '../types' */
|
/** @import { ComponentContext } from '../types' */
|
||||||
import { build_event } from './shared/element.js';
|
import * as b from '../../../../utils/builders.js';
|
||||||
|
import { build_event, build_event_handler } from './shared/events.js';
|
||||||
|
|
||||||
|
const modifiers = [
|
||||||
|
'stopPropagation',
|
||||||
|
'stopImmediatePropagation',
|
||||||
|
'preventDefault',
|
||||||
|
'self',
|
||||||
|
'trusted',
|
||||||
|
'once'
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {OnDirective} node
|
* @param {OnDirective} node
|
||||||
* @param {ComponentContext} context
|
* @param {ComponentContext} context
|
||||||
*/
|
*/
|
||||||
export function OnDirective(node, context) {
|
export function OnDirective(node, context) {
|
||||||
build_event(node, node.metadata.expression, context);
|
if (!node.expression) {
|
||||||
|
context.state.analysis.needs_props = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let handler = build_event_handler(node.expression, node.metadata.expression, context);
|
||||||
|
|
||||||
|
for (const modifier of modifiers) {
|
||||||
|
if (node.modifiers.includes(modifier)) {
|
||||||
|
handler = b.call('$.' + modifier, handler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const capture = node.modifiers.includes('capture');
|
||||||
|
const passive =
|
||||||
|
node.modifiers.includes('passive') ||
|
||||||
|
(node.modifiers.includes('nonpassive') ? false : undefined);
|
||||||
|
|
||||||
|
return build_event(node.name, context.state.node, handler, capture, passive);
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,11 @@
|
|||||||
/** @import { SvelteBody } from '#compiler' */
|
/** @import { SvelteBody } from '#compiler' */
|
||||||
/** @import { ComponentContext } from '../types' */
|
/** @import { ComponentContext } from '../types' */
|
||||||
import * as b from '../../../../utils/builders.js';
|
import { visit_special_element } from './shared/special_element.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {SvelteBody} node
|
* @param {SvelteBody} node
|
||||||
* @param {ComponentContext} context
|
* @param {ComponentContext} context
|
||||||
*/
|
*/
|
||||||
export function SvelteBody(node, context) {
|
export function SvelteBody(node, context) {
|
||||||
context.next({
|
visit_special_element(node, '$.document.body', context);
|
||||||
...context.state,
|
|
||||||
node: b.id('$.document.body')
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,11 @@
|
|||||||
/** @import { SvelteDocument } from '#compiler' */
|
/** @import { SvelteDocument } from '#compiler' */
|
||||||
/** @import { ComponentContext } from '../types' */
|
/** @import { ComponentContext } from '../types' */
|
||||||
import * as b from '../../../../utils/builders.js';
|
import { visit_special_element } from './shared/special_element.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {SvelteDocument} node
|
* @param {SvelteDocument} node
|
||||||
* @param {ComponentContext} context
|
* @param {ComponentContext} context
|
||||||
*/
|
*/
|
||||||
export function SvelteDocument(node, context) {
|
export function SvelteDocument(node, context) {
|
||||||
context.next({
|
visit_special_element(node, '$.document', context);
|
||||||
...context.state,
|
|
||||||
node: b.id('$.document')
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,12 @@
|
|||||||
|
/** @import { Expression } from 'estree' */
|
||||||
/** @import { SvelteWindow } from '#compiler' */
|
/** @import { SvelteWindow } from '#compiler' */
|
||||||
/** @import { ComponentContext } from '../types' */
|
/** @import { ComponentContext } from '../types' */
|
||||||
import * as b from '../../../../utils/builders.js';
|
import { visit_special_element } from './shared/special_element.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {SvelteWindow} node
|
* @param {SvelteWindow} node
|
||||||
* @param {ComponentContext} context
|
* @param {ComponentContext} context
|
||||||
*/
|
*/
|
||||||
export function SvelteWindow(node, context) {
|
export function SvelteWindow(node, context) {
|
||||||
context.next({
|
visit_special_element(node, '$.window', context);
|
||||||
...context.state,
|
|
||||||
node: b.id('$.window')
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,144 @@
|
|||||||
|
/** @import { Expression } from 'estree' */
|
||||||
|
/** @import { Attribute, ExpressionMetadata, ExpressionTag, OnDirective, SvelteNode } from '#compiler' */
|
||||||
|
/** @import { ComponentContext } from '../../types' */
|
||||||
|
import { is_capture_event, is_passive_event } from '../../../../../../utils.js';
|
||||||
|
import * as b from '../../../../../utils/builders.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Attribute} node
|
||||||
|
* @param {ComponentContext} context
|
||||||
|
*/
|
||||||
|
export function visit_event_attribute(node, context) {
|
||||||
|
let capture = false;
|
||||||
|
|
||||||
|
let event_name = node.name.slice(2);
|
||||||
|
if (is_capture_event(event_name)) {
|
||||||
|
event_name = event_name.slice(0, -7);
|
||||||
|
capture = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// we still need to support the weird `onclick="{() => {...}}" form
|
||||||
|
const tag = Array.isArray(node.value)
|
||||||
|
? /** @type {ExpressionTag} */ (node.value[0])
|
||||||
|
: /** @type {ExpressionTag} */ (node.value);
|
||||||
|
|
||||||
|
let handler = build_event_handler(tag.expression, tag.metadata.expression, context);
|
||||||
|
|
||||||
|
if (node.metadata.delegated) {
|
||||||
|
let delegated_assignment;
|
||||||
|
|
||||||
|
if (!context.state.events.has(event_name)) {
|
||||||
|
context.state.events.add(event_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hoist function if we can, otherwise we leave the function as is
|
||||||
|
if (node.metadata.delegated.hoisted) {
|
||||||
|
if (node.metadata.delegated.function === tag.expression) {
|
||||||
|
const func_name = context.state.scope.root.unique('on_' + event_name);
|
||||||
|
context.state.hoisted.push(b.var(func_name, handler));
|
||||||
|
handler = func_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hoisted_params = /** @type {Expression[]} */ (
|
||||||
|
node.metadata.delegated.function.metadata.hoisted_params
|
||||||
|
);
|
||||||
|
|
||||||
|
// When we hoist a function we assign an array with the function and all
|
||||||
|
// hoisted closure params.
|
||||||
|
const args = [handler, ...hoisted_params];
|
||||||
|
delegated_assignment = b.array(args);
|
||||||
|
} else {
|
||||||
|
delegated_assignment = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.state.init.push(
|
||||||
|
b.stmt(
|
||||||
|
b.assignment(
|
||||||
|
'=',
|
||||||
|
b.member(context.state.node, b.id('__' + event_name)),
|
||||||
|
delegated_assignment
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const statement = b.stmt(
|
||||||
|
build_event(event_name, context.state.node, handler, capture, undefined)
|
||||||
|
);
|
||||||
|
|
||||||
|
const type = /** @type {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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a `$.event(...)` call for non-delegated event handlers
|
||||||
|
* @param {string} event_name
|
||||||
|
* @param {Expression} node
|
||||||
|
* @param {Expression} handler
|
||||||
|
* @param {boolean} capture
|
||||||
|
* @param {boolean | undefined} passive
|
||||||
|
*/
|
||||||
|
export function build_event(event_name, node, handler, capture, passive) {
|
||||||
|
return b.call(
|
||||||
|
'$.event',
|
||||||
|
b.literal(event_name),
|
||||||
|
node,
|
||||||
|
handler,
|
||||||
|
capture && b.true,
|
||||||
|
passive === undefined ? undefined : b.literal(passive)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an event handler
|
||||||
|
* @param {Expression | null} node
|
||||||
|
* @param {ExpressionMetadata} metadata
|
||||||
|
* @param {ComponentContext} context
|
||||||
|
* @returns {Expression}
|
||||||
|
*/
|
||||||
|
export function build_event_handler(node, metadata, context) {
|
||||||
|
if (node === null) {
|
||||||
|
// bubble event
|
||||||
|
return b.function(
|
||||||
|
null,
|
||||||
|
[b.id('$$arg')],
|
||||||
|
b.block([b.stmt(b.call('$.bubble_event.call', b.this, b.id('$$props'), b.id('$$arg')))])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let handler = /** @type {Expression} */ (context.visit(node));
|
||||||
|
|
||||||
|
// inline handler
|
||||||
|
if (handler.type === 'ArrowFunctionExpression' || handler.type === 'FunctionExpression') {
|
||||||
|
return handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
// function declared in the script
|
||||||
|
if (
|
||||||
|
handler.type === 'Identifier' &&
|
||||||
|
context.state.scope.get(handler.name)?.declaration_kind !== 'import'
|
||||||
|
) {
|
||||||
|
return handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metadata.has_call) {
|
||||||
|
// memoize where necessary
|
||||||
|
const id = b.id(context.state.scope.generate('event_handler'));
|
||||||
|
|
||||||
|
context.state.init.push(b.var(id, b.call('$.derived', b.thunk(handler))));
|
||||||
|
handler = b.call('$.get', id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// wrap the handler in a function, so the expression is re-evaluated for each event
|
||||||
|
return b.function(
|
||||||
|
null,
|
||||||
|
[b.rest(b.id('$$args'))],
|
||||||
|
b.block([b.stmt(b.call(b.member(handler, b.id('apply'), false, true), b.this, b.id('$$args')))])
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
/** @import { Expression } from 'estree' */
|
||||||
|
/** @import { SvelteBody, SvelteDocument, SvelteWindow } from '#compiler' */
|
||||||
|
/** @import { ComponentContext } from '../../types' */
|
||||||
|
import { is_event_attribute } from '../../../../../utils/ast.js';
|
||||||
|
import * as b from '../../../../../utils/builders.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {SvelteBody | SvelteDocument | SvelteWindow} node
|
||||||
|
* @param {string} id
|
||||||
|
* @param {ComponentContext} context
|
||||||
|
*/
|
||||||
|
export function visit_special_element(node, id, context) {
|
||||||
|
const state = { ...context.state, node: b.id(id) };
|
||||||
|
|
||||||
|
for (const attribute of node.attributes) {
|
||||||
|
if (attribute.type === 'OnDirective') {
|
||||||
|
context.state.init.push(b.stmt(/** @type {Expression} */ (context.visit(attribute, state))));
|
||||||
|
} else {
|
||||||
|
context.visit(attribute, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
<button on:click>
|
||||||
|
<slot />
|
||||||
|
</button>
|
@ -0,0 +1,34 @@
|
|||||||
|
import { test } from '../../test';
|
||||||
|
|
||||||
|
export default test({
|
||||||
|
test({ assert, logs, target }) {
|
||||||
|
const [b1, b2, b3] = target.querySelectorAll('button');
|
||||||
|
|
||||||
|
b2?.click();
|
||||||
|
b2?.click();
|
||||||
|
b3?.click();
|
||||||
|
b3?.click();
|
||||||
|
|
||||||
|
b1?.click();
|
||||||
|
|
||||||
|
b2?.click();
|
||||||
|
b2?.click();
|
||||||
|
b3?.click();
|
||||||
|
b3?.click();
|
||||||
|
|
||||||
|
assert.deepEqual(logs, [
|
||||||
|
'creating handler (1)',
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
'creating handler (1)',
|
||||||
|
3,
|
||||||
|
4,
|
||||||
|
'creating handler (2)',
|
||||||
|
6,
|
||||||
|
8,
|
||||||
|
'creating handler (2)',
|
||||||
|
10,
|
||||||
|
12
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
@ -0,0 +1,27 @@
|
|||||||
|
<script>
|
||||||
|
import Button from './Button.svelte';
|
||||||
|
|
||||||
|
let count = $state(0);
|
||||||
|
let d = $state(1);
|
||||||
|
|
||||||
|
function create_handler() {
|
||||||
|
const change = d;
|
||||||
|
|
||||||
|
console.log(`creating handler (${change})`);
|
||||||
|
|
||||||
|
return function increment() {
|
||||||
|
count += change;
|
||||||
|
console.log(count);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button on:click={() => (d += 1)}>increase d ({d})</button>
|
||||||
|
|
||||||
|
<button on:click={create_handler()}>
|
||||||
|
clicks: {count}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Button on:click={create_handler()}>
|
||||||
|
clicks: {count}
|
||||||
|
</Button>
|
@ -0,0 +1,19 @@
|
|||||||
|
import { flushSync } from 'svelte';
|
||||||
|
import { test } from '../../test';
|
||||||
|
|
||||||
|
export default test({
|
||||||
|
test({ assert, target }) {
|
||||||
|
const [btn1, btn2, btn3] = target.querySelectorAll('button');
|
||||||
|
|
||||||
|
flushSync(() => btn3.click());
|
||||||
|
assert.htmlEqual(/** @type {string} */ (btn3.textContent), 'clicks: 1');
|
||||||
|
|
||||||
|
flushSync(() => btn2.click());
|
||||||
|
flushSync(() => btn3.click());
|
||||||
|
assert.htmlEqual(/** @type {string} */ (btn3.textContent), 'clicks: 0');
|
||||||
|
|
||||||
|
flushSync(() => btn1.click());
|
||||||
|
flushSync(() => btn3.click());
|
||||||
|
assert.htmlEqual(/** @type {string} */ (btn3.textContent), 'clicks: 1');
|
||||||
|
}
|
||||||
|
});
|
@ -0,0 +1,19 @@
|
|||||||
|
<script>
|
||||||
|
let count = $state(0);
|
||||||
|
|
||||||
|
const handlers = {
|
||||||
|
current: increment
|
||||||
|
};
|
||||||
|
|
||||||
|
function increment() {
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function decrement() {
|
||||||
|
count -= 1;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button onclick={() => (handlers.current = increment)}>increment</button>
|
||||||
|
<button onclick={() => (handlers.current = decrement)}>decrement</button>
|
||||||
|
<button onclick={handlers.current}>clicks: {count}</button>
|
Loading…
Reference in new issue