fix: migrate events to be more inline with svelte 4 (#13362)

* fix: migrate events to be more inline with svelte 4

* fix: use function instead of action

* chore: generate types

* re-export event modifier substitutes

* chore: replace map with object

* fix: handle nullish values

* fix: use bubbler for delegated events

* chore: generate types

* chore: deprecate legacy stuff

* chore: damn type generations

* fix: implement bubble on props strategy

* chore: revert bubble as prop init but keep fix for restProps

* fix: use `passive` and `nonpassive` actions for `passive` and `nonpassive` modifiers

* chore: i swear i will setup a commit hook for generating types

* chore: update output

* tweak passive/nonpassive types to make them more compact and show up as deprecated

* make terminology more consistent with other places

* eat local, shop local, declare local

* unify name replacement stuff

* remove errant newline

* ensure modifier order is stable and matches svelte 4

* fix

* compute indent once

* joining without newlines seems to work better in common cases

* replace passive/nonpassive handlers in situ

* unused

* simplify

* simplify, remove errant spaces

* only import handlers when necessary

* changeset

---------

Co-authored-by: Rich Harris <rich.harris@vercel.com>
pull/13383/head
Paolo Ricciuti 3 months ago committed by GitHub
parent 4b7cb9aeae
commit 0f6cc2b1ed
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: higher fidelity event migration

@ -7,12 +7,11 @@ import MagicString from 'magic-string';
import { walk } from 'zimmerframe';
import { parse } from '../phases/1-parse/index.js';
import { analyze_component } from '../phases/2-analyze/index.js';
import { validate_component_options } from '../validate-options.js';
import { get_rune } from '../phases/scope.js';
import { reset, reset_warning_filter } from '../state.js';
import { extract_identifiers } from '../utils/ast.js';
import { regex_is_valid_identifier } from '../phases/patterns.js';
import { migrate_svelte_ignore } from '../utils/extract_svelte_ignore.js';
import { validate_component_options } from '../validate-options.js';
/**
* Does a best-effort migration of Svelte code towards using runes, event attributes and render tags.
@ -50,11 +49,27 @@ export function migrate(source) {
props: [],
props_insertion_point: parsed.instance?.content.start ?? 0,
has_props_rune: false,
props_name: analysis.root.unique('props').name,
rest_props_name: analysis.root.unique('rest').name,
end: source.length,
run_name: analysis.root.unique('run').name,
needs_run: false
names: {
props: analysis.root.unique('props').name,
rest: analysis.root.unique('rest').name,
// event stuff
run: analysis.root.unique('run').name,
handlers: analysis.root.unique('handlers').name,
stopImmediatePropagation: analysis.root.unique('stopImmediatePropagation').name,
preventDefault: analysis.root.unique('preventDefault').name,
stopPropagation: analysis.root.unique('stopPropagation').name,
once: analysis.root.unique('once').name,
self: analysis.root.unique('self').name,
trusted: analysis.root.unique('trusted').name,
createBubbler: analysis.root.unique('createBubbler').name,
bubble: analysis.root.unique('bubble').name,
passive: analysis.root.unique('passive').name,
nonpassive: analysis.root.unique('nonpassive').name
},
legacy_imports: new Set(),
script_insertions: new Set()
};
if (parsed.module) {
@ -71,7 +86,12 @@ export function migrate(source) {
state = { ...state, scope: analysis.template.scope };
walk(parsed.fragment, state, template);
const run_import = `import { run${state.run_name === 'run' ? '' : ` as ${state.run_name}`} } from 'svelte/legacy';`;
const specifiers = [...state.legacy_imports].map((imported) => {
const local = state.names[imported];
return imported === local ? imported : `${imported} as ${local}`;
});
const legacy_import = `import { ${specifiers.join(', ')} } from 'svelte/legacy';`;
let added_legacy_import = false;
if (state.props.length > 0 || analysis.uses_rest_props || analysis.uses_props) {
@ -80,7 +100,7 @@ export function migrate(source) {
const props_separator = has_many_props ? newline_separator : ' ';
let props = '';
if (analysis.uses_props) {
props = `...${state.props_name}`;
props = `...${state.names.props}`;
} else {
props = state.props
.map((prop) => {
@ -96,7 +116,7 @@ export function migrate(source) {
.join(`,${props_separator}`);
if (analysis.uses_rest_props) {
props += `,${props_separator}...${state.rest_props_name}`;
props += `${state.props.length > 0 ? `,${props_separator}` : ''}...${state.names.rest}`;
}
}
@ -145,8 +165,14 @@ export function migrate(source) {
props_declaration = `\n${indent}${props_declaration}`;
str.appendRight(state.props_insertion_point, props_declaration);
} else {
const imports = state.needs_run ? `${indent}${run_import}\n` : '';
str.prepend(`<script>\n${imports}${indent}${props_declaration}\n</script>\n\n`);
const imports = state.legacy_imports.size > 0 ? `${indent}${legacy_import}\n` : '';
const script_insertions =
state.script_insertions.size > 0
? `\n${indent}${[...state.script_insertions].join(`\n${indent}`)}`
: '';
str.prepend(
`<script>\n${imports}${indent}${props_declaration}${script_insertions}\n</script>\n\n`
);
added_legacy_import = true;
}
}
@ -193,14 +219,21 @@ export function migrate(source) {
}
}
if (state.needs_run && !added_legacy_import) {
if (state.legacy_imports.size > 0 && !added_legacy_import) {
const script_insertions =
state.script_insertions.size > 0
? `\n${indent}${[...state.script_insertions].join(`\n${indent}`)}`
: '';
if (parsed.instance) {
str.appendRight(
/** @type {number} */ (parsed.instance.content.start),
`\n${indent}${run_import}\n`
`\n${indent}${legacy_import}${script_insertions}\n`
);
} else {
str.prepend(`<script>\n${indent}${run_import}\n</script>\n\n`);
str.prepend(
`<script>\n${indent}${legacy_import}\n${indent}${script_insertions}\n</script>\n\n`
);
}
}
@ -221,11 +254,10 @@ export function migrate(source) {
* props: Array<{ local: string; exported: string; init: string; bindable: boolean; slot_name?: string; optional: boolean; type: string; comment?: string }>;
* props_insertion_point: number;
* has_props_rune: boolean;
* props_name: string;
* rest_props_name: string;
* end: number;
* run_name: string;
* needs_run: boolean;
* names: Record<string, string>;
* legacy_imports: Set<string>;
* script_insertions: Set<string>
* }} State
*/
@ -476,7 +508,7 @@ const instance_script = {
}
}
state.needs_run = true;
state.legacy_imports.add('run');
const is_block_stmt = node.body.type === 'BlockStatement';
const start_end = /** @type {number} */ (node.body.start);
// TODO try to find out if we can use $derived.by instead?
@ -484,7 +516,7 @@ const instance_script = {
state.str.update(
/** @type {number} */ (node.start),
start_end + 1,
`${state.run_name}(() => {`
`${state.names.run}(() => {`
);
const end = /** @type {number} */ (node.body.end);
state.str.update(end - 1, end, '});');
@ -492,7 +524,7 @@ const instance_script = {
state.str.update(
/** @type {number} */ (node.start),
start_end,
`${state.run_name}(() => {\n${state.indent}`
`${state.names.run}(() => {\n${state.indent}`
);
state.str.indent(state.indent, {
exclude: [
@ -667,6 +699,16 @@ function extract_type_and_comment(declarator, str, path) {
return { type: 'any', comment };
}
// Ensure modifiers are applied in the same order as Svelte 4
const modifier_order = [
'preventDefault',
'stopPropagation',
'stopImmediatePropagation',
'self',
'trusted',
'once'
];
/**
* @param {AST.RegularElement | AST.SvelteElement | AST.SvelteWindow | AST.SvelteDocument | AST.SvelteBody} element
* @param {State} state
@ -688,198 +730,76 @@ function handle_events(element, state) {
}
for (const [name, nodes] of handlers) {
// turn on:click into a prop
let exported = name;
if (!regex_is_valid_identifier.test(name)) {
exported = `'${exported}'`;
}
// Check if prop already set, could happen when on:click on different elements
let local = state.props.find((prop) => prop.exported === exported)?.local;
const last = nodes[nodes.length - 1];
const payload_name =
last.expression?.type === 'ArrowFunctionExpression' &&
last.expression.params[0]?.type === 'Identifier'
? last.expression.params[0].name
: generate_event_name(last, state);
let prepend = '';
for (let i = 0; i < nodes.length - 1; i += 1) {
const node = nodes[i];
const indent = get_indent(state, node, element);
if (node.expression) {
let body = '';
if (node.expression.type === 'ArrowFunctionExpression') {
body = state.str.original.substring(
/** @type {number} */ (node.expression.body.start),
/** @type {number} */ (node.expression.body.end)
);
} else {
body = `${state.str.original.substring(
/** @type {number} */ (node.expression.start),
/** @type {number} */ (node.expression.end)
)}();`;
}
const handlers = [];
for (const modifier of node.modifiers) {
if (modifier === 'stopPropagation') {
body = `\n${indent}${payload_name}.stopPropagation();\n${body}`;
} else if (modifier === 'preventDefault') {
body = `\n${indent}${payload_name}.preventDefault();\n${body}`;
} else if (modifier === 'stopImmediatePropagation') {
body = `\n${indent}${payload_name}.stopImmediatePropagation();\n${body}`;
} else {
body = `\n${indent}// @migration-task: incorporate ${modifier} modifier\n${body}`;
}
}
let first = null;
prepend += `\n${indent}${body}\n`;
for (const node of nodes) {
/** @type {string} */
let body;
if (node.expression) {
body = state.str.original.substring(
/** @type {number} */ (node.expression.start),
/** @type {number} */ (node.expression.end)
);
} else {
if (!local) {
local = state.scope.generate(`on${node.name}`);
state.props.push({
local,
exported,
init: '',
bindable: false,
optional: true,
type: '(event: any) => void'
});
}
prepend += `\n${indent}${local}?.(${payload_name});\n`;
body = `${state.names.bubble}('${node.name}')`;
state.legacy_imports.add('createBubbler');
state.script_insertions.add(
`const ${state.names.bubble} = ${state.names.createBubbler}();`
);
}
state.str.remove(node.start, node.end);
}
const has_passive = node.modifiers.includes('passive');
const has_nonpassive = node.modifiers.includes('nonpassive');
if (last.expression) {
// remove : from on:click
state.str.remove(last.start + 2, last.start + 3);
// remove modifiers
if (last.modifiers.length > 0) {
state.str.remove(
last.start + last.name.length + 3,
state.str.original.indexOf('=', last.start)
);
}
if (last.modifiers.includes('capture')) {
state.str.appendRight(last.start + last.name.length + 3, 'capture');
}
const modifiers = modifier_order.filter((modifier) => node.modifiers.includes(modifier));
const indent = get_indent(state, last, element);
for (const modifier of last.modifiers) {
if (modifier === 'stopPropagation') {
prepend += `\n${indent}${payload_name}.stopPropagation();\n`;
} else if (modifier === 'preventDefault') {
prepend += `\n${indent}${payload_name}.preventDefault();\n`;
} else if (modifier === 'stopImmediatePropagation') {
prepend += `\n${indent}${payload_name}.stopImmediatePropagation();\n`;
} else if (modifier !== 'capture') {
prepend += `\n${indent}// @migration-task: incorporate ${modifier} modifier\n`;
}
for (const modifier of modifiers) {
state.legacy_imports.add(modifier);
body = `${state.names[modifier]}(${body})`;
}
if (prepend) {
let pos = last.expression.start;
if (last.expression.type === 'ArrowFunctionExpression') {
pos = last.expression.body.start;
if (
last.expression.params.length > 0 &&
last.expression.params[0].type !== 'Identifier'
) {
const start = /** @type {number} */ (last.expression.params[0].start);
const end = /** @type {number} */ (last.expression.params[0].end);
// replace event payload with generated one that others use,
// then destructure generated payload param into what the user wrote
state.str.overwrite(start, end, payload_name);
prepend = `let ${state.str.original.substring(
start,
end
)} = ${payload_name};\n${prepend}`;
} else if (last.expression.params.length === 0) {
// add generated payload param to arrow function
const pos = state.str.original.lastIndexOf(')', last.expression.body.start);
state.str.prependLeft(pos, payload_name);
}
if (has_passive || has_nonpassive) {
const action = has_passive ? 'passive' : 'nonpassive';
state.legacy_imports.add(action);
const needs_curlies = last.expression.body.type !== 'BlockStatement';
const end = /** @type {number} */ (last.expression.body.end) - (needs_curlies ? 0 : 1);
pos = /** @type {number} */ (pos) + (needs_curlies ? 0 : 1);
if (needs_curlies && state.str.original[pos - 1] === '(') {
// Prettier does something like on:click={() => (foo = true)}, we need to remove the braces in this case
state.str.update(pos - 1, pos, `{${prepend}${indent}`);
state.str.update(end, end + 1, `\n${indent.slice(state.indent.length)}}`);
} else {
state.str.prependRight(pos, `${needs_curlies ? '{' : ''}${prepend}${indent}`);
state.str.appendRight(
end,
`\n${indent.slice(state.indent.length)}${needs_curlies ? '}' : ''}`
);
}
} else {
state.str.update(
/** @type {number} */ (last.expression.start),
/** @type {number} */ (last.expression.end),
`(${payload_name}) => {${prepend}\n${indent}${state.str.original.substring(
/** @type {number} */ (last.expression.start),
/** @type {number} */ (last.expression.end)
)}?.(${payload_name});\n}`
);
}
}
} else {
// turn on:click into a prop
// Check if prop already set, could happen when on:click on different elements
if (!local) {
local = state.scope.generate(`on${last.name}`);
state.props.push({
local,
exported,
init: '',
bindable: false,
optional: true,
type: '(event: any) => void'
});
}
state.str.overwrite(
node.start,
node.end,
`use:${state.names[action]}={['${node.name}', () => ${body}]}`
);
} else {
if (first) {
let start = node.start;
let end = node.end;
let replacement = '';
if (!prepend) {
if (exported === local) {
replacement = `{${name}}`;
while (/[\s\n]/.test(state.str.original[start - 1])) start -= 1;
state.str.remove(start, end);
} else {
replacement = `${name}={${local}}`;
first = node;
}
} else {
replacement = `${name}={(${payload_name}) => {${prepend}\n${state.indent}${local}?.(${payload_name});\n}}`;
}
state.str.update(last.start, last.end, replacement);
handlers.push(body);
}
}
}
}
/**
* Returns the next indentation level of the first node that has all-whitespace before it
* @param {State} state
* @param {Array<{start: number; end: number}>} nodes
*/
function get_indent(state, ...nodes) {
let indent = state.indent;
if (first) {
/** @type {string} */
let replacement;
for (const node of nodes) {
const line_start = state.str.original.lastIndexOf('\n', node.start);
indent = state.str.original.substring(line_start + 1, node.start);
if (handlers.length > 1) {
state.legacy_imports.add('handlers');
replacement = `${name}={${state.names.handlers}(${handlers.join(', ')})}`;
} else {
const handler = handlers[0];
replacement = handler === name ? `{${handler}}` : `${name}={${handler}}`;
}
if (indent.trim() === '') {
indent = state.indent + indent;
return indent;
} else {
indent = state.indent;
state.str.overwrite(first.start, first.end, replacement);
}
}
return indent;
}
/**
@ -937,19 +857,19 @@ function handle_identifier(node, state, path) {
state.str.update(
/** @type {number} */ (node.start),
/** @type {number} */ (node.end),
state.props_name
state.names.props
);
} else {
const binding = state.scope.get(node.name);
if (binding?.kind === 'bindable_prop') {
state.str.prependLeft(/** @type {number} */ (node.start), `${state.props_name}.`);
state.str.prependLeft(/** @type {number} */ (node.start), `${state.names.props}.`);
}
}
} else if (node.name === '$$restProps' && state.analysis.uses_rest_props) {
state.str.update(
/** @type {number} */ (node.start),
/** @type {number} */ (node.end),
state.rest_props_name
state.names.rest
);
} else if (node.name === '$$slots' && state.analysis.uses_slots) {
if (parent?.type === 'MemberExpression') {

@ -1,4 +1,11 @@
/** @import { ActionReturn } from 'svelte/action' */
import { noop } from '../../../shared/utils.js';
import { user_pre_effect } from '../../reactivity/effects.js';
import { on } from '../elements/events.js';
/**
* Substitute for the `trusted` event modifier
* @deprecated
* @param {(event: Event, ...args: Array<unknown>) => void} fn
* @returns {(event: Event, ...args: unknown[]) => void}
*/
@ -7,12 +14,14 @@ export function trusted(fn) {
var event = /** @type {Event} */ (args[0]);
if (event.isTrusted) {
// @ts-ignore
fn.apply(this, args);
fn?.apply(this, args);
}
};
}
/**
* Substitute for the `self` event modifier
* @deprecated
* @param {(event: Event, ...args: Array<unknown>) => void} fn
* @returns {(event: Event, ...args: unknown[]) => void}
*/
@ -22,12 +31,14 @@ export function self(fn) {
// @ts-ignore
if (event.target === this) {
// @ts-ignore
fn.apply(this, args);
fn?.apply(this, args);
}
};
}
/**
* Substitute for the `stopPropagation` event modifier
* @deprecated
* @param {(event: Event, ...args: Array<unknown>) => void} fn
* @returns {(event: Event, ...args: unknown[]) => void}
*/
@ -36,11 +47,13 @@ export function stopPropagation(fn) {
var event = /** @type {Event} */ (args[0]);
event.stopPropagation();
// @ts-ignore
return fn.apply(this, args);
return fn?.apply(this, args);
};
}
/**
* Substitute for the `once` event modifier
* @deprecated
* @param {(event: Event, ...args: Array<unknown>) => void} fn
* @returns {(event: Event, ...args: unknown[]) => void}
*/
@ -52,11 +65,13 @@ export function once(fn) {
ran = true;
// @ts-ignore
return fn.apply(this, args);
return fn?.apply(this, args);
};
}
/**
* Substitute for the `stopImmediatePropagation` event modifier
* @deprecated
* @param {(event: Event, ...args: Array<unknown>) => void} fn
* @returns {(event: Event, ...args: unknown[]) => void}
*/
@ -65,11 +80,13 @@ export function stopImmediatePropagation(fn) {
var event = /** @type {Event} */ (args[0]);
event.stopImmediatePropagation();
// @ts-ignore
return fn.apply(this, args);
return fn?.apply(this, args);
};
}
/**
* Substitute for the `preventDefault` event modifier
* @deprecated
* @param {(event: Event, ...args: Array<unknown>) => void} fn
* @returns {(event: Event, ...args: unknown[]) => void}
*/
@ -78,6 +95,34 @@ export function preventDefault(fn) {
var event = /** @type {Event} */ (args[0]);
event.preventDefault();
// @ts-ignore
return fn.apply(this, args);
return fn?.apply(this, args);
};
}
/**
* Substitute for the `passive` event modifier, implemented as an action
* @deprecated
* @param {HTMLElement} node
* @param {[event: string, handler: () => EventListener]} options
*/
export function passive(node, [event, handler]) {
user_pre_effect(() => {
return on(node, event, handler() ?? noop, {
passive: true
});
});
}
/**
* Substitute for the `nonpassive` event modifier, implemented as an action
* @deprecated
* @param {HTMLElement} node
* @param {[event: string, handler: () => EventListener]} options
*/
export function nonpassive(node, [event, handler]) {
user_pre_effect(() => {
return on(node, event, handler() ?? noop, {
passive: false
});
});
}

