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,
end: source.length,
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) {
@ -71,7 +73,7 @@ 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 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;
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}`;
str.appendRight(state.props_insertion_point, props_declaration);
} 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`);
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) {
str.appendRight(
/** @type {number} */ (parsed.instance.content.start),
`\n${indent}${run_import}\n`
`\n${indent}${legacy_import}\n`
);
} 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;
* end: number;
* run_name: string;
* listener_name: string;
* needs_run: boolean;
* needs_listener: boolean;
* }} State
*/
@ -702,38 +707,19 @@ function handle_events(element, state) {
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) {
for (let i = 0; i < nodes.length; i += 1) {
let prepend = '';
const node = nodes[i];
const indent = get_indent(state, node, element);
const args = `,"${node.name}"${node.modifiers.length > 0 ? `,${JSON.stringify(node.modifiers)}` : ''}`;
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)
)}();`;
}
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`;
let body = state.str.original.substring(
/** @type {number} */ (node.expression.start),
/** @type {number} */ (node.expression.end)
);
prepend += `use:${state.listener_name}={[${body}${args}]}`;
state.needs_listener = true;
} else {
if (!local) {
local = state.scope.generate(`on${node.name}`);
@ -746,115 +732,11 @@ function handle_events(element, state) {
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)
);
}
if (last.modifiers.includes('capture')) {
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 {
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'
});
}
let replacement = '';
if (!prepend) {
if (exported === local) {
replacement = `{${name}}`;
} else {
replacement = `${name}={${local}}`;
}
} else {
replacement = `${name}={(${payload_name}) => {${prepend}\n${state.indent}${local}?.(${payload_name});\n}}`;
prepend += `use:${state.listener_name}={[(${payload_name})=>{${local}?.(${payload_name});}${args}]}`;
state.needs_listener = true;
}
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) {
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>
import { listener } from 'svelte/legacy';
/** @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();
</script>
<button onclick={(event) => {
console.log('hi')
onclick?.(event);
}}>click me</button>
<button onclick={(event) => {
console.log('before')
onclick?.(event);
console.log('after')
}}
<button use:listener={[() => console.log('hi'),"click"]} use:listener={[(event)=>{onclick?.(event);},"click"]}>click me</button>
<button use:listener={[() => console.log('before'),"click"]} use:listener={[(event)=>{onclick?.(event);},"click"]} use:listener={[() => console.log('after'),"click"]}
>click me</button
>
<button onclick={(event) => {
onclick?.(event);
foo?.(event);
}}>click me</button>
<button {onclick}>click me</button>
<button use:listener={[(event)=>{onclick?.(event);},"click"]} use:listener={[foo,"click"]}>click me</button>
<button use:listener={[(event)=>{onclick?.(event);},"click"]}>click me</button>
<button ondblclick={() => console.log('hi')}>click me</button>
<button {ontoggle}>click me</button>
<button oncustom-event={() => 'hi'}>click me</button>
<button oncustom-event-bubble={oncustom_event_bubble}>click me</button>
<button use:listener={[() => console.log('hi'),"dblclick"]}>click me</button>
<button use:listener={[(event)=>{ontoggle?.(event);},"toggle"]}>click me</button>
<button use:listener={[() => 'hi',"custom-event"]}>click me</button>
<button use:listener={[(event)=>{oncustom_event_bubble?.(event);},"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 onclickcapture={() => ''}>click me</button>
<button onclick={(event) => {
// @migration-task: incorporate self modifier
''
}}>click me</button>
<button use:listener={[() => '',"click",["preventDefault"]]}>click me</button>
<button use:listener={[() => (searching = true),"click",["preventDefault"]]}>click me</button>
<button use:listener={[() => {},"click",["stopPropagation"]]}>click me</button>
<button use:listener={[() => '',"click",["stopImmediatePropagation"]]}>click me</button>
<button use:listener={[() => '',"click",["capture"]]}>click me</button>
<button use:listener={[() => '',"click",["self"]]}>click me</button>
<Button on:click={() => 'leave untouched'} on:click>click me</Button>
<div>
<button
onclick={() => {
use:listener={[() => {
console.log('hi');
}}>click me</button
},"click"]}>click me</button
>
<button
onclick={(event) => {
event.preventDefault();
use:listener={[() => {
console.log('hi');
}}>click me</button
},"click",["preventDefault"]]}>click me</button
>
<button onclick={(event) => {
event.preventDefault();
count += 1
}}>click me</button>
<button use:listener={[() => (count += 1),"click",["preventDefault"]]}>click me</button>
</div>

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