diff --git a/.changeset/rare-feet-hang.md b/.changeset/rare-feet-hang.md new file mode 100644 index 000000000000..8cbd906eee78 --- /dev/null +++ b/.changeset/rare-feet-hang.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: add support of handleEvent object as event listener diff --git a/packages/svelte/elements.d.ts b/packages/svelte/elements.d.ts index 99d87b4c09a4..7201fb788b4e 100644 --- a/packages/svelte/elements.d.ts +++ b/packages/svelte/elements.d.ts @@ -39,9 +39,11 @@ type Booleanish = boolean | 'true' | 'false'; // Event Handler Types // ---------------------------------------------------------------------- -type EventHandler = ( - event: E & { currentTarget: EventTarget & T } -) => any; +type EventHandler = + | ((event: E & { currentTarget: EventTarget & T }) => any) + | { + handleEvent: (event: E & { currentTarget: EventTarget & T }) => any; + }; export type ClipboardEventHandler = EventHandler; export type CompositionEventHandler = EventHandler; 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 d252bd5474dc..9cdc8fd8120f 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 @@ -151,7 +151,7 @@ export function build_event_handler(node, metadata, context) { } // wrap the handler in a function, so the expression is re-evaluated for each event - let call = b.call(b.member(handler, 'apply', false, true), b.this, b.id('$$args')); + let call = b.call('$.call_event_handler', handler, b.this, b.id('$$evt'), b.id('$$data')); if (dev) { const loc = locator(/** @type {number} */ (node.start)); @@ -165,7 +165,8 @@ export function build_event_handler(node, metadata, context) { '$.apply', b.thunk(handler), b.this, - b.id('$$args'), + b.id('$$evt'), + b.id('$$data'), b.id(context.state.analysis.name), loc && b.array([b.literal(loc.line), b.literal(loc.column)]), has_side_effects(node) && b.true, @@ -173,7 +174,7 @@ export function build_event_handler(node, metadata, context) { ); } - return b.function(null, [b.rest(b.id('$$args'))], b.block([b.stmt(call)])); + return b.function(null, [b.id('$$evt'), b.rest(b.id('$$data'))], b.block([b.stmt(call)])); } /** diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index f63f55cc6ee6..2ac0bba691f5 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -1,7 +1,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 { call_event_handler, create_event, delegate } from './events.js'; import { add_form_reset_listener, autofocus } from './misc.js'; import * as w from '../../warnings.js'; import { LOADING_ATTR_SYMBOL } from '#client/constants'; @@ -376,7 +376,7 @@ export function set_attributes(element, prev, next, css_hash, skip_warning = fal * @param {Event} evt */ function handle(evt) { - current[key].call(this, evt); + call_event_handler(current[key], this, evt); } current[event_handle_key] = create_event(event_name, element, handle, opts); diff --git a/packages/svelte/src/internal/client/dom/elements/events.js b/packages/svelte/src/internal/client/dom/elements/events.js index 3374fe713ff8..a26705f421d4 100644 --- a/packages/svelte/src/internal/client/dom/elements/events.js +++ b/packages/svelte/src/internal/client/dom/elements/events.js @@ -12,6 +12,8 @@ import { } from '../../runtime.js'; import { without_reactive_context } from './bindings/shared.js'; +/** @typedef {((ev: Event, ...args: any) => void) | { handleEvent: (ev: Event, ...args: any) => void }} EventListenerWrapper */ + /** @type {Set} */ export const all_registered_events = new Set(); @@ -45,10 +47,24 @@ export function replay_events(dom) { } } +/** + * @param {EventListenerWrapper | undefined} handler + * @param {EventTarget} current_target + * @param {Event} event + * @param {any[]} [data] + */ +export function call_event_handler(handler, current_target, event, data = []) { + if (typeof handler === 'function') { + handler.call(current_target, event, ...data); + } else { + handler?.handleEvent(event); + } +} + /** * @param {string} event_name * @param {EventTarget} dom - * @param {EventListener} [handler] + * @param {EventListenerOrEventListenerObject} [handler] * @param {AddEventListenerOptions} [options] */ export function create_event(event_name, dom, handler, options = {}) { @@ -61,8 +77,8 @@ export function create_event(event_name, dom, handler, options = {}) { handle_event_propagation.call(dom, event); } if (!event.cancelBubble) { - return without_reactive_context(() => { - return handler?.call(this, event); + without_reactive_context(() => { + call_event_handler(handler, this, event); }); } } @@ -93,7 +109,7 @@ export function create_event(event_name, dom, handler, options = {}) { * * @param {EventTarget} element * @param {string} type - * @param {EventListener} handler + * @param {EventListenerOrEventListenerObject} handler * @param {AddEventListenerOptions} [options] */ export function on(element, type, handler, options = {}) { @@ -107,7 +123,7 @@ export function on(element, type, handler, options = {}) { /** * @param {string} event_name * @param {Element} dom - * @param {EventListener} [handler] + * @param {EventListenerOrEventListenerObject} [handler] * @param {boolean} [capture] * @param {boolean} [passive] * @returns {void} @@ -245,9 +261,9 @@ export function handle_event_propagation(event) { ) { if (is_array(delegated)) { var [fn, ...data] = delegated; - fn.apply(current_target, [event, ...data]); + call_event_handler(fn, current_target, event, data); } else { - delegated.call(current_target, event); + call_event_handler(delegated, current_target, event); } } } catch (error) { @@ -285,9 +301,10 @@ export function handle_event_propagation(event) { /** * In dev, warn if an event handler is not a function, as it means the * user probably called the handler or forgot to add a `() =>` - * @param {() => (event: Event, ...args: any) => void} thunk + * @param {() => EventListenerWrapper} thunk * @param {EventTarget} element - * @param {[Event, ...any]} args + * @param {Event} evt + * @param {any[]} data * @param {any} component * @param {[number, number]} [loc] * @param {boolean} [remove_parens] @@ -295,7 +312,8 @@ export function handle_event_propagation(event) { export function apply( thunk, element, - args, + evt, + data, component, loc, has_side_effects = false, @@ -310,11 +328,15 @@ export function apply( error = e; } - if (typeof handler !== 'function' && (has_side_effects || handler != null || error)) { + if ( + typeof handler !== 'function' && + typeof handler?.handleEvent !== 'function' && + (has_side_effects || handler != null || error) + ) { const filename = component?.[FILENAME]; const location = loc ? ` at ${filename}:${loc[0]}:${loc[1]}` : ` in ${filename}`; - const phase = args[0]?.eventPhase < Event.BUBBLING_PHASE ? 'capture' : ''; - const event_name = args[0]?.type + phase; + const phase = evt?.eventPhase < Event.BUBBLING_PHASE ? 'capture' : ''; + const event_name = evt?.type + phase; const description = `\`${event_name}\` handler${location}`; const suggestion = remove_parens ? 'remove the trailing `()`' : 'add a leading `() =>`'; @@ -324,5 +346,5 @@ export function apply( throw error; } } - handler?.apply(element, args); + call_event_handler(handler, element, evt, data); } diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index 14d6e29f5bb4..575832ebdf84 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -37,7 +37,13 @@ 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, + call_event_handler, + event, + delegate, + replay_events +} from './dom/elements/events.js'; export { autofocus, remove_textarea_child } from './dom/elements/misc.js'; export { set_style } from './dom/elements/style.js'; export { animation, transition } from './dom/elements/transitions.js'; diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index b58a1d4372a6..c5d92876aa4b 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -184,6 +184,7 @@ export function spread_attributes(attrs, css_hash, classes, styles, flags = 0) { for (name in attrs) { // omit functions, internal svelte properties and invalid attribute names if (typeof attrs[name] === 'function') continue; + if (name[0] === 'o' && name[1] === 'n' && typeof attrs[name] === 'object') continue; if (name[0] === '$' && name[1] === '$') continue; // faster than name.startsWith('$$') if (INVALID_ATTR_NAME_CHAR_REGEX.test(name)) continue; diff --git a/packages/svelte/tests/runtime-runes/samples/event-handler-object-dev/_config.js b/packages/svelte/tests/runtime-runes/samples/event-handler-object-dev/_config.js new file mode 100644 index 000000000000..ff4916c27704 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/event-handler-object-dev/_config.js @@ -0,0 +1,19 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['client'], + + compileOptions: { + dev: true + }, + + test({ assert, target, logs }) { + const [b1] = target.querySelectorAll('button'); + + b1.click(); + flushSync(); + assert.htmlEqual(target.innerHTML, ''); + assert.deepEqual(logs, []); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/event-handler-object-dev/main.svelte b/packages/svelte/tests/runtime-runes/samples/event-handler-object-dev/main.svelte new file mode 100644 index 000000000000..d9d0213a9119 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/event-handler-object-dev/main.svelte @@ -0,0 +1,13 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/event-handler-object-invalid/_config.js b/packages/svelte/tests/runtime-runes/samples/event-handler-object-invalid/_config.js new file mode 100644 index 000000000000..63a380c80fe8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/event-handler-object-invalid/_config.js @@ -0,0 +1,34 @@ +import { test } from '../../test'; + +export default test({ + mode: ['client'], + + compileOptions: { + dev: true + }, + + test({ assert, target, warnings, logs, errors }) { + const handler = (/** @type {any} */ e) => { + e.stopImmediatePropagation(); + }; + + window.addEventListener('error', handler, true); + + const [b1, b2, b3] = target.querySelectorAll('button'); + + b1.click(); + b2.click(); + b3.click(); + assert.deepEqual(logs, []); + assert.deepEqual(warnings, [ + '`click` handler at main.svelte:7:17 should be a function. Did you mean to add a leading `() =>`?', + '`click` handler at main.svelte:8:17 should be a function. Did you mean to add a leading `() =>`?', + '`click` handler at main.svelte:9:17 should be a function. Did you mean to add a leading `() =>`?' + ]); + assert.include(errors[0], 'is not a function'); + assert.include(errors[2], 'is not a function'); + assert.include(errors[4], 'is not a function'); + + window.removeEventListener('error', handler, true); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/event-handler-object-invalid/main.svelte b/packages/svelte/tests/runtime-runes/samples/event-handler-object-invalid/main.svelte new file mode 100644 index 000000000000..881cfc317738 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/event-handler-object-invalid/main.svelte @@ -0,0 +1,9 @@ + + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/event-handler-object-ssr/_config.js b/packages/svelte/tests/runtime-runes/samples/event-handler-object-ssr/_config.js new file mode 100644 index 000000000000..39d1cdf0eafd --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/event-handler-object-ssr/_config.js @@ -0,0 +1,10 @@ +import { test } from '../../test'; + +export default test({ + test({ assert, target }) { + assert.htmlEqual( + target.innerHTML, + `` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/event-handler-object-ssr/child.svelte b/packages/svelte/tests/runtime-runes/samples/event-handler-object-ssr/child.svelte new file mode 100644 index 000000000000..ab1f6d3f4e79 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/event-handler-object-ssr/child.svelte @@ -0,0 +1,6 @@ + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/event-handler-object-ssr/main.svelte b/packages/svelte/tests/runtime-runes/samples/event-handler-object-ssr/main.svelte new file mode 100644 index 000000000000..672c0691b494 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/event-handler-object-ssr/main.svelte @@ -0,0 +1,12 @@ + + + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/event-handler-object/_config.js b/packages/svelte/tests/runtime-runes/samples/event-handler-object/_config.js new file mode 100644 index 000000000000..8b8586a480a3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/event-handler-object/_config.js @@ -0,0 +1,32 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['client'], + + test({ assert, target, logs }) { + const buttons = target.querySelectorAll('button'); + + buttons.forEach((b) => b.click()); + flushSync(); + buttons.forEach((b) => b.click()); + flushSync(); + buttons.forEach((b) => b.click()); + flushSync(); + assert.deepEqual(logs, [ + 'click', + true, + 'click', + true, + 'mutated', + true, + 'mutated', + true, + 'assigned', + true, + 'assigned', + true + ]); + assert.htmlEqual(target.innerHTML, ''); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/event-handler-object/main.svelte b/packages/svelte/tests/runtime-runes/samples/event-handler-object/main.svelte new file mode 100644 index 000000000000..c270b989c122 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/event-handler-object/main.svelte @@ -0,0 +1,38 @@ + + + + +