@ -1,9 +1,10 @@
/** @import { ComponentConstructorOptions, ComponentType, SvelteComponent, Component } from 'svelte' */
import { mutable_source, set } from '../internal/client/reactivity/sources.js';
import { user_pre_effect } from '../internal/client/reactivity/effects.js';
import { mutable_source, set } from '../internal/client/reactivity/sources.js';
import { hydrate, mount, unmount } from '../internal/client/render.js';
import { flush_sync, get } from '../internal/client/runtime.js';
import { define_property } from '../internal/shared/utils.js';
import { component_context, flush_sync, get } from '../internal/client/runtime.js';
import { lifecycle_outside_component } from '../internal/shared/errors.js';
import { define_property, is_array } from '../internal/shared/utils.js';
/**
* Takes the same options as a Svelte 4 component and the component function and returns a Svelte 4 compatible component.
@ -170,3 +171,79 @@ class Svelte4Component {
export function run(fn) {
user_pre_effect(fn);
}
/**
* Function to mimic the multiple listeners available in svelte 4
* @deprecated
* @param {EventListener[]} handlers
* @returns {EventListener}
*/
export function handlers(...handlers) {
return function (event) {
const { stopImmediatePropagation } = event;
let stopped = false;
event.stopImmediatePropagation = () => {
stopped = true;
stopImmediatePropagation.call(event);
};
const errors = [];
for (const handler of handlers) {
try {
// @ts-expect-error `this` is not typed
handler?.call(this, event);
} catch (e) {
errors.push(e);
}
if (stopped) {
break;
}
}
for (let error of errors) {
queueMicrotask(() => {
throw error;
});
}
};
}
/**
* Function to create a `bubble` function that mimic the behavior of `on:click` without handler available in svelte 4.
* @deprecated Use this only as a temporary solution to migrate your automatically delegated events in Svelte 5.
*/
export function createBubbler() {
const active_component_context = component_context;
if (active_component_context === null) {
lifecycle_outside_component('createBubbler');
}
return (/**@type {string}*/ type) => (/**@type {Event}*/ event) => {
const events = /** @type {Record<string, Function | Function[]>} */ (
active_component_context.s.$$events
)?.[/** @type {any} */ (type)];
if (events) {
const callbacks = is_array(events) ? events.slice() : [events];
for (const fn of callbacks) {
fn.call(active_component_context.x, event);
}
return !event.defaultPrevented;
}
return true;
};
}
export {
once,
preventDefault,
self,
stopImmediatePropagation,
stopPropagation,
trusted,
passive,
nonpassive
} from '../internal/client/dom/legacy/event-modifiers.js';

