Merge branch 'main' into teardown-effects

pull/11936/head
Rich Harris 2 years ago
commit de70d89491

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: make `legacy.componentApi` option more visible

@ -57,6 +57,7 @@
"chilled-pumas-invite",
"chilled-seas-jog",
"chilly-dolphins-lick",
"chilly-laws-juggle",
"chilly-pans-raise",
"chilly-rocks-hug",
"chilly-snakes-scream",
@ -69,6 +70,7 @@
"cold-beans-tease",
"cold-birds-own",
"cold-cheetahs-judge",
"cold-lamps-accept",
"cold-masks-learn",
"cool-actors-tan",
"cool-ants-leave",
@ -219,6 +221,7 @@
"honest-pans-kick",
"hot-cooks-repair",
"hot-jobs-tap",
"hot-sloths-clap",
"hungry-boxes-relate",
"hungry-dots-fry",
"hungry-pants-push",
@ -337,6 +340,7 @@
"olive-seals-sell",
"olive-shirts-complain",
"olive-socks-kick",
"orange-comics-prove",
"orange-crews-rescue",
"orange-dingos-poke",
"orange-masks-exercise",
@ -511,6 +515,7 @@
"tall-tigers-wait",
"tame-cycles-kneel",
"tame-dots-battle",
"tame-goats-bow",
"tame-spies-drum",
"tasty-cheetahs-appear",
"tasty-numbers-perform",

@ -0,0 +1,5 @@
---
"svelte": patch
---
feat: add svelte/events package and export `on` function

