diff --git a/CHANGELOG.md b/CHANGELOG.md index a24223e5bc..2d8e6a09ac 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 3deba786b4..c843594599 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 b397335c87..6b56010d46 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 ead6d90e06..aa2941f404 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 22d9cd939c..1b3f5cc945 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 e129e66d71..fa955e4fd5 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 7d66145f0a..77b68f1b27 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 db0e7cb007..00e8a5bf00 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 a314b0cff3..3e355e1491 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 a65d9186a7..c032a0636d 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 771b20dec4..b33047d8f3 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 f5f6d07812..ba9e7152d7 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 16b4a3f626..d8f5710023 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 c519fac668..8bf2de7693 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 c12c3523a0..2eca5f29b8 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 2a2254fbd7..1c1e57fc9b 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 d588f0bf73..f72daa22a3 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 12dfd3e90e..17a8065449 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 21c7bfc83b..31be2895ac 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 fefe867e14..1e379032f3 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 7634481a2d..f45e52aebd 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 c154608cd5..9fe9640978 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 77780baa99..6f3bea4010 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 4fe45616c7..ed095353bd 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 52fef36792..e58d32cf29 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 c8cd1d84ce..3cad17b34d 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 e129e66d71..fa955e4fd5 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 70c39eedd2..30723cc142 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 0000000000..702addd3c3 --- /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 0000000000..ca7447f728 --- /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}