fix: migrate events to be more inline with svelte 4

pull/13362/head
paoloricciuti 2 years ago
parent c5fa0b46e1
commit 528da1c1b4

@ -54,7 +54,9 @@ export function migrate(source) {
rest_props_name: analysis.root.unique('rest').name, rest_props_name: analysis.root.unique('rest').name,
end: source.length, end: source.length,
run_name: analysis.root.unique('run').name, run_name: analysis.root.unique('run').name,
needs_run: false listener_name: analysis.root.unique('listener').name,
needs_run: false,
needs_listener: false
}; };
if (parsed.module) { if (parsed.module) {
@ -71,7 +73,7 @@ export function migrate(source) {
state = { ...state, scope: analysis.template.scope }; state = { ...state, scope: analysis.template.scope };
walk(parsed.fragment, state, template); walk(parsed.fragment, state, template);
const run_import = `import { run${state.run_name === 'run' ? '' : ` as ${state.run_name}`} } from 'svelte/legacy';`; const legacy_import = `import { ${state.needs_run ? `run${state.run_name === 'run' ? '' : ` as ${state.run_name}`}` : ''}${state.needs_listener ? `${state.needs_run ? ', ' : ''}listener${state.listener_name === 'listener' ? '' : ` as ${state.listener_name}`}` : ''} } from 'svelte/legacy';`;
let added_legacy_import = false; let added_legacy_import = false;
if (state.props.length > 0 || analysis.uses_rest_props || analysis.uses_props) { if (state.props.length > 0 || analysis.uses_rest_props || analysis.uses_props) {
@ -145,7 +147,8 @@ export function migrate(source) {
props_declaration = `\n${indent}${props_declaration}`; props_declaration = `\n${indent}${props_declaration}`;
str.appendRight(state.props_insertion_point, props_declaration); str.appendRight(state.props_insertion_point, props_declaration);
} else { } else {
const imports = state.needs_run ? `${indent}${run_import}\n` : ''; const imports =
state.needs_run || state.needs_listener ? `${indent}${legacy_import}\n` : '';
str.prepend(`<script>\n${imports}${indent}${props_declaration}\n</script>\n\n`); str.prepend(`<script>\n${imports}${indent}${props_declaration}\n</script>\n\n`);
added_legacy_import = true; added_legacy_import = true;
} }
@ -193,14 +196,14 @@ export function migrate(source) {
} }
} }
if (state.needs_run && !added_legacy_import) { if ((state.needs_run || state.needs_listener) && !added_legacy_import) {
if (parsed.instance) { if (parsed.instance) {
str.appendRight( str.appendRight(
/** @type {number} */ (parsed.instance.content.start), /** @type {number} */ (parsed.instance.content.start),
`\n${indent}${run_import}\n` `\n${indent}${legacy_import}\n`
); );
} else { } else {
str.prepend(`<script>\n${indent}${run_import}\n</script>\n\n`); str.prepend(`<script>\n${indent}${legacy_import}\n</script>\n\n`);
} }
} }
@ -225,7 +228,9 @@ export function migrate(source) {
* rest_props_name: string; * rest_props_name: string;
* end: number; * end: number;
* run_name: string; * run_name: string;
* listener_name: string;
* needs_run: boolean; * needs_run: boolean;
* needs_listener: boolean;
* }} State * }} State
*/ */
@ -702,137 +707,22 @@ function handle_events(element, state) {
last.expression.params[0]?.type === 'Identifier' last.expression.params[0]?.type === 'Identifier'
? last.expression.params[0].name ? last.expression.params[0].name
: generate_event_name(last, state); : generate_event_name(last, state);
let prepend = '';
for (let i = 0; i < nodes.length - 1; i += 1) { for (let i = 0; i < nodes.length; i += 1) {
let prepend = '';
const node = nodes[i]; const node = nodes[i];
const indent = get_indent(state, node, element); const indent = get_indent(state, node, element);
const args = `,"${node.name}"${node.modifiers.length > 0 ? `,${JSON.stringify(node.modifiers)}` : ''}`;
if (node.expression) { if (node.expression) {
let body = ''; let body = state.str.original.substring(
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.start),
/** @type {number} */ (node.expression.end) /** @type {number} */ (node.expression.end)
)}();`;
}
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}`;
}
}
prepend += `\n${indent}${body}\n`;
} 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`;
}
state.str.remove(node.start, node.end);
}
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)
); );
} prepend += `use:${state.listener_name}={[${body}${args}]}`;
if (last.modifiers.includes('capture')) { state.needs_listener = true;
state.str.appendRight(last.start + last.name.length + 3, 'capture');
}
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`;
}
}
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);
}
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 { } 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) { if (!local) {
local = state.scope.generate(`on${last.name}`); local = state.scope.generate(`on${node.name}`);
state.props.push({ state.props.push({
local, local,
exported, exported,
@ -842,19 +732,11 @@ function handle_events(element, state) {
type: '(event: any) => void' type: '(event: any) => void'
}); });
} }
prepend += `use:${state.listener_name}={[(${payload_name})=>{${local}?.(${payload_name});}${args}]}`;
let replacement = ''; state.needs_listener = true;
if (!prepend) {
if (exported === local) {
replacement = `{${name}}`;
} else {
replacement = `${name}={${local}}`;
}
} else {
replacement = `${name}={(${payload_name}) => {${prepend}\n${state.indent}${local}?.(${payload_name});\n}}`;
} }
state.str.update(last.start, last.end, replacement); state.str.overwrite(node.start, node.end, prepend);
} }
} }
} }

