Skip to content

Commit 95d4ec8

Browse files
committed
add support of handleEvent object listener
1 parent 1c2fc21 commit 95d4ec8

File tree

12 files changed

+195
-23
lines changed

12 files changed

+195
-23
lines changed

.changeset/rare-feet-hang.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': minor
3+
---
4+
5+
feat: add support of handleEvent object as event listener

packages/svelte/elements.d.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,11 @@ type Booleanish = boolean | 'true' | 'false';
3939
// Event Handler Types
4040
// ----------------------------------------------------------------------
4141

42-
type EventHandler<E extends Event = Event, T extends EventTarget = Element> = (
43-
event: E & { currentTarget: EventTarget & T }
44-
) => any;
42+
type EventHandler<E extends Event = Event, T extends EventTarget = Element> =
43+
| ((event: E & { currentTarget: EventTarget & T }) => any)
44+
| {
45+
handleEvent: (event: E & { currentTarget: EventTarget & T }) => any;
46+
};
4547

4648
export type ClipboardEventHandler<T extends EventTarget> = EventHandler<ClipboardEvent, T>;
4749
export type CompositionEventHandler<T extends EventTarget> = EventHandler<CompositionEvent, T>;

packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/events.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ export function build_event_handler(node, metadata, context) {
151151
}
152152

153153
// wrap the handler in a function, so the expression is re-evaluated for each event
154-
let call = b.call(b.member(handler, 'apply', false, true), b.this, b.id('$$args'));
154+
let call = b.call('$.call_event_handler', handler, b.this, b.id('$$evt'), b.id('$$data'));
155155

156156
if (dev) {
157157
const loc = locator(/** @type {number} */ (node.start));
@@ -165,15 +165,16 @@ export function build_event_handler(node, metadata, context) {
165165
'$.apply',
166166
b.thunk(handler),
167167
b.this,
168-
b.id('$$args'),
168+
b.id('$$evt'),
169+
b.id('$$data'),
169170
b.id(context.state.analysis.name),
170171
loc && b.array([b.literal(loc.line), b.literal(loc.column)]),
171172
has_side_effects(node) && b.true,
172173
remove_parens && b.true
173174
);
174175
}
175176

176-
return b.function(null, [b.rest(b.id('$$args'))], b.block([b.stmt(call)]));
177+
return b.function(null, [b.id('$$evt'), b.rest(b.id('$$data'))], b.block([b.stmt(call)]));
177178
}
178179

179180
/**

packages/svelte/src/internal/client/dom/elements/attributes.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { DEV } from 'esm-env';
22
import { hydrating, set_hydrating } from '../hydration.js';
33
import { get_descriptors, get_prototype_of } from '../../../shared/utils.js';
4-
import { create_event, delegate } from './events.js';
4+
import { call_event_handler, create_event, delegate } from './events.js';
55
import { add_form_reset_listener, autofocus } from './misc.js';
66
import * as w from '../../warnings.js';
77
import { LOADING_ATTR_SYMBOL } from '#client/constants';
@@ -376,7 +376,7 @@ export function set_attributes(element, prev, next, css_hash, skip_warning = fal
376376
* @param {Event} evt
377377
*/
378378
function handle(evt) {
379-
current[key].call(this, evt);
379+
call_event_handler(current[key], this, evt);
380380
}
381381

382382
current[event_handle_key] = create_event(event_name, element, handle, opts);

packages/svelte/src/internal/client/dom/elements/events.js

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import {
1212
} from '../../runtime.js';
1313
import { without_reactive_context } from './bindings/shared.js';
1414

15+
/** @typedef {((ev: Event, ...args: any) => void) | { handleEvent: (ev: Event, ...args: any) => void }} EventListenerWrapper */
16+
1517
/** @type {Set<string>} */
1618
export const all_registered_events = new Set();
1719

@@ -45,10 +47,29 @@ export function replay_events(dom) {
4547
}
4648
}
4749