@ -0,0 +1,92 @@
<script>
let handlers;
let stopPropagation;
let preventDefault;
let stopImmediatePropagation;
let once;
let trusted;
let self;
let createBubbler;
let bubble;
let passive;
let nonpassive;
</script>
<button on:click={() => console.log('hi')} on:click>click me</button>
<button on:click={function(){ console.log('hi') }} on:click>click me</button>
<button on:click={() => console.log('before')} on:click on:click={() => console.log('after')}
>click me</button
>
<button on:click on:click={foo}>click me</button>
<button on:click>click me</button>
<button on:dblclick={() => console.log('hi')}>click me</button>
<button on:toggle>click me</button>
<button on:custom-event={() => 'hi'}>click me</button>
<button on:custom-event-bubble>click me</button>
<button on:click|preventDefault={() => (searching = true)}>click me</button>
<button on:click|preventDefault={() => ''}>click me</button>
<button on:click|stopPropagation={() => {}}>click me</button>
<button on:click|stopImmediatePropagation={() => ''}>click me</button>
<button on:click|capture={() => ''}>click me</button>
<button on:click|self={() => ''}>click me</button>
<button on:click|trusted={() => ''}>click me</button>
<button on:click|once={() => ''}>click me</button>
<button on:click|preventDefault|stopPropagation={() => ''}>click me</button>
<button on:click|stopPropagation|stopImmediatePropagation={() => {}}>click me</button>
<button on:click|stopImmediatePropagation|self={() => ''}>click me</button>
<button on:click|self|trusted={() => ''}>click me</button>
<button on:click|trusted|once={() => ''}>click me</button>
<button on:click|once|preventDefault={() => ''}>click me</button>
<button on:click|passive>click me</button>
<button on:click|nonpassive>click me</button>
<button on:click|passive={()=>''}>click me</button>
<button on:click|nonpassive={()=>''}>click me</button>
<button on:click|passive={foo}>click me</button>
<button on:click|nonpassive={foo}>click me</button>
<button on:click|stopPropagation|passive={()=>''}>click me</button>
<button on:click|trusted|nonpassive={()=>''}>click me</button>
<button
on:click|passive={()=>''}
on:click
on:click={()=>''}
>click me</button>
<button
on:click|nonpassive={()=>''}
on:click
on:click={()=>''}
>click me</button>
<button
on:click
on:click={foo}
on:blur={foo}
on:click={()=>''}
on:click|trusted|preventDefault|once={()=>''}
on:blur|trusted|preventDefault|once
>
click me
</button>
<Button on:click={() => 'leave untouched'} on:click>click me</Button>
<div>
<button
on:click={() => {
console.log('hi');
}}>click me</button
>
<button
on:click|preventDefault={() => {
console.log('hi');
}}>click me</button
>
<button on:click|preventDefault={() => (count += 1)}>click me</button>
</div>