@ -170,3 +170,34 @@ class Svelte4Component {
export function run(fn) { export function run(fn) {
user_pre_effect(fn); user_pre_effect(fn);
} }
/**
* @param {EventTarget} node
* @param {[EventListener, string, string[]]} options
*/
export function listener(node, [handler, type, modifiers = []]) {
const aborter = new AbortController();
node.addEventListener(
type,
(e) => {
for (let modifier of modifiers) {
// @ts-expect-error modifier is just a string
if (modifier in e && typeof e[modifier] === 'function') {
// @ts-expect-error modifier is just a string
e[modifier]();
}
}
handler?.(e);
},
{
signal: aborter.signal,
passive: modifiers.includes('passive'),
capture: modifiers.includes('capture')
}
);
return {
destroy() {
aborter.abort();
}
};
}

@ -1,73 +1,40 @@
<script> <script>
import { listener } from 'svelte/legacy';
/** @type {{onclick?: (event: any) => void, ontoggle?: (event: any) => void, 'oncustom-event-bubble'?: (event: any) => void}} */ /** @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(); let { onclick, ontoggle, 'oncustom-event-bubble': oncustom_event_bubble } = $props();
</script> </script>
<button onclick={(event) => { <button use:listener={[() => console.log('hi'),"click"]} use:listener={[(event)=>{onclick?.(event);},"click"]}>click me</button>
console.log('hi') <button use:listener={[() => console.log('before'),"click"]} use:listener={[(event)=>{onclick?.(event);},"click"]} use:listener={[() => console.log('after'),"click"]}
onclick?.(event);
}}>click me</button>
<button onclick={(event) => {
console.log('before')
onclick?.(event);
console.log('after')
}}
>click me</button >click me</button
> >
<button onclick={(event) => { <button use:listener={[(event)=>{onclick?.(event);},"click"]} use:listener={[foo,"click"]}>click me</button>
onclick?.(event); <button use:listener={[(event)=>{onclick?.(event);},"click"]}>click me</button>
foo?.(event);
}}>click me</button>
<button {onclick}>click me</button>
<button ondblclick={() => console.log('hi')}>click me</button> <button use:listener={[() => console.log('hi'),"dblclick"]}>click me</button>
<button {ontoggle}>click me</button> <button use:listener={[(event)=>{ontoggle?.(event);},"toggle"]}>click me</button>
<button oncustom-event={() => 'hi'}>click me</button> <button use:listener={[() => 'hi',"custom-event"]}>click me</button>
<button oncustom-event-bubble={oncustom_event_bubble}>click me</button> <button use:listener={[(event)=>{oncustom_event_bubble?.(event);},"custom-event-bubble"]}>click me</button>
<button onclick={(event) => { <button use:listener={[() => '',"click",["preventDefault"]]}>click me</button>
event.preventDefault(); <button use:listener={[() => (searching = true),"click",["preventDefault"]]}>click me</button>
'' <button use:listener={[() => {},"click",["stopPropagation"]]}>click me</button>
}}>click me</button> <button use:listener={[() => '',"click",["stopImmediatePropagation"]]}>click me</button>
<button onclick={(event) => { <button use:listener={[() => '',"click",["capture"]]}>click me</button>
event.preventDefault(); <button use:listener={[() => '',"click",["self"]]}>click me</button>
searching = true
}}>click me</button>
<button onclick={(event) => {
event.stopPropagation();
}}>click me</button>
<button onclick={(event) => {
event.stopImmediatePropagation();
''
}}>click me</button>
<button onclickcapture={() => ''}>click me</button>
<button onclick={(event) => {
// @migration-task: incorporate self modifier
''
}}>click me</button>
<Button on:click={() => 'leave untouched'} on:click>click me</Button> <Button on:click={() => 'leave untouched'} on:click>click me</Button>
<div> <div>
<button <button
onclick={() => { use:listener={[() => {
console.log('hi'); console.log('hi');
}}>click me</button },"click"]}>click me</button
> >
<button <button
onclick={(event) => { use:listener={[() => {
event.preventDefault();
console.log('hi'); console.log('hi');
},"click",["preventDefault"]]}>click me</button
}}>click me</button
> >
<button onclick={(event) => { <button use:listener={[() => (count += 1),"click",["preventDefault"]]}>click me</button>
event.preventDefault();
count += 1
}}>click me</button>
</div> </div>

@ -1,8 +1,10 @@
<script lang="ts"> <script lang="ts">
import { listener } from 'svelte/legacy';
// here is a comment // here is a comment
let div: HTMLIFrameElement = $state(); let div: HTMLIFrameElement = $state();
let count = $state(0); let count = $state(0);
</script> </script>
<div bind:this={div}></div> <div bind:this={div}></div>
<button onclick={() => count++}>{count}</button> <button use:listener={[() => count++,"click"]}>{count}</button>
Loading…
Cancel
Save