diff --git a/CHANGELOG.md b/CHANGELOG.md
index a24223e5bcf7..2d8e6a09ac30 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@
* Allow transitions and animations to work within iframes ([#3624](https://github.com/sveltejs/svelte/issues/3624))
* Fix initialising slot fallbacks when unnecessary ([#3763](https://github.com/sveltejs/svelte/issues/3763))
* Disallow binding directly to `const` variables ([#4479](https://github.com/sveltejs/svelte/issues/4479))
+* Fix re-attaching event handlers on keyed `{#each}` blocks ([#4491](https://github.com/sveltejs/svelte/issues/4491))
* Fix updating keyed `{#each}` blocks with `{:else}` ([#4536](https://github.com/sveltejs/svelte/issues/4536), [#4549](https://github.com/sveltejs/svelte/issues/4549))
* Fix hydration of top-level content ([#4542](https://github.com/sveltejs/svelte/issues/4542))
diff --git a/src/compiler/compile/render_dom/Block.ts b/src/compiler/compile/render_dom/Block.ts
index 3deba786b4f7..c84359459912 100644
--- a/src/compiler/compile/render_dom/Block.ts
+++ b/src/compiler/compile/render_dom/Block.ts
@@ -294,10 +294,14 @@ export default class Block {
if (this.chunks.mount.length === 0) {
properties.mount = noop;
- } else {
+ } else if (this.event_listeners.length === 0) {
properties.mount = x`function #mount(#target, anchor) {
${this.chunks.mount}
}`;
+ } else {
+ properties.mount = x`function #mount(#target, anchor, #remount) {
+ ${this.chunks.mount}
+ }`;
}
if (this.has_update_method || this.maintain_context) {
@@ -457,7 +461,10 @@ export default class Block {
if (this.event_listeners.length === 1) {
this.chunks.mount.push(
- b`${dispose} = ${this.event_listeners[0]};`
+ b`
+ if (#remount) ${dispose}();
+ ${dispose} = ${this.event_listeners[0]};
+ `
);
this.chunks.destroy.push(
@@ -465,6 +472,7 @@ export default class Block {
);
} else {
this.chunks.mount.push(b`
+ if (#remount) @run_all(${dispose});
${dispose} = [
${this.event_listeners}
];
diff --git a/src/runtime/internal/keyed_each.ts b/src/runtime/internal/keyed_each.ts
index b397335c8766..6b56010d46cc 100644
--- a/src/runtime/internal/keyed_each.ts
+++ b/src/runtime/internal/keyed_each.ts
@@ -56,7 +56,7 @@ export function update_keyed_each(old_blocks, dirty, get_key, dynamic, ctx, list
function insert(block) {
transition_in(block, 1);
- block.m(node, next);
+ block.m(node, next, lookup.has(block.key));
lookup.set(block.key, block);
next = block.first;
n--;
diff --git a/test/js/samples/action-custom-event-handler/expected.js b/test/js/samples/action-custom-event-handler/expected.js
index ead6d90e0639..aa2941f404c8 100644
--- a/test/js/samples/action-custom-event-handler/expected.js
+++ b/test/js/samples/action-custom-event-handler/expected.js
@@ -21,8 +21,9 @@ function create_fragment(ctx) {
button = element("button");
button.textContent = "foo";
},
- m(target, anchor) {
+ m(target, anchor, remount) {
insert(target, button, anchor);
+ if (remount) dispose();
dispose = action_destroyer(foo_action = foo.call(null, button, /*foo_function*/ ctx[1]));
},
p(ctx, [dirty]) {
diff --git a/test/js/samples/action/expected.js b/test/js/samples/action/expected.js
index 22d9cd939c2c..1b3f5cc94583 100644
--- a/test/js/samples/action/expected.js
+++ b/test/js/samples/action/expected.js
@@ -22,8 +22,9 @@ function create_fragment(ctx) {
a.textContent = "Test";
attr(a, "href", "#");
},
- m(target, anchor) {
+ m(target, anchor, remount) {
insert(target, a, anchor);
+ if (remount) dispose();
dispose = action_destroyer(link_action = link.call(null, a));
},
p: noop,
diff --git a/test/js/samples/bind-online/expected.js b/test/js/samples/bind-online/expected.js
index e129e66d7163..fa955e4fd59b 100644
--- a/test/js/samples/bind-online/expected.js
+++ b/test/js/samples/bind-online/expected.js
@@ -15,7 +15,9 @@ function create_fragment(ctx) {
return {
c: noop,
- m(target, anchor) {
+ m(target, anchor, remount) {
+ if (remount) run_all(dispose);
+
dispose = [
listen(window, "online", /*onlinestatuschanged*/ ctx[1]),
listen(window, "offline", /*onlinestatuschanged*/ ctx[1])
diff --git a/test/js/samples/bind-open/expected.js b/test/js/samples/bind-open/expected.js
index 7d66145f0ae7..77b68f1b27fc 100644
--- a/test/js/samples/bind-open/expected.js
+++ b/test/js/samples/bind-open/expected.js
@@ -21,9 +21,10 @@ function create_fragment(ctx) {
details.innerHTML = `summarycontent
`;
},
- m(target, anchor) {
+ m(target, anchor, remount) {
insert(target, details, anchor);
details.open = /*open*/ ctx[0];
+ if (remount) dispose();
dispose = listen(details, "toggle", /*details_toggle_handler*/ ctx[1]);
},
p(ctx, [dirty]) {
diff --git a/test/js/samples/bindings-readonly-order/expected.js b/test/js/samples/bindings-readonly-order/expected.js
index db0e7cb007ae..00e8a5bf00cc 100644
--- a/test/js/samples/bindings-readonly-order/expected.js
+++ b/test/js/samples/bindings-readonly-order/expected.js
@@ -27,10 +27,11 @@ function create_fragment(ctx) {
attr(input0, "type", "file");
attr(input1, "type", "file");
},
- m(target, anchor) {
+ m(target, anchor, remount) {
insert(target, input0, anchor);
insert(target, t, anchor);
insert(target, input1, anchor);
+ if (remount) run_all(dispose);
dispose = [
listen(input0, "change", /*input0_change_handler*/ ctx[1]),
diff --git a/test/js/samples/capture-inject-dev-only/expected.js b/test/js/samples/capture-inject-dev-only/expected.js
index a314b0cff33b..3e355e149186 100644
--- a/test/js/samples/capture-inject-dev-only/expected.js
+++ b/test/js/samples/capture-inject-dev-only/expected.js
@@ -29,12 +29,13 @@ function create_fragment(ctx) {
t1 = space();
input = element("input");
},
- m(target, anchor) {
+ m(target, anchor, remount) {
insert(target, p, anchor);
append(p, t0);
insert(target, t1, anchor);
insert(target, input, anchor);
set_input_value(input, /*foo*/ ctx[0]);
+ if (remount) dispose();
dispose = listen(input, "input", /*input_input_handler*/ ctx[1]);
},
p(ctx, [dirty]) {
diff --git a/test/js/samples/component-static-var/expected.js b/test/js/samples/component-static-var/expected.js
index a65d9186a78e..c032a0636d42 100644
--- a/test/js/samples/component-static-var/expected.js
+++ b/test/js/samples/component-static-var/expected.js
@@ -36,7 +36,7 @@ function create_fragment(ctx) {
t1 = space();
input = element("input");
},
- m(target, anchor) {
+ m(target, anchor, remount) {
mount_component(foo, target, anchor);
insert(target, t0, anchor);
mount_component(bar, target, anchor);
@@ -44,6 +44,7 @@ function create_fragment(ctx) {
insert(target, input, anchor);
set_input_value(input, /*z*/ ctx[0]);
current = true;
+ if (remount) dispose();
dispose = listen(input, "input", /*input_input_handler*/ ctx[1]);
},
p(ctx, [dirty]) {
diff --git a/test/js/samples/component-store-reassign-invalidate/expected.js b/test/js/samples/component-store-reassign-invalidate/expected.js
index 771b20dec4ea..b33047d8f377 100644
--- a/test/js/samples/component-store-reassign-invalidate/expected.js
+++ b/test/js/samples/component-store-reassign-invalidate/expected.js
@@ -32,11 +32,12 @@ function create_fragment(ctx) {
button = element("button");
button.textContent = "reset";
},
- m(target, anchor) {
+ m(target, anchor, remount) {
insert(target, h1, anchor);
append(h1, t0);
insert(target, t1, anchor);
insert(target, button, anchor);
+ if (remount) dispose();
dispose = listen(button, "click", /*click_handler*/ ctx[2]);
},
p(ctx, [dirty]) {
diff --git a/test/js/samples/dont-invalidate-this/expected.js b/test/js/samples/dont-invalidate-this/expected.js
index f5f6d078125d..ba9e7152d75c 100644
--- a/test/js/samples/dont-invalidate-this/expected.js
+++ b/test/js/samples/dont-invalidate-this/expected.js
@@ -18,8 +18,9 @@ function create_fragment(ctx) {
c() {
input = element("input");
},
- m(target, anchor) {
+ m(target, anchor, remount) {
insert(target, input, anchor);
+ if (remount) dispose();
dispose = listen(input, "input", make_uppercase);
},
p: noop,
diff --git a/test/js/samples/event-handler-dynamic/expected.js b/test/js/samples/event-handler-dynamic/expected.js
index 16b4a3f6262e..d8f5710023a6 100644
--- a/test/js/samples/event-handler-dynamic/expected.js
+++ b/test/js/samples/event-handler-dynamic/expected.js
@@ -43,7 +43,7 @@ function create_fragment(ctx) {
button2 = element("button");
button2.textContent = "click";
},
- m(target, anchor) {
+ m(target, anchor, remount) {
insert(target, p0, anchor);
append(p0, button0);
append(p0, t1);
@@ -53,6 +53,7 @@ function create_fragment(ctx) {
append(p1, t4);
insert(target, t5, anchor);
insert(target, button2, anchor);
+ if (remount) run_all(dispose);
dispose = [
listen(button0, "click", /*updateHandler1*/ ctx[2]),
diff --git a/test/js/samples/event-handler-no-passive/expected.js b/test/js/samples/event-handler-no-passive/expected.js
index c519fac668f5..8bf2de769371 100644
--- a/test/js/samples/event-handler-no-passive/expected.js
+++ b/test/js/samples/event-handler-no-passive/expected.js
@@ -21,8 +21,9 @@ function create_fragment(ctx) {
a.textContent = "this should not navigate to example.com";
attr(a, "href", "https://example.com");
},
- m(target, anchor) {
+ m(target, anchor, remount) {
insert(target, a, anchor);
+ if (remount) dispose();
dispose = listen(a, "touchstart", touchstart_handler);
},
p: noop,
diff --git a/test/js/samples/event-modifiers/expected.js b/test/js/samples/event-modifiers/expected.js
index c12c3523a0a1..2eca5f29b89c 100644
--- a/test/js/samples/event-modifiers/expected.js
+++ b/test/js/samples/event-modifiers/expected.js
@@ -36,13 +36,14 @@ function create_fragment(ctx) {
button2 = element("button");
button2.textContent = "or me!";
},
- m(target, anchor) {
+ m(target, anchor, remount) {
insert(target, div, anchor);
append(div, button0);
append(div, t1);
append(div, button1);
append(div, t3);
append(div, button2);
+ if (remount) run_all(dispose);
dispose = [
listen(button0, "click", stop_propagation(prevent_default(handleClick))),
diff --git a/test/js/samples/input-files/expected.js b/test/js/samples/input-files/expected.js
index 2a2254fbd754..1c1e57fc9b24 100644
--- a/test/js/samples/input-files/expected.js
+++ b/test/js/samples/input-files/expected.js
@@ -21,8 +21,9 @@ function create_fragment(ctx) {
attr(input, "type", "file");
input.multiple = true;
},
- m(target, anchor) {
+ m(target, anchor, remount) {
insert(target, input, anchor);
+ if (remount) dispose();
dispose = listen(input, "change", /*input_change_handler*/ ctx[1]);
},
p: noop,
diff --git a/test/js/samples/input-no-initial-value/expected.js b/test/js/samples/input-no-initial-value/expected.js
index d588f0bf735a..f72daa22a302 100644
--- a/test/js/samples/input-no-initial-value/expected.js
+++ b/test/js/samples/input-no-initial-value/expected.js
@@ -32,12 +32,13 @@ function create_fragment(ctx) {
attr(input, "type", "text");
input.required = true;
},
- m(target, anchor) {
+ m(target, anchor, remount) {
insert(target, form, anchor);
append(form, input);
set_input_value(input, /*test*/ ctx[0]);
append(form, t0);
append(form, button);
+ if (remount) run_all(dispose);
dispose = [
listen(input, "input", /*input_input_handler*/ ctx[2]),
diff --git a/test/js/samples/input-range/expected.js b/test/js/samples/input-range/expected.js
index 12dfd3e90efe..17a806544907 100644
--- a/test/js/samples/input-range/expected.js
+++ b/test/js/samples/input-range/expected.js
@@ -23,9 +23,10 @@ function create_fragment(ctx) {
input = element("input");
attr(input, "type", "range");
},
- m(target, anchor) {
+ m(target, anchor, remount) {
insert(target, input, anchor);
set_input_value(input, /*value*/ ctx[0]);
+ if (remount) run_all(dispose);
dispose = [
listen(input, "change", /*input_change_input_handler*/ ctx[1]),
diff --git a/test/js/samples/input-value/expected.js b/test/js/samples/input-value/expected.js
index 21c7bfc83b9e..31be2895ac0d 100644
--- a/test/js/samples/input-value/expected.js
+++ b/test/js/samples/input-value/expected.js
@@ -31,12 +31,13 @@ function create_fragment(ctx) {
t2 = text("!");
input.value = /*name*/ ctx[0];
},
- m(target, anchor) {
+ m(target, anchor, remount) {
insert(target, input, anchor);
insert(target, t0, anchor);
insert(target, h1, anchor);
append(h1, t1);
append(h1, t2);
+ if (remount) dispose();
dispose = listen(input, "input", /*onInput*/ ctx[1]);
},
p(ctx, [dirty]) {
diff --git a/test/js/samples/input-without-blowback-guard/expected.js b/test/js/samples/input-without-blowback-guard/expected.js
index fefe867e146b..1e379032f3a1 100644
--- a/test/js/samples/input-without-blowback-guard/expected.js
+++ b/test/js/samples/input-without-blowback-guard/expected.js
@@ -20,9 +20,10 @@ function create_fragment(ctx) {
input = element("input");
attr(input, "type", "checkbox");
},
- m(target, anchor) {
+ m(target, anchor, remount) {
insert(target, input, anchor);
input.checked = /*foo*/ ctx[0];
+ if (remount) dispose();
dispose = listen(input, "change", /*input_change_handler*/ ctx[1]);
},
p(ctx, [dirty]) {
diff --git a/test/js/samples/instrumentation-script-if-no-block/expected.js b/test/js/samples/instrumentation-script-if-no-block/expected.js
index 7634481a2d6a..f45e52aebd3b 100644
--- a/test/js/samples/instrumentation-script-if-no-block/expected.js
+++ b/test/js/samples/instrumentation-script-if-no-block/expected.js
@@ -31,12 +31,13 @@ function create_fragment(ctx) {
t2 = text("x: ");
t3 = text(/*x*/ ctx[0]);
},
- m(target, anchor) {
+ m(target, anchor, remount) {
insert(target, button, anchor);
insert(target, t1, anchor);
insert(target, p, anchor);
append(p, t2);
append(p, t3);
+ if (remount) dispose();
dispose = listen(button, "click", /*foo*/ ctx[1]);
},
p(ctx, [dirty]) {
diff --git a/test/js/samples/instrumentation-script-x-equals-x/expected.js b/test/js/samples/instrumentation-script-x-equals-x/expected.js
index c154608cd52c..9fe964097829 100644
--- a/test/js/samples/instrumentation-script-x-equals-x/expected.js
+++ b/test/js/samples/instrumentation-script-x-equals-x/expected.js
@@ -32,12 +32,13 @@ function create_fragment(ctx) {
t2 = text("number of things: ");
t3 = text(t3_value);
},
- m(target, anchor) {
+ m(target, anchor, remount) {
insert(target, button, anchor);
insert(target, t1, anchor);
insert(target, p, anchor);
append(p, t2);
append(p, t3);
+ if (remount) dispose();
dispose = listen(button, "click", /*foo*/ ctx[1]);
},
p(ctx, [dirty]) {
diff --git a/test/js/samples/instrumentation-template-if-no-block/expected.js b/test/js/samples/instrumentation-template-if-no-block/expected.js
index 77780baa99d1..6f3bea4010fc 100644
--- a/test/js/samples/instrumentation-template-if-no-block/expected.js
+++ b/test/js/samples/instrumentation-template-if-no-block/expected.js
@@ -31,12 +31,13 @@ function create_fragment(ctx) {
t2 = text("x: ");
t3 = text(/*x*/ ctx[0]);
},
- m(target, anchor) {
+ m(target, anchor, remount) {
insert(target, button, anchor);
insert(target, t1, anchor);
insert(target, p, anchor);
append(p, t2);
append(p, t3);
+ if (remount) dispose();
dispose = listen(button, "click", /*click_handler*/ ctx[1]);
},
p(ctx, [dirty]) {
diff --git a/test/js/samples/instrumentation-template-x-equals-x/expected.js b/test/js/samples/instrumentation-template-x-equals-x/expected.js
index 4fe45616c75b..ed095353bd53 100644
--- a/test/js/samples/instrumentation-template-x-equals-x/expected.js
+++ b/test/js/samples/instrumentation-template-x-equals-x/expected.js
@@ -32,12 +32,13 @@ function create_fragment(ctx) {
t2 = text("number of things: ");
t3 = text(t3_value);
},
- m(target, anchor) {
+ m(target, anchor, remount) {
insert(target, button, anchor);
insert(target, t1, anchor);
insert(target, p, anchor);
append(p, t2);
append(p, t3);
+ if (remount) dispose();
dispose = listen(button, "click", /*click_handler*/ ctx[1]);
},
p(ctx, [dirty]) {
diff --git a/test/js/samples/media-bindings/expected.js b/test/js/samples/media-bindings/expected.js
index 52fef3679246..e58d32cf294d 100644
--- a/test/js/samples/media-bindings/expected.js
+++ b/test/js/samples/media-bindings/expected.js
@@ -42,7 +42,7 @@ function create_fragment(ctx) {
if (/*seeking*/ ctx[8] === void 0) add_render_callback(() => /*audio_seeking_seeked_handler*/ ctx[17].call(audio));
if (/*ended*/ ctx[9] === void 0) add_render_callback(() => /*audio_ended_handler*/ ctx[18].call(audio));
},
- m(target, anchor) {
+ m(target, anchor, remount) {
insert(target, audio, anchor);
if (!isNaN(/*volume*/ ctx[6])) {
@@ -53,6 +53,8 @@ function create_fragment(ctx) {
audio.playbackRate = /*playbackRate*/ ctx[7];
}
+ if (remount) run_all(dispose);
+
dispose = [
listen(audio, "progress", /*audio_progress_handler*/ ctx[10]),
listen(audio, "loadedmetadata", /*audio_loadedmetadata_handler*/ ctx[11]),
diff --git a/test/js/samples/video-bindings/expected.js b/test/js/samples/video-bindings/expected.js
index c8cd1d84ce0d..3cad17b34d3e 100644
--- a/test/js/samples/video-bindings/expected.js
+++ b/test/js/samples/video-bindings/expected.js
@@ -38,9 +38,10 @@ function create_fragment(ctx) {
if (/*videoHeight*/ ctx[1] === void 0 || /*videoWidth*/ ctx[2] === void 0) add_render_callback(() => /*video_resize_handler*/ ctx[5].call(video));
add_render_callback(() => /*video_elementresize_handler*/ ctx[6].call(video));
},
- m(target, anchor) {
+ m(target, anchor, remount) {
insert(target, video, anchor);
video_resize_listener = add_resize_listener(video, /*video_elementresize_handler*/ ctx[6].bind(video));
+ if (remount) run_all(dispose);
dispose = [
listen(video, "timeupdate", video_timeupdate_handler),
diff --git a/test/js/samples/window-binding-online/expected.js b/test/js/samples/window-binding-online/expected.js
index e129e66d7163..fa955e4fd59b 100644
--- a/test/js/samples/window-binding-online/expected.js
+++ b/test/js/samples/window-binding-online/expected.js
@@ -15,7 +15,9 @@ function create_fragment(ctx) {
return {
c: noop,
- m(target, anchor) {
+ m(target, anchor, remount) {
+ if (remount) run_all(dispose);
+
dispose = [
listen(window, "online", /*onlinestatuschanged*/ ctx[1]),
listen(window, "offline", /*onlinestatuschanged*/ ctx[1])
diff --git a/test/js/samples/window-binding-scroll/expected.js b/test/js/samples/window-binding-scroll/expected.js
index 70c39eedd20c..30723cc142fc 100644
--- a/test/js/samples/window-binding-scroll/expected.js
+++ b/test/js/samples/window-binding-scroll/expected.js
@@ -34,10 +34,11 @@ function create_fragment(ctx) {
t0 = text("scrolled to ");
t1 = text(/*y*/ ctx[0]);
},
- m(target, anchor) {
+ m(target, anchor, remount) {
insert(target, p, anchor);
append(p, t0);
append(p, t1);
+ if (remount) dispose();
dispose = listen(window, "scroll", () => {
scrolling = true;
diff --git a/test/runtime/samples/event-handler-each-modifier/_config.js b/test/runtime/samples/event-handler-each-modifier/_config.js
new file mode 100644
index 000000000000..702addd3c317
--- /dev/null
+++ b/test/runtime/samples/event-handler-each-modifier/_config.js
@@ -0,0 +1,42 @@
+export default {
+ async test({ assert, component, target, window }) {
+ // set first
+ await component.lists.update(() => [
+ { text: "item1" },
+ { text: "item2" },
+ { text: "item3" }
+ ]);
+
+ await component.lists.update(() => [
+ { text: "item3" },
+ { text: "item2" },
+ { text: "item1" }
+ ]);
+
+ await component.lists.update(() => [
+ { text: "item1" },
+ { text: "item2" },
+ { text: "item3" }
+ ]);
+
+ assert.equal(component.updated, 4);
+
+ const [item1, item2] = target.childNodes;
+ const [item1Btn1, item1Btn2] = item1.querySelectorAll('button');
+ const [item2Btn1, item2Btn2] = item2.querySelectorAll('button');
+
+ const clickEvent = new window.MouseEvent('click');
+
+ await item1Btn1.dispatchEvent(clickEvent);
+ assert.equal(component.getNormalCount(), 1);
+
+ await item1Btn2.dispatchEvent(clickEvent);
+ assert.equal(component.getModifierCount(), 1);
+
+ await item2Btn1.dispatchEvent(clickEvent);
+ assert.equal(component.getNormalCount(), 2);
+
+ await item2Btn2.dispatchEvent(clickEvent);
+ assert.equal(component.getModifierCount(), 2);
+ }
+};
diff --git a/test/runtime/samples/event-handler-each-modifier/main.svelte b/test/runtime/samples/event-handler-each-modifier/main.svelte
new file mode 100644
index 000000000000..ca7447f728f3
--- /dev/null
+++ b/test/runtime/samples/event-handler-each-modifier/main.svelte
@@ -0,0 +1,37 @@
+
+
+{#each $lists as item (item.text)}
+
+ {item.text}
+
+
+
+{/each}