fix: use function instead of action

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

@ -53,10 +53,17 @@ export function migrate(source) {
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,
listener_name: analysis.root.unique('listener').name,
needs_run: false,
needs_listener: false
legacy_imports_names: new Map([
['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]
]),
legacy_imports: new Set()
};
if (parsed.module) {
@ -73,7 +80,16 @@ export function migrate(source) {
state = { ...state, scope: analysis.template.scope };
walk(parsed.fragment, state, template);
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';`;
const imports = [...state.legacy_imports]
.map((legacy_import) => {
const maybe_as = state.legacy_imports_names.get(legacy_import);
if (legacy_import === maybe_as) {
return legacy_import;
}
return `${legacy_import} as ${maybe_as}`;
})
.join(', ');
const legacy_import = `import { ${imports} } from 'svelte/legacy';`;
let added_legacy_import = false;
if (state.props.length > 0 || analysis.uses_rest_props || analysis.uses_props) {
@ -147,8 +163,7 @@ export function migrate(source) {
props_declaration = `\n${indent}${props_declaration}`;
str.appendRight(state.props_insertion_point, props_declaration);
} else {
const imports =
state.needs_run || state.needs_listener ? `${indent}${legacy_import}\n` : '';
const imports = state.legacy_imports.size > 0 ? `${indent}${legacy_import}\n` : '';
str.prepend(`<script>\n${imports}${indent}${props_declaration}\n</script>\n\n`);
added_legacy_import = true;
}
@ -196,7 +211,7 @@ export function migrate(source) {
}
}
if ((state.needs_run || state.needs_listener) && !added_legacy_import) {
if (state.legacy_imports.size > 0 && !added_legacy_import) {
if (parsed.instance) {
str.appendRight(
/** @type {number} */ (parsed.instance.content.start),
@ -227,10 +242,8 @@ export function migrate(source) {
* props_name: string;
* rest_props_name: string;
* end: number;
* run_name: string;
* listener_name: string;
* needs_run: boolean;
* needs_listener: boolean;
* legacy_imports_names: Map<string, string>;
* legacy_imports: Set<string>;
* }} State
*/
@ -481,7 +494,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?
@ -489,7 +502,7 @@ const instance_script = {
state.str.update(
/** @type {number} */ (node.start),
start_end + 1,
`${state.run_name}(() => {`
`${state.legacy_imports_names.get('run')}(() => {`
);
const end = /** @type {number} */ (node.body.end);
state.str.update(end - 1, end, '});');
@ -497,7 +510,7 @@ const instance_script = {
state.str.update(
/** @type {number} */ (node.start),
start_end,
`${state.run_name}(() => {\n${state.indent}`
`${state.legacy_imports_names.get('run')}(() => {\n${state.indent}`
);
state.str.indent(state.indent, {
exclude: [
@ -701,25 +714,48 @@ function handle_events(element, state) {
// 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);
if (nodes.length > 1) {
state.legacy_imports.add('handlers');
}
const handlers = [];
for (let i = 0; i < nodes.length; i += 1) {
let prepend = '';
const node = nodes[i];
const payload_name =
(node.expression?.type === 'ArrowFunctionExpression' ||
node.expression?.type === 'FunctionExpression') &&
node.expression.params[0]?.type === 'Identifier'
? node.expression.params[0].name
: generate_event_name(node, state);
const indent = get_indent(state, node, element);
const args = `,"${node.name}"${node.modifiers.length > 0 ? `,${JSON.stringify(node.modifiers)}` : ''}`;
const new_line_index = state.str.original.lastIndexOf('\n', node.start);
const needs_line_delete =
state.str.original.substring(new_line_index, node.start).trim() === '' && i !== 0;
// always move once as the first modifier
const sorted_modifier = [...node.modifiers].sort((a, b) =>
a === 'once' ? 1 : b === 'once' ? -1 : 0
);
if (node.expression) {
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;
for (const modifier of sorted_modifier) {
if (modifier !== 'capture' && modifier !== 'passive' && modifier !== 'nonpassive') {
state.legacy_imports.add(modifier);
body = `${state.legacy_imports_names.get(modifier)}(${body})`;
}
}
handlers.push({
handler: body,
indent,
needs_line_delete
});
state.str.remove(needs_line_delete ? new_line_index : node.start, node.end);
} else {
if (!local) {
local = state.scope.generate(`on${node.name}`);
@ -732,11 +768,40 @@ function handle_events(element, state) {
type: '(event: any) => void'
});
}
prepend += `use:${state.listener_name}={[(${payload_name})=>{${local}?.(${payload_name});}${args}]}`;
state.needs_listener = true;
let body = `(${payload_name})=>{`;
let added_modifiers = 0;
for (const modifier of sorted_modifier) {
if (modifier !== 'capture' && modifier !== 'passive' && modifier !== 'nonpassive') {
added_modifiers++;
body = `\n${indent}${state.legacy_imports_names.get(modifier)}(${body}`;
state.legacy_imports.add(modifier);
}
}
body += `${local}?.(${payload_name});}${')'.repeat(added_modifiers)}`;
handlers.push({
handler: body,
indent
});
state.str.remove(needs_line_delete ? new_line_index : node.start, node.end);
}
}
const first_node = nodes[0];
state.str.overwrite(node.start, node.end, prepend);
if (first_node) {
let handlers_body = '';
for (let handler of handlers) {
handlers_body += `${handler.needs_line_delete || nodes.length > 1 ? `\n${handler.indent}` : ''}${handler.handler},`;
}
handlers_body = handlers_body.substring(0, handlers_body.length - 1);
state.str.overwrite(
first_node.start,
first_node.end,
`${name}={${nodes.length > 1 ? `${state.legacy_imports_names.get('handlers')}(` : ''}${handlers_body}${nodes.length > 1 ? ')' : ''}}`
);
}
}
}

@ -172,32 +172,110 @@ export function run(fn) {
}
/**
* @param {EventTarget} node
* @param {[EventListener, string, string[]]} options
* Function to mimic the multiple listeners available in svelte 4
* @param {EventListener[]} handlers
* @returns {EventListener}
*/
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]();
}
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);
}
handler?.(e);
},
{
signal: aborter.signal,
passive: modifiers.includes('passive'),
capture: modifiers.includes('capture')
if (stopped) {
break;
}
}
for (let error of errors) {
queueMicrotask(() => {
throw error;
});
}
};
}
/**
* Migration helper to substitute the `stopImmediatePropagation` event modifier
* @param {EventListener} fn
*/
export function stopImmediatePropagation(fn) {
return (/**@type {Event}*/ e) => {
e.stopImmediatePropagation();
fn(e);
};
}
/**
* Migration helper to substitute the `preventDefault` event modifier
* @param {EventListener} fn
*/
export function preventDefault(fn) {
return (/**@type {Event}*/ e) => {
e.preventDefault();
fn(e);
};
}
/**
* Migration helper to substitute the `stopPropagation` event modifier
* @param {EventListener} fn
*/
export function stopPropagation(fn) {
return (/**@type {Event}*/ e) => {
e.stopPropagation();
fn(e);
};
}
/**
* Migration helper to substitute the `trusted` event modifier
* @param {EventListener} fn
*/
export function trusted(fn) {
return (/**@type {Event}*/ e) => {
if (e.isTrusted) {
fn(e);
}
);
return {
destroy() {
aborter.abort();
};
}
/**
* Migration helper to substitute the `self` event modifier
* @param {EventListener} fn
*/
export function self(fn) {
return (/**@type {Event}*/ e) => {
if (e.target === e.currentTarget) {
fn(e);
}
};
}
/**
* Migration helper to substitute the `once` event modifier
* @param {EventListener} fn
*/
export function once(fn) {
let executed = false;
return (/**@type {Event}*/ e) => {
if (!executed) {
executed = true;
fn(e);
}
};
}

@ -0,0 +1,65 @@
<script>
let handlers;
let stopPropagation;
let stopImmediatePropagation;
let once;
let trusted;
let self;
</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
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,88 @@
<script>
/** @type {{onclick?: (event: any) => void, ontoggle?: (event: any) => void, 'oncustom-event-bubble'?: (event: any) => void, onblur?: (event: any) => void}} */
let {
onclick,
ontoggle,
'oncustom-event-bubble': oncustom_event_bubble,
onblur
} = $props();
import { handlers as handlers_1, preventDefault, stopPropagation as stopPropagation_1, stopImmediatePropagation as stopImmediatePropagation_1, self as self_1, trusted as trusted_1, once as once_1 } from 'svelte/legacy';
let handlers;
let stopPropagation;
let stopImmediatePropagation;
let once;
let trusted;
let self;
</script>
<button onclick={handlers_1(
() => console.log('hi'),
(event)=>{onclick?.(event);})} >click me</button>
<button onclick={handlers_1(
function(){ console.log('hi') },
(event)=>{onclick?.(event);})} >click me</button>
<button onclick={handlers_1(
() => console.log('before'),
(event)=>{onclick?.(event);},
() => console.log('after'))}
>click me</button
>
<button onclick={handlers_1(
(event)=>{onclick?.(event);},
foo)} >click me</button>
<button onclick={(event)=>{onclick?.(event);}}>click me</button>
<button ondblclick={() => console.log('hi')}>click me</button>
<button ontoggle={(event)=>{ontoggle?.(event);}}>click me</button>
<button oncustom-event={() => 'hi'}>click me</button>
<button oncustom-event-bubble={(event)=>{oncustom_event_bubble?.(event);}}>click me</button>
<button onclick={preventDefault(() => (searching = true))}>click me</button>
<button onclick={preventDefault(() => '')}>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(() => ''))}>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(() => ''))}>click me</button>
<button
onclick={handlers_1(
(event)=>{onclick?.(event);},
foo,
()=>'',
once_1(preventDefault(trusted_1(()=>''))))}
onblur={handlers_1(
foo,
once_1(
preventDefault(
trusted_1((event)=>{onblur?.(event);}))))}
>
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(() => {
console.log('hi');
})}>click me</button
>
<button onclick={preventDefault(() => (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,33 @@
<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|once={() => ''}>click me</button>
<button on:click|once|preventDefault={() => ''}>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,40 +1,81 @@
<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();
import { handlers, preventDefault, stopPropagation, stopImmediatePropagation, self, trusted, once } from 'svelte/legacy';
/** @type {{onclick?: (event: any) => void, ontoggle?: (event: any) => void, 'oncustom-event-bubble'?: (event: any) => void, onblur?: (event: any) => void}} */
let {
onclick,
ontoggle,
'oncustom-event-bubble': oncustom_event_bubble,
onblur
} = $props();
</script>
<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"]}
<button onclick={handlers(
() => console.log('hi'),
(event)=>{onclick?.(event);})} >click me</button>
<button onclick={handlers(
function(){ console.log('hi') },
(event)=>{onclick?.(event);})} >click me</button>
<button onclick={handlers(
() => console.log('before'),
(event)=>{onclick?.(event);},
() => console.log('after'))}
>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 onclick={handlers(
(event)=>{onclick?.(event);},
foo)} >click me</button>
<button onclick={(event)=>{onclick?.(event);}}>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 ondblclick={() => console.log('hi')}>click me</button>
<button ontoggle={(event)=>{ontoggle?.(event);}}>click me</button>
<button oncustom-event={() => 'hi'}>click me</button>
<button oncustom-event-bubble={(event)=>{oncustom_event_bubble?.(event);}}>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={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={once(trusted(() => ''))}>click me</button>
<button onclick={once(preventDefault(() => ''))}>click me</button>
<button
onclick={handlers(
(event)=>{onclick?.(event);},
foo,
()=>'',
once(preventDefault(trusted(()=>''))))}
onblur={handlers(
foo,
once(
preventDefault(
trusted((event)=>{onblur?.(event);}))))}
>
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
use:listener={[() => {
onclick={() => {
console.log('hi');
},"click"]}>click me</button
}}>click me</button
>
<button
use:listener={[() => {
onclick={preventDefault(() => {
console.log('hi');
},"click",["preventDefault"]]}>click me</button
})}>click me</button
>
<button use:listener={[() => (count += 1),"click",["preventDefault"]]}>click me</button>
<button onclick={preventDefault(() => (count += 1))}>click me</button>
</div>

@ -1,10 +1,8 @@
<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 use:listener={[() => count++,"click"]}>{count}</button>
<button onclick={() => count++}>{count}</button>
Loading…
Cancel
Save