@ -1,5 +1,19 @@
# svelte
## 5.0.0-next.151
### Patch Changes
- fix: relax `Component` type ([#11929](https://github.com/sveltejs/svelte/pull/11929))
- fix: sort `{@const ...}` tags topologically in legacy mode ([#11908](https://github.com/sveltejs/svelte/pull/11908))
- chore: deprecate html in favour of body for render() ([#11927](https://github.com/sveltejs/svelte/pull/11927))
- fix: append start/end info to `AssignmentPattern` and `VariableDeclarator` ([#11930](https://github.com/sveltejs/svelte/pull/11930))
- fix: relax slot prop validation on components ([#11923](https://github.com/sveltejs/svelte/pull/11923))
## 5.0.0-next.150
### Patch Changes

@ -14,6 +14,10 @@
> %parent% called `%method%` on an instance of %component%, which is no longer valid in Svelte 5. See https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes for more information
## component_api_invalid_new
> Attempted to instantiate %component% with `new %name%`, which is no longer valid in Svelte 5. If this component is not under your control, set the `legacy.componentApi` compiler option to keep it working. See https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes for more information
## each_key_duplicate
> Keyed each block has duplicate key at indexes %a% and %b%

@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.0.0-next.150",
"version": "5.0.0-next.151",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
@ -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": {

@ -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`

@ -452,7 +452,7 @@ export function client_component(source, analysis, options) {
body.unshift(b.imports([['createClassComponent', '$$_createClassComponent']], 'svelte/legacy'));
component_block.body.unshift(
b.if(
b.binary('===', b.id('new.target'), b.id(analysis.name)),
b.id('new.target'),
b.return(
b.call(
'$$_createClassComponent',
@ -463,15 +463,7 @@ export function client_component(source, analysis, options) {
)
);
} else if (options.dev) {
component_block.body.unshift(
b.if(
b.binary('===', b.id('new.target'), b.id(analysis.name)),
b.throw_error(
`Instantiating a component with \`new\` is no longer valid in Svelte 5. ` +
'See https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes for more information'
)
)
);
component_block.body.unshift(b.stmt(b.call('$.check_target', b.id('new.target'))));
}
if (state.events.size > 0) {

@ -2747,10 +2747,9 @@ export const template_visitors = {
'$.bind_property',
b.literal(node.name),
b.literal(property.event),
b.literal(property.type ?? 'get'),
state.node,
getter,
setter
setter,
property.bidirectional && getter
);
} else {
// special cases

@ -2,7 +2,7 @@
* @typedef BindingProperty
* @property {string} [event] This is set if the binding corresponds to the property name on the dom element it's bound to
* and there's an event that notifies of a change to that property
* @property {string} [type] Set this to `set` if updates are written to the dom property
* @property {boolean} [bidirectional] Set this to `true` if updates are written to the dom property
* @property {boolean} [omit_in_ssr] Set this to true if the binding should not be included in SSR
* @property {string[]} [valid_elements] If this is set, the binding is only valid on the given elements
* @property {string[]} [invalid_elements] If this is set, the binding is invalid on the given elements
@ -175,7 +175,7 @@ export const binding_properties = {
// checkbox/radio
indeterminate: {
event: 'change',
type: 'set',
bidirectional: true,
valid_elements: ['input'],
omit_in_ssr: true // no corresponding attribute
},
@ -200,7 +200,7 @@ export const binding_properties = {
},
open: {
event: 'toggle',
type: 'set',
bidirectional: true,
valid_elements: ['details']
},
value: {

@ -0,0 +1 @@
export { on } from '../internal/client/dom/elements/events.js';

@ -1,6 +1,7 @@
import { block, branch, destroy_effect } from '../reactivity/effects.js';
import { set_should_intro } from '../render.js';
import { get } from '../runtime.js';
import { check_target } from './legacy.js';
/**
* @template {(anchor: Comment, props: any) => any} Component
@ -11,7 +12,7 @@ export function hmr(source) {
* @param {Comment} anchor
* @param {any} props
*/
return (anchor, props) => {
return function (anchor, props) {
let instance = {};
/** @type {import("#client").Effect} */
@ -31,7 +32,10 @@ export function hmr(source) {
// preserve getters/setters
Object.defineProperties(
instance,
Object.getOwnPropertyDescriptors(component(anchor, props))
Object.getOwnPropertyDescriptors(
// @ts-expect-error
new.target ? new component(anchor, props) : component(anchor, props)
)
);
set_should_intro(true);
});

@ -2,6 +2,13 @@ import * as e from '../errors.js';
import { current_component_context } from '../runtime.js';
import { get_component } from './ownership.js';
/** @param {Function & { filename: string }} target */
export function check_target(target) {
if (target) {
e.component_api_invalid_new(target.filename ?? 'a component', target.name);
}
}
export function legacy_api() {
const component = current_component_context?.function;

@ -33,40 +33,36 @@ export function bind_content_editable(property, element, get_value, update) {
/**
* @param {string} property
* @param {string} event_name
* @param {'get' | 'set'} type
* @param {Element} element
* @param {() => unknown} get_value
* @param {(value: unknown) => void} update
* @param {(value: unknown) => void} set
* @param {() => unknown} [get]
* @returns {void}
*/
export function bind_property(property, event_name, type, element, get_value, update) {
var target_handler = () => {
export function bind_property(property, event_name, element, set, get) {
var handler = () => {
// @ts-ignore
update(element[property]);
set(element[property]);
};
element.addEventListener(event_name, target_handler);
element.addEventListener(event_name, handler);
if (type === 'set') {
if (get) {
render_effect(() => {
// @ts-ignore
element[property] = get_value();
element[property] = get();
});
} else {
handler();
}
if (type === 'get') {
// @ts-ignore
update(element[property]);
}
render_effect(() => {
// @ts-ignore
if (element === document.body || element === window || element === document) {
// @ts-ignore
if (element === document.body || element === window || element === document) {
render_effect(() => {
return () => {
element.removeEventListener(event_name, target_handler);
element.removeEventListener(event_name, handler);
};
}
});
});
}
}
/**

@ -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

@ -75,6 +75,24 @@ export function component_api_changed(parent, method, component) {
}
}
/**
* Attempted to instantiate %component% with `new %name%`, which is no longer valid in Svelte 5. If this component is not under your control, set the `legacy.componentApi` compiler option to keep it working. See https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes for more information
* @param {string} component
* @param {string} name
* @returns {never}
*/
export function component_api_invalid_new(component, name) {
if (DEV) {
const error = new Error(`${"component_api_invalid_new"}\n${`Attempted to instantiate ${component} with \`new ${name}\`, which is no longer valid in Svelte 5. If this component is not under your control, set the \`legacy.componentApi\` compiler option to keep it working. See https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes for more information`}`);
error.name = 'Svelte error';
throw error;
} else {
// TODO print a link to the documentation
throw new Error("component_api_invalid_new");
}
}
/**
* Keyed each block has duplicate key `%value%` at indexes %a% and %b%
* @param {string} a

@ -7,7 +7,7 @@ export {
mark_module_end,
add_owner_effect
} from './dev/ownership.js';
export { legacy_api } from './dev/legacy.js';
export { check_target, legacy_api } from './dev/legacy.js';
export { inspect } from './dev/inspect.js';
export { await_block as await } from './dom/blocks/await.js';
export { if_block as if } from './dom/blocks/if.js';

@ -6,5 +6,5 @@
* https://svelte.dev/docs/svelte-compiler#svelte-version
* @type {string}
*/
export const VERSION = '5.0.0-next.150';
export const VERSION = '5.0.0-next.151';
export const PUBLIC_VERSION = '5';

@ -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, []);
}
});

@ -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>

@ -2340,6 +2340,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;

@ -145,6 +145,7 @@ export function autocomplete(context, selected, files) {
'svelte',
'svelte/animate',
'svelte/easing',
'svelte/events',
'svelte/legacy',
'svelte/motion',
'svelte/reactivity',

@ -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`

@ -70,7 +70,16 @@ import App from './App.svelte'
export default app;
```
If this component is not under your control, you can use the `legacy.componentApi` compiler option for auto-applied backwards compatibility (note that this adds a bit of overhead to each component). This will also add `$set` and `$on` methods for all component instances you get through `bind:this`.
If this component is not under your control, you can use the `legacy.componentApi` compiler option for auto-applied backwards compatibility, which means code using `new Component(...)` keeps working without adjustments (note that this adds a bit of overhead to each component). This will also add `$set` and `$on` methods for all component instances you get through `bind:this`.
```js
/// svelte.config.js
export default {
compilerOptions: {
legacy: { componentApi: true }
}
};
```
### Server API changes

Loading…
Cancel
Save