@ -0,0 +1,89 @@
<script>
import { createBubbler as createBubbler_1, handlers as handlers_1, preventDefault as preventDefault_1, stopPropagation as stopPropagation_1, stopImmediatePropagation as stopImmediatePropagation_1, self as self_1, trusted as trusted_1, once as once_1, passive as passive_1, nonpassive as nonpassive_1 } from 'svelte/legacy';
const bubble_1 = createBubbler_1();
let handlers;
let stopPropagation;
let preventDefault;
let stopImmediatePropagation;
let once;
let trusted;
let self;
let createBubbler;
let bubble;
let passive;
let nonpassive;
</script>
<button onclick={handlers_1(() => console.log('hi'), bubble_1('click'))}>click me</button>
<button onclick={handlers_1(function(){ console.log('hi') }, bubble_1('click'))}>click me</button>
<button onclick={handlers_1(() => console.log('before'), bubble_1('click'), () => console.log('after'))}
>click me</button
>
<button onclick={handlers_1(bubble_1('click'), foo)}>click me</button>
<button onclick={bubble_1('click')}>click me</button>
<button ondblclick={() => console.log('hi')}>click me</button>
<button ontoggle={bubble_1('toggle')}>click me</button>
<button oncustom-event={() => 'hi'}>click me</button>
<button oncustom-event-bubble={bubble_1('custom-event-bubble')}>click me</button>
<button onclick={preventDefault_1(() => (searching = true))}>click me</button>
<button onclick={preventDefault_1(() => '')}>click me</button>
<button onclick={stopPropagation_1(() => {})}>click me</button>
<button onclick={stopImmediatePropagation_1(() => '')}>click me</button>
<button onclickcapture={() => ''}>click me</button>
<button onclick={self_1(() => '')}>click me</button>
<button onclick={trusted_1(() => '')}>click me</button>
<button onclick={once_1(() => '')}>click me</button>
<button onclick={stopPropagation_1(preventDefault_1(() => ''))}>click me</button>
<button onclick={stopImmediatePropagation_1(stopPropagation_1(() => {}))}>click me</button>
<button onclick={self_1(stopImmediatePropagation_1(() => ''))}>click me</button>
<button onclick={trusted_1(self_1(() => ''))}>click me</button>
<button onclick={once_1(trusted_1(() => ''))}>click me</button>
<button onclick={once_1(preventDefault_1(() => ''))}>click me</button>
<button use:passive_1={['click', () => bubble_1('click')]}>click me</button>
<button use:nonpassive_1={['click', () => bubble_1('click')]}>click me</button>
<button use:passive_1={['click', () => ()=>'']}>click me</button>
<button use:nonpassive_1={['click', () => ()=>'']}>click me</button>
<button use:passive_1={['click', () => foo]}>click me</button>
<button use:nonpassive_1={['click', () => foo]}>click me</button>
<button use:passive_1={['click', () => stopPropagation_1(()=>'')]}>click me</button>
<button use:nonpassive_1={['click', () => trusted_1(()=>'')]}>click me</button>
<button
use:passive_1={['click', () => ()=>'']}
onclick={handlers_1(bubble_1('click'), ()=>'')}
>click me</button>
<button
use:nonpassive_1={['click', () => ()=>'']}
onclick={handlers_1(bubble_1('click'), ()=>'')}
>click me</button>
<button
onclick={handlers_1(bubble_1('click'), foo, ()=>'', once_1(trusted_1(preventDefault_1(()=>''))))}
onblur={handlers_1(foo, once_1(trusted_1(preventDefault_1(bubble_1('blur')))))}
>
click me
</button>
<Button on:click={() => 'leave untouched'} on:click>click me</Button>
<div>
<button
onclick={() => {
console.log('hi');
}}>click me</button
>
<button
onclick={preventDefault_1(() => {
console.log('hi');
})}>click me</button
>
<button onclick={preventDefault_1(() => (count += 1))}>click me</button>
</div>

