Skip to content

Commit aadf629

Browse files
trueadmRich-Harris
andauthored
feat: add svelte/events package and export on function (#11912)
* feat: add svelete/events package and export attach function * feat: add svelete/events package and export attach function * docs * docs * docs * feedback * feedback * add more info * lint * rename * add package to autocomplete * rename args * fix * update inline docs * tweak docs --------- Co-authored-by: Rich Harris <[email protected]>
1 parent fdecad6 commit aadf629

File tree

10 files changed

+105
-0
lines changed

10 files changed

+105
-0
lines changed

.changeset/spicy-peas-vanish.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"svelte": patch
3+
---
4+
5+
feat: add svelte/events package and export `on` function

packages/svelte/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@
8181
"./transition": {
8282
"types": "./types/index.d.ts",
8383
"default": "./src/transition/index.js"
84+
},
85+
"./events": {
86+
"types": "./types/index.d.ts",
87+
"default": "./src/events/index.js"
8488
}
8589
},
8690
"repository": {

packages/svelte/scripts/generate-types.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ await createBundle({
3333
[`${pkg.name}/server`]: `${dir}/src/server/index.js`,
3434
[`${pkg.name}/store`]: `${dir}/src/store/public.d.ts`,
3535
[`${pkg.name}/transition`]: `${dir}/src/transition/public.d.ts`,
36+
[`${pkg.name}/events`]: `${dir}/src/events/index.js`,
3637
// TODO remove in Svelte 6
3738
[`${pkg.name}/types/compiler/preprocess`]: `${dir}/src/compiler/preprocess/legacy-public.d.ts`,
3839
[`${pkg.name}/types/compiler/interfaces`]: `${dir}/src/compiler/types/legacy-interfaces.d.ts`

packages/svelte/src/events/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { on } from '../internal/client/dom/elements/events.js';

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,24 @@ export function create_event(event_name, dom, handler, options) {
6666
return target_handler;
6767
}
6868

69+
/**
70+
* Attaches an event handler to an element and returns a function that removes the handler. Using this
71+
* rather than `addEventListener` will preserve the correct order relative to handlers added declaratively
72+
* (with attributes like `onclick`), which use event delegation for performance reasons
73+
*
74+
* @param {Element} element
75+
* @param {string} type
76+
* @param {EventListener} handler
77+
* @param {AddEventListenerOptions} [options]
78+
*/
79+
export function on(element, type, handler, options = {}) {
80+
var target_handler = create_event(type, element, handler, options);
81+
82+
return () => {
83+
element.removeEventListener(type, target_handler, options);
84+
};
85+
}
86+
6987
/**
7088
* @param {string} event_name
7189
* @param {Element} dom
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { flushSync } from 'svelte';
2+
import { test } from '../../test';
3+
4+
export default test({
5+
mode: ['client'],
6+
7+
test({ assert, target, logs }) {
8+
const [b1] = target.querySelectorAll('button');
9+
10+
b1?.click();
11+
b1?.click();
12+
b1?.click();
13+
flushSync();
14+
assert.htmlEqual(target.innerHTML, '<section><button>clicks: 3</button></section>');
15+
assert.deepEqual(logs, []);
16+
}
17+
});
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<script>
2+
import { on } from 'svelte/events';
3+
4+
let count = $state(0);
5+
6+
function increment(e) {
7+
e.stopPropagation();
8+
count += 1;
9+
}
10+
11+
let sectionEl
12+
$effect(() => {
13+
return on(sectionEl, 'click', () => {
14+
console.log('logged from addEventListener');
15+
});
16+
});
17+
</script>
18+
19+
<section bind:this={sectionEl} onclick={() => console.log('logged from onclick')}>
20+
<button onclick={increment}>
21+
clicks: {count}
22+
</button>
23+
</section>

packages/svelte/types/index.d.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2340,6 +2340,17 @@ declare module 'svelte/transition' {
23402340
}) => () => TransitionConfig];
23412341
}
23422342

2343+
declare module 'svelte/events' {
2344+
/**
2345+
* Attaches an event handler to an element and returns a function that removes the handler. Using this
2346+
* rather than `addEventListener` will preserve the correct order relative to handlers added declaratively
2347+
* (with attributes like `onclick`), which use event delegation for performance reasons
2348+
*
2349+
*
2350+
*/
2351+
export function on(element: Element, type: string, handler: EventListener, options?: AddEventListenerOptions | undefined): () => void;
2352+
}
2353+
23432354
declare module 'svelte/types/compiler/preprocess' {
23442355
/** @deprecated import this from 'svelte/preprocess' instead */
23452356
export type MarkupPreprocessor = MarkupPreprocessor_1;

sites/svelte-5-preview/src/lib/autocomplete.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ export function autocomplete(context, selected, files) {
145145
'svelte',
146146
'svelte/animate',
147147
'svelte/easing',
148+
'svelte/events',
148149
'svelte/legacy',
149150
'svelte/motion',
150151
'svelte/reactivity',

sites/svelte-5-preview/src/routes/docs/content/01-api/05-imports.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,30 @@ Svelte provides reactive `Map`, `Set`, `Date` and `URL` classes. These can be im
115115
<input bind:value={url.href} />
116116
```
117117

118+
## `svelte/events`
119+
120+
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.
121+
122+
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.
123+
124+
To preserve the relative order, use `on` rather than `addEventListener` ([demo](/#H4sIAAAAAAAAE3VRy26DMBD8lZUvECkqdwpI_YB-QdJDgpfGqlkjex2pQv73rnmoStQeMB52dnZmmdVgLAZVn2ZFlxFVrd6mSR0Vf08ZhDtaRsHBRd_nL03ovZm4O9OZzTg5zzCDo3cXiSHB4N0IxdpWvD6RnuoV3pE4rLT8WGTQ5p6xoE20LA_QdjAvJB4i9WxE6nYhbdFLcaucuaqAbyZAuLloNfhIELB3pHeC3IOz-GLdZ1m4yOh3GRiMR10cViucto7l9MjRk9gvxdsRit6a_qs47q1rT8qvpvpdDjXChqshXWdT7SwwLVtrrpElnAguSu38EPCPEOItbF4eEhiifxKkdZLw8wQYcZlbrYO7bFTcdPJbR6fNYFCrmn3E9JF-AJZOg9MRAgAA)):
125+
126+
```js
127+
// @filename: index.ts
128+
const element: Element = null as any;
129+
// ---cut---
130+
import { on } from 'svelte/events';
131+
132+
const off = on(element, 'click', () => {
133+
console.log('element was clicked');
134+
});
135+
136+
// later, if we need to remove the event listener:
137+
off();
138+
```
139+
140+
`on` also accepts an optional fourth argument which matches the options argument for `addEventListener`.
141+
118142
## `svelte/server`
119143

120144
### `render`

0 commit comments

Comments
 (0)