Skip to content

feat: add svelte/events package and export on function #11912

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Jun 6, 2024
5 changes: 5 additions & 0 deletions .changeset/spicy-peas-vanish.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"svelte": patch
---

feat: add svelte/events package and export `on` function
4 changes: 4 additions & 0 deletions packages/svelte/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
1 change: 1 addition & 0 deletions packages/svelte/scripts/generate-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
1 change: 1 addition & 0 deletions packages/svelte/src/events/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { on } from '../internal/client/dom/elements/events.js';
18 changes: 18 additions & 0 deletions packages/svelte/src/internal/client/dom/elements/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions packages/svelte/tests/runtime-runes/samples/event-on/_config.js
Original file line number Diff line number Diff line change
@@ -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, '<section><button>clicks: 3</button></section>');
assert.deepEqual(logs, []);
}
});
23 changes: 23 additions & 0 deletions packages/svelte/tests/runtime-runes/samples/event-on/main.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<script>
import { on } from 'svelte/events';

let count = $state(0);

function increment(e) {
e.stopPropagation();
count += 1;
}

let sectionEl
$effect(() => {
return on(sectionEl, 'click', () => {
console.log('logged from addEventListener');
});
});
</script>

<section bind:this={sectionEl} onclick={() => console.log('logged from onclick')}>
<button onclick={increment}>
clicks: {count}
</button>
</section>
11 changes: 11 additions & 0 deletions packages/svelte/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions sites/svelte-5-preview/src/lib/autocomplete.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ export function autocomplete(context, selected, files) {
'svelte',
'svelte/animate',
'svelte/easing',
'svelte/events',
'svelte/legacy',
'svelte/motion',
'svelte/reactivity',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,30 @@ Svelte provides reactive `Map`, `Set`, `Date` and `URL` classes. These can be im
<input bind:value={url.href} />
```

## `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`
Expand Down
Loading