@ -1,4 +1,5 @@
<button on:click={() => console.log('hi')} on:click>click me</button>
<button on:click={function(){ console.log('hi') }} on:click>click me</button>
<button on:click={() => console.log('before')} on:click on:click={() => console.log('after')}
>click me</button
>
@ -10,12 +11,58 @@
<button on:custom-event={() => 'hi'}>click me</button>
<button on:custom-event-bubble>click me</button>
<button on:click|preventDefault={() => ''}>click me</button>
<button on:click|preventDefault={() => (searching = true)}>click me</button>
<button on:click|preventDefault={() => ''}>click me</button>
<button on:click|stopPropagation={() => {}}>click me</button>
<button on:click|stopImmediatePropagation={() => ''}>click me</button>
<button on:click|capture={() => ''}>click me</button>
<button on:click|self={() => ''}>click me</button>
<button on:click|trusted={() => ''}>click me</button>
<button on:click|once={() => ''}>click me</button>
<button on:click|preventDefault|stopPropagation={() => ''}>click me</button>
<button on:click|stopPropagation|stopImmediatePropagation={() => {}}>click me</button>
<button on:click|stopImmediatePropagation|self={() => ''}>click me</button>
<button on:click|self|trusted={() => ''}>click me</button>
<button on:click|trusted|self={() => ''}>click me</button>
<button on:click|trusted|once={() => ''}>click me</button>
<button on:click|once|preventDefault={() => ''}>click me</button>
<button on:click|passive>click me</button>
<button on:click|nonpassive>click me</button>
<button on:click|passive={()=>''}>click me</button>
<button on:click|nonpassive={()=>''}>click me</button>
<button on:click|passive={foo}>click me</button>
<button on:click|nonpassive={foo}>click me</button>
<button on:click|stopPropagation|passive={()=>''}>click me</button>
<button on:click|trusted|nonpassive={()=>''}>click me</button>
<button
on:click|passive={()=>''}
on:click
on:click={()=>''}
>click me</button>
<button
on:click|nonpassive={()=>''}
on:click
on:click={()=>{
return 'multiline';
}}
>click me</button>
<button
on:click
on:click={foo}
on:blur={foo}
on:click={()=>''}
on:click|trusted|preventDefault|once={()=>''}
on:blur|trusted|preventDefault|once
>
click me
</button>
<Button on:click={() => 'leave untouched'} on:click>click me</Button>

