diff --git a/.changeset/spicy-peas-vanish.md b/.changeset/spicy-peas-vanish.md new file mode 100644 index 000000000000..0c377b61899b --- /dev/null +++ b/.changeset/spicy-peas-vanish.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +feat: add svelte/events package and export `on` function diff --git a/packages/svelte/package.json b/packages/svelte/package.json index bf6c66846658..5c891d52878d 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -81,6 +81,10 @@ "./transition": { "types": "./types/index.d.ts", "default": "./src/transition/index.js" + }, + "./events": { + "types": "./types/index.d.ts", + "default": "./src/events/index.js" } }, "repository": { diff --git a/packages/svelte/scripts/generate-types.js b/packages/svelte/scripts/generate-types.js index c52693f5f342..bb195f5f8230 100644 --- a/packages/svelte/scripts/generate-types.js +++ b/packages/svelte/scripts/generate-types.js @@ -33,6 +33,7 @@ await createBundle({ [`${pkg.name}/server`]: `${dir}/src/server/index.js`, [`${pkg.name}/store`]: `${dir}/src/store/public.d.ts`, [`${pkg.name}/transition`]: `${dir}/src/transition/public.d.ts`, + [`${pkg.name}/events`]: `${dir}/src/events/index.js`, // TODO remove in Svelte 6 [`${pkg.name}/types/compiler/preprocess`]: `${dir}/src/compiler/preprocess/legacy-public.d.ts`, [`${pkg.name}/types/compiler/interfaces`]: `${dir}/src/compiler/types/legacy-interfaces.d.ts` diff --git a/packages/svelte/src/events/index.js b/packages/svelte/src/events/index.js new file mode 100644 index 000000000000..ba5bd46c5240 --- /dev/null +++ b/packages/svelte/src/events/index.js @@ -0,0 +1 @@ +export { on } from '../internal/client/dom/elements/events.js'; diff --git a/packages/svelte/src/internal/client/dom/elements/events.js b/packages/svelte/src/internal/client/dom/elements/events.js index 480314f8ca8a..33cb2c3ca0e5 100644 --- a/packages/svelte/src/internal/client/dom/elements/events.js +++ b/packages/svelte/src/internal/client/dom/elements/events.js @@ -66,6 +66,24 @@ export function create_event(event_name, dom, handler, options) { return target_handler; } +/** + * Attaches an event handler to an element and returns a function that removes the handler. Using this + * rather than `addEventListener` will preserve the correct order relative to handlers added declaratively + * (with attributes like `onclick`), which use event delegation for performance reasons + * + * @param {Element} element + * @param {string} type + * @param {EventListener} handler + * @param {AddEventListenerOptions} [options] + */ +export function on(element, type, handler, options = {}) { + var target_handler = create_event(type, element, handler, options); + + return () => { + element.removeEventListener(type, target_handler, options); + }; +} + /** * @param {string} event_name * @param {Element} dom diff --git a/packages/svelte/tests/runtime-runes/samples/event-on/_config.js b/packages/svelte/tests/runtime-runes/samples/event-on/_config.js new file mode 100644 index 000000000000..23e33a3f4acb --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/event-on/_config.js @@ -0,0 +1,17 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['client'], + + test({ assert, target, logs }) { + const [b1] = target.querySelectorAll('button'); + + b1?.click(); + b1?.click(); + b1?.click(); + flushSync(); + assert.htmlEqual(target.innerHTML, '
'); + assert.deepEqual(logs, []); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/event-on/main.svelte b/packages/svelte/tests/runtime-runes/samples/event-on/main.svelte new file mode 100644 index 000000000000..2ab365590889 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/event-on/main.svelte @@ -0,0 +1,23 @@ + + +
console.log('logged from onclick')}> + +
diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 48827bfc7e00..a8b8515dda75 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -2336,6 +2336,17 @@ declare module 'svelte/transition' { }) => () => TransitionConfig]; } +declare module 'svelte/events' { + /** + * Attaches an event handler to an element and returns a function that removes the handler. Using this + * rather than `addEventListener` will preserve the correct order relative to handlers added declaratively + * (with attributes like `onclick`), which use event delegation for performance reasons + * + * + */ + export function on(element: Element, type: string, handler: EventListener, options?: AddEventListenerOptions | undefined): () => void; +} + declare module 'svelte/types/compiler/preprocess' { /** @deprecated import this from 'svelte/preprocess' instead */ export type MarkupPreprocessor = MarkupPreprocessor_1; diff --git a/sites/svelte-5-preview/src/lib/autocomplete.js b/sites/svelte-5-preview/src/lib/autocomplete.js index ff0c2f741059..7fe6b8e4c1a1 100644 --- a/sites/svelte-5-preview/src/lib/autocomplete.js +++ b/sites/svelte-5-preview/src/lib/autocomplete.js @@ -145,6 +145,7 @@ export function autocomplete(context, selected, files) { 'svelte', 'svelte/animate', 'svelte/easing', + 'svelte/events', 'svelte/legacy', 'svelte/motion', 'svelte/reactivity', diff --git a/sites/svelte-5-preview/src/routes/docs/content/01-api/05-imports.md b/sites/svelte-5-preview/src/routes/docs/content/01-api/05-imports.md index 61a4f46d410a..de77e2914f12 100644 --- a/sites/svelte-5-preview/src/routes/docs/content/01-api/05-imports.md +++ b/sites/svelte-5-preview/src/routes/docs/content/01-api/05-imports.md @@ -115,6 +115,30 @@ Svelte provides reactive `Map`, `Set`, `Date` and `URL` classes. These can be im ``` +## `svelte/events` + +Where possible, event handlers added with [attributes like `onclick`](/docs/event-handlers) use a technique called _event delegation_. It works by creating a single handler for each event type on the root DOM element, rather than creating a handler for each element, resulting in better performance and memory usage. + +Delegated event handlers run after other event handlers. In other words, a handler added programmatically with [`addEventListener`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener) will run _before_ a handler added declaratively with `onclick`, regardless of their relative position in the DOM ([demo](/#H4sIAAAAAAAAE41Sy2rDMBD8lUUXJxDiu-sYeugt_YK6h8RaN6LyykgrQzH6965shxJooQc_RhrNzA6aVW8sBlW9zYouA6pKPY-jOij-GjMIE1pGwcFF3-WVOnTejNy01LIZRucZZnD06iIxJOi9G6BYjxVPmZQfiwzaTBkL2ti73R5ODcwLiftIHRtHcLuQtuhlc9tpuSyBbyZAuLloNfhIELBzpO8E-Q_O4tG6j13hIqO_y0BvPOpiv0bhtJ1Y3pLoeNH6ZULiswmMJLZFZ033WRzuAvstdMseOXqCh9SriMfBTfgPnZxg-aYM6_KnS6pFCK6GdJVHPc0C01JyfY0slUnHi-JpfgjwSzUycdgmfOjFEP3RS1qdhJ8dYMDFt1yNmxxU0jRyCwanTW9Qq4p9xPSevgHI3m43QAIAAA==)). It also means that calling `event.stopPropagation()` inside a declarative handler _won't_ prevent the programmatic handler (created inside an action, for example) from running. + +To preserve the relative order, use `on` rather than `addEventListener` ([demo](/#H4sIAAAAAAAAE3VRy26DMBD8lZUvECkqdwpI_YB-QdJDgpfGqlkjex2pQv73rnmoStQeMB52dnZmmdVgLAZVn2ZFlxFVrd6mSR0Vf08ZhDtaRsHBRd_nL03ovZm4O9OZzTg5zzCDo3cXiSHB4N0IxdpWvD6RnuoV3pE4rLT8WGTQ5p6xoE20LA_QdjAvJB4i9WxE6nYhbdFLcaucuaqAbyZAuLloNfhIELB3pHeC3IOz-GLdZ1m4yOh3GRiMR10cViucto7l9MjRk9gvxdsRit6a_qs47q1rT8qvpvpdDjXChqshXWdT7SwwLVtrrpElnAguSu38EPCPEOItbF4eEhiifxKkdZLw8wQYcZlbrYO7bFTcdPJbR6fNYFCrmn3E9JF-AJZOg9MRAgAA)): + +```js +// @filename: index.ts +const element: Element = null as any; +// ---cut--- +import { on } from 'svelte/events'; + +const off = on(element, 'click', () => { + console.log('element was clicked'); +}); + +// later, if we need to remove the event listener: +off(); +``` + +`on` also accepts an optional fourth argument which matches the options argument for `addEventListener`. + ## `svelte/server` ### `render`