50+
/**
51+
* @param {EventListenerWrapper | undefined} handler
52+
* @param {EventTarget} current_target
53+
* @param {Event} event
54+
* @param {any[]} [data]
55+
*/
56+
export function call_event_handler(handler, current_target, event, data = []) {
57+
if (typeof handler === 'function') {
58+
handler.call(current_target, event, ...data);
59+
} else {
60+
if (handler && handler.handleEvent == null) {
61+
// @ts-expect-error - do so to get a nicer "handler.handleEvent is not a function" error instead of "Cannot read properties of null (reading 'call')"
62+
handler.handleEvent();
63+
} else {
64+
handler?.handleEvent.call(current_target, event);
65+
}
66+
}
67+
}
68+
4869
/**
4970
* @param {string} event_name
5071
* @param {EventTarget} dom
51-
* @param {EventListener} [handler]
72+
* @param {EventListenerOrEventListenerObject} [handler]
5273
* @param {AddEventListenerOptions} [options]
5374
*/
5475
export function create_event(event_name, dom, handler, options = {}) {
@@ -61,8 +82,8 @@ export function create_event(event_name, dom, handler, options = {}) {
6182
handle_event_propagation.call(dom, event);
6283
}
6384
if (!event.cancelBubble) {
64-
return without_reactive_context(() => {
65-
return handler?.call(this, event);
85+
without_reactive_context(() => {
86+
call_event_handler(handler, this, event);
6687
});
6788
}
6889
}
@@ -93,7 +114,7 @@ export function create_event(event_name, dom, handler, options = {}) {
93114
*
94115
* @param {EventTarget} element
95116
* @param {string} type
96-
* @param {EventListener} handler
117+
* @param {EventListenerOrEventListenerObject} handler
97118
* @param {AddEventListenerOptions} [options]
98119
*/
99120
export function on(element, type, handler, options = {}) {
@@ -107,7 +128,7 @@ export function on(element, type, handler, options = {}) {
107128
/**
108129
* @param {string} event_name
109130
* @param {Element} dom
110-
* @param {EventListener} [handler]
131+
* @param {EventListenerOrEventListenerObject} [handler]
111132
* @param {boolean} [capture]
112133
* @param {boolean} [passive]
113134
* @returns {void}
@@ -245,9 +266,9 @@ export function handle_event_propagation(event) {
245266
) {
246267
if (is_array(delegated)) {
247268
var [fn, ...data] = delegated;
248-
fn.apply(current_target, [event, ...data]);
269+
call_event_handler(fn, current_target, event, data);
249270
} else {
250-
delegated.call(current_target, event);
271+
call_event_handler(delegated, current_target, event);
251272
}
252273
}
253274
} catch (error) {
@@ -285,17 +306,19 @@ export function handle_event_propagation(event) {
285306
/**
286307
* In dev, warn if an event handler is not a function, as it means the
287308
* user probably called the handler or forgot to add a `() =>`
288-
* @param {() => (event: Event, ...args: any) => void} thunk
309+
* @param {() => EventListenerWrapper} thunk
289310
* @param {EventTarget} element
290-
* @param {[Event, ...any]} args
311+
* @param {Event} evt
312+
* @param {any[]} data
291313
* @param {any} component
292314
* @param {[number, number]} [loc]
293315
* @param {boolean} [remove_parens]
294316
*/
295317
export function apply(
296318
thunk,
297319
element,
298-
args,
320+
evt,
321+
data,
299322
component,
300323
loc,
301324
has_side_effects = false,
@@ -310,11 +333,15 @@ export function apply(
310333
error = e;
311334
}
312335

313-
if (typeof handler !== 'function' && (has_side_effects || handler != null || error)) {
336+
if (
337+
typeof handler !== 'function' &&
338+
typeof handler?.handleEvent !== 'function' &&
339+
(has_side_effects || handler != null || error)
340+
) {
314341
const filename = component?.[FILENAME];
315342
const location = loc ? ` at ${filename}:${loc[0]}:${loc[1]}` : ` in ${filename}`;
316-
const phase = args[0]?.eventPhase < Event.BUBBLING_PHASE ? 'capture' : '';
317-
const event_name = args[0]?.type + phase;
343+
const phase = evt?.eventPhase < Event.BUBBLING_PHASE ? 'capture' : '';
344+
const event_name = evt?.type + phase;
318345
const description = `\`${event_name}\` handler${location}`;
319346
const suggestion = remove_parens ? 'remove the trailing `()`' : 'add a leading `() =>`';
320347

@@ -324,5 +351,5 @@ export function apply(
324351
throw error;
325352
}
326353
}
327-
handler?.apply(element, args);
354+
call_event_handler(handler, element, evt, data);
328355
}

packages/svelte/src/internal/client/index.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,13 @@ export {
3737
STYLE
3838
} from './dom/elements/attributes.js';
3939
export { set_class } from './dom/elements/class.js';
40-
export { apply, event, delegate, replay_events } from './dom/elements/events.js';
40+
export {
41+
apply,
42+
call_event_handler,
43+
event,
44+
delegate,
45+
replay_events
46+
} from './dom/elements/events.js';
4147
export { autofocus, remove_textarea_child } from './dom/elements/misc.js';
4248
export { set_style } from './dom/elements/style.js';
4349
export { animation, transition } from './dom/elements/transitions.js';
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { flushSync } from 'svelte';
2+
import { test } from '../../test';
3+
4+
export default test({
5+
mode: ['client'],
6+
7+
compileOptions: {
8+
dev: true
9+
},
10+
11+
test({ assert, target, logs }) {
12+
const [b1] = target.querySelectorAll('button');
13+
14+
b1.click();
15+
flushSync();
16+
assert.htmlEqual(target.innerHTML, '<button data-step="2">clicks: 2</button>');
17+
assert.deepEqual(logs, []);
18+
}
19+
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<script>
2+
let count = $state(0);
3+
4+
let onclick = $state({
5+
handleEvent() {
6+
count += +this.dataset.step;
7+
}
8+
});
9+
</script>
10+
11+
<button data-step="2" {onclick}>
12+
clicks: {count}
13+
</button>
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
mode: ['client'],
5+
6+
compileOptions: {
7+
dev: true
8+
},
9+
10+
test({ assert, target, warnings, logs, errors }) {
11+
const handler = (/** @type {any} */ e) => {
12+
e.stopImmediatePropagation();
13+
};
14+
15+
window.addEventListener('error', handler, true);
16+
17+
const [b1, b2] = target.querySelectorAll('button');
18+
19+
b1.click();
20+
b2.click();
21+
assert.deepEqual(logs, []);
22+
assert.deepEqual(warnings, [
23+
'`click` handler at main.svelte:6:17 should be a function. Did you mean to add a leading `() =>`?',
24+
'`click` handler at main.svelte:7:17 should be a function. Did you mean to add a leading `() =>`?'
25+
]);
26+
assert.include(errors[0], 'is not a function');
27+
assert.include(errors[2], 'is not a function');
28+
29+
window.removeEventListener('error', handler, true);
30+
}
31+
});
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<script>
2+
let empty = {};
3+
let wrong = { handleEvent: "bad" };
4+
</script>
5+
6+
<button onclick={empty}>click</button>
7+
<button onclick={wrong}>click</button>

0 commit comments

Comments
 (0)