@ -1,54 +1,68 @@
<script>
/** @type {{onclick?: (event: any) => void, ontoggle?: (event: any) => void, 'oncustom-event-bubble'?: (event: any) => void}} */
let { onclick, ontoggle, 'oncustom-event-bubble': oncustom_event_bubble } = $props();
import { createBubbler, handlers, preventDefault, stopPropagation, stopImmediatePropagation, self, trusted, once, passive, nonpassive } from 'svelte/legacy';
const bubble = createBubbler();
</script>
<button onclick={(event) => {
console.log('hi')
onclick?.(event);
}}>click me</button>
<button onclick={(event) => {
console.log('before')
onclick?.(event);
console.log('after')
}}
<button onclick={handlers(() => console.log('hi'), bubble('click'))}>click me</button>
<button onclick={handlers(function(){ console.log('hi') }, bubble('click'))}>click me</button>
<button onclick={handlers(() => console.log('before'), bubble('click'), () => console.log('after'))}
>click me</button
>
<button onclick={(event) => {
onclick?.(event);
foo?.(event);
}}>click me</button>
<button {onclick}>click me</button>
<button onclick={handlers(bubble('click'), foo)}>click me</button>
<button onclick={bubble('click')}>click me</button>
<button ondblclick={() => console.log('hi')}>click me</button>
<button {ontoggle}>click me</button>
<button ontoggle={bubble('toggle')}>click me</button>
<button oncustom-event={() => 'hi'}>click me</button>
<button oncustom-event-bubble={oncustom_event_bubble}>click me</button>
<button oncustom-event-bubble={bubble('custom-event-bubble')}>click me</button>
<button onclick={(event) => {
event.preventDefault();
''
}}>click me</button>
<button onclick={(event) => {
event.preventDefault();
searching = true
}}>click me</button>
<button onclick={(event) => {
event.stopPropagation();
}}>click me</button>
<button onclick={(event) => {
event.stopImmediatePropagation();
''
}}>click me</button>
<button onclick={preventDefault(() => (searching = true))}>click me</button>
<button onclick={preventDefault(() => '')}>click me</button>
<button onclick={stopPropagation(() => {})}>click me</button>
<button onclick={stopImmediatePropagation(() => '')}>click me</button>
<button onclickcapture={() => ''}>click me</button>
<button onclick={(event) => {
// @migration-task: incorporate self modifier
''
}}>click me</button>
<button onclick={self(() => '')}>click me</button>
<button onclick={trusted(() => '')}>click me</button>
<button onclick={once(() => '')}>click me</button>
<button onclick={stopPropagation(preventDefault(() => ''))}>click me</button>
<button onclick={stopImmediatePropagation(stopPropagation(() => {}))}>click me</button>
<button onclick={self(stopImmediatePropagation(() => ''))}>click me</button>
<button onclick={trusted(self(() => ''))}>click me</button>
<button onclick={trusted(self(() => ''))}>click me</button>
<button onclick={once(trusted(() => ''))}>click me</button>
<button onclick={once(preventDefault(() => ''))}>click me</button>
<button use:passive={['click', () => bubble('click')]}>click me</button>
<button use:nonpassive={['click', () => bubble('click')]}>click me</button>
<button use:passive={['click', () => ()=>'']}>click me</button>
<button use:nonpassive={['click', () => ()=>'']}>click me</button>
<button use:passive={['click', () => foo]}>click me</button>
<button use:nonpassive={['click', () => foo]}>click me</button>
<button use:passive={['click', () => stopPropagation(()=>'')]}>click me</button>
<button use:nonpassive={['click', () => trusted(()=>'')]}>click me</button>
<button
use:passive={['click', () => ()=>'']}
onclick={handlers(bubble('click'), ()=>'')}
>click me</button>
<button
use:nonpassive={['click', () => ()=>'']}
onclick={handlers(bubble('click'), ()=>{
return 'multiline';
})}
>click me</button>
<button
onclick={handlers(bubble('click'), foo, ()=>'', once(trusted(preventDefault(()=>''))))}
onblur={handlers(foo, once(trusted(preventDefault(bubble('blur')))))}
>
click me
</button>
<Button on:click={() => 'leave untouched'} on:click>click me</Button>
@ -59,15 +73,9 @@
}}>click me</button
>
<button
onclick={(event) => {
event.preventDefault();
onclick={preventDefault(() => {
console.log('hi');
}}>click me</button
})}>click me</button
>
<button onclick={(event) => {
event.preventDefault();
count += 1
}}>click me</button>
</div>
<button onclick={preventDefault(() => (count += 1))}>click me</button>
</div>

@ -1632,6 +1632,56 @@ declare module 'svelte/legacy' {
* @deprecated Use this only as a temporary solution to migrate your component code to Svelte 5.
* */
export function run(fn: () => void | (() => void)): void;
/**
* Function to mimic the multiple listeners available in svelte 4
* @deprecated
* */
export function handlers(...handlers: EventListener[]): EventListener;
/**
* Function to create a `bubble` function that mimic the behavior of `on:click` without handler available in svelte 4.
* @deprecated Use this only as a temporary solution to migrate your automatically delegated events in Svelte 5.
*/
export function createBubbler(): (type: string) => (event: Event) => boolean;
/**
* Substitute for the `trusted` event modifier
* @deprecated
* */
export function trusted(fn: (event: Event, ...args: Array<unknown>) => void): (event: Event, ...args: unknown[]) => void;
/**
* Substitute for the `self` event modifier
* @deprecated
* */
export function self(fn: (event: Event, ...args: Array<unknown>) => void): (event: Event, ...args: unknown[]) => void;
/**
* Substitute for the `stopPropagation` event modifier
* @deprecated
* */
export function stopPropagation(fn: (event: Event, ...args: Array<unknown>) => void): (event: Event, ...args: unknown[]) => void;
/**
* Substitute for the `once` event modifier
* @deprecated
* */
export function once(fn: (event: Event, ...args: Array<unknown>) => void): (event: Event, ...args: unknown[]) => void;
/**
* Substitute for the `stopImmediatePropagation` event modifier
* @deprecated
* */
export function stopImmediatePropagation(fn: (event: Event, ...args: Array<unknown>) => void): (event: Event, ...args: unknown[]) => void;
/**
* Substitute for the `preventDefault` event modifier
* @deprecated
* */
export function preventDefault(fn: (event: Event, ...args: Array<unknown>) => void): (event: Event, ...args: unknown[]) => void;
/**
* Substitute for the `passive` event modifier, implemented as an action
* @deprecated
* */
export function passive(node: HTMLElement, [event, handler]: [event: string, handler: () => EventListener]): void;
/**
* Substitute for the `nonpassive` event modifier, implemented as an action
* @deprecated
* */
export function nonpassive(node: HTMLElement, [event, handler]: [event: string, handler: () => EventListener]): void;
export {};
}

Loading…
Cancel
Save