diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js index 9a41da8ccd..7b69b612ee 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js @@ -28,6 +28,7 @@ import { AttributeAliases, DOMBooleanAttributes, EACH_INDEX_REACTIVE, + EACH_IS_ANIMATED, EACH_IS_CONTROLLED, EACH_IS_STRICT_EQUALS, EACH_ITEM_REACTIVE, @@ -2337,6 +2338,16 @@ export const template_visitors = { each_type |= EACH_ITEM_REACTIVE; } + if ( + node.key && + node.body.nodes.some((child) => { + if (child.type !== 'RegularElement' && child.type !== 'SvelteElement') return false; + return child.attributes.some((attr) => attr.type === 'AnimateDirective'); + }) + ) { + each_type |= EACH_IS_ANIMATED; + } + if (each_node_meta.is_controlled) { each_type |= EACH_IS_CONTROLLED; } diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index 97a430278e..2d97913c22 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -15,12 +15,13 @@ import { } from '../hydration.js'; import { empty } from '../operations.js'; import { insert, remove } from '../reconciler.js'; -import { current_block } from '../../runtime.js'; +import { current_block, untrack } from '../../runtime.js'; import { destroy_effect, pause_effect, render_effect, - resume_effect + resume_effect, + user_effect } from '../../reactivity/effects.js'; import { source, mutable_source, set } from '../../reactivity/sources.js'; import { is_array, is_frozen, map_get, map_set } from '../../utils.js'; @@ -29,6 +30,14 @@ import { STATE_SYMBOL } from '../../constants.js'; var NEW_BLOCK = -1; var LIS_BLOCK = -2; +/** @type {import('#client').EachItemBlock | null} */ +export let current_each_item_block = null; + +/** @param {import('#client').EachItemBlock | null} block */ +export function set_current_each_item_block(block) { + current_each_item_block = block; +} + /** * @template V * @param {Element | Comment} anchor The next sibling node, or the parent node if this is a 'controlled' block @@ -316,13 +325,23 @@ function reconcile_tracked_array(array, each_block, anchor, render_fn, flags, ke var b_blocks = Array(b); var is_animated = (flags & EACH_IS_ANIMATED) !== 0; - var should_update = is_animated || (flags & (EACH_ITEM_REACTIVE | EACH_INDEX_REACTIVE)) !== 0; + var should_update = (flags & (EACH_ITEM_REACTIVE | EACH_INDEX_REACTIVE)) !== 0; var start = 0; var block; /** @type {Array} */ var to_destroy = []; + /** @type {Array} */ + var to_animate = []; + + if (is_animated) { + for (block of a_blocks) { + // TODO can we avoid measuring blocks that will be destroyed? + block.a?.measure(); + } + } + // Step 1 — trim common suffix while (a > 0 && b > 0 && a_blocks[a - 1].k === keys[b - 1]) { block = b_blocks[--b] = a_blocks[--a]; @@ -392,8 +411,7 @@ function reconcile_tracked_array(array, each_block, anchor, render_fn, flags, ke b_blocks[index] = block; if (is_animated) { - // If keys are animated, we need to do updates before actual moves - update_block(block, array[index], index, flags); + to_animate.push(block); } } } @@ -424,6 +442,16 @@ function reconcile_tracked_array(array, each_block, anchor, render_fn, flags, ke } } + if (to_animate.length > 0) { + user_effect(() => { + untrack(() => { + for (block of to_animate) { + block.a?.apply(); + } + }); + }); + } + var remaining = to_destroy.length; if (remaining > 0) { var clear = () => { @@ -602,6 +630,7 @@ function create_block(item, key, index, render_fn, flags) { /** @type {import('#client').EachItemBlock} */ var block = { + a: null, // dom d: null, // effect @@ -615,13 +644,21 @@ function create_block(item, key, index, render_fn, flags) { v: item_value }; - block.e = render_effect( - () => { - render_fn(null, block.v, block.i); - }, - block, - true - ); + var previous_each_item_block = current_each_item_block; + + try { + current_each_item_block = block; - return block; + block.e = render_effect( + () => { + render_fn(null, block.v, block.i); + }, + block, + true + ); + + return block; + } finally { + current_each_item_block = previous_each_item_block; + } } diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-element.js b/packages/svelte/src/internal/client/dom/blocks/svelte-element.js index 8818274556..cc7561ac1e 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-element.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-element.js @@ -11,6 +11,7 @@ import { remove } from '../reconciler.js'; import { current_block } from '../../runtime.js'; import { is_array } from '../../utils.js'; import { set_run_transitions } from '../../render.js'; +import { current_each_item_block, set_current_each_item_block } from './each.js'; /** * @param {import('#client').Block} block @@ -64,10 +65,15 @@ export function element(anchor_node, tag_fn, is_svg, render_fn) { /** @type {import('#client').Effect | null} */ let effect; + let each_item_block = current_each_item_block; + const wrapper = render_effect(() => { const next_tag = tag_fn() || null; if (next_tag === tag) return; + var previous_each_item_block = current_each_item_block; + set_current_each_item_block(each_item_block); + // We try our best infering the namespace in case it's not possible to determine statically, // but on the first render on the client (without hydration) the parent will be undefined, // since the anchor is not attached to its parent / the dom yet. @@ -134,6 +140,8 @@ export function element(anchor_node, tag_fn, is_svg, render_fn) { tag = next_tag; if (tag) current_tag = tag; set_run_transitions(true); + + set_current_each_item_block(previous_each_item_block); }, block); wrapper.ondestroy = () => { diff --git a/packages/svelte/src/internal/client/dom/elements/transitions.js b/packages/svelte/src/internal/client/dom/elements/transitions.js index 6fd2624380..d940aaa6cc 100644 --- a/packages/svelte/src/internal/client/dom/elements/transitions.js +++ b/packages/svelte/src/internal/client/dom/elements/transitions.js @@ -1,11 +1,12 @@ import { noop } from '../../../common.js'; import { user_effect } from '../../reactivity/effects.js'; -import { current_effect, untrack } from '../../runtime.js'; +import { current_block, current_effect, untrack } from '../../runtime.js'; import { raf } from '../../timing.js'; import { loop } from '../../loop.js'; import { run_transitions } from '../../render.js'; import { TRANSITION_GLOBAL, TRANSITION_IN, TRANSITION_OUT } from '../../constants.js'; import { is_function } from '../../utils.js'; +import { current_each_item_block } from '../blocks/each.js'; /** * @template T @@ -66,8 +67,42 @@ function css_to_keyframe(css) { /** @param {number} t */ const linear = (t) => t; -export function animation() { - // TODO +/** + * @template P + * @param {Element} element + * @param {() => import('#client').AnimateFn

} get_fn + * @param {(() => P) | null} get_params + */ +export function animation(element, get_fn, get_params) { + /** @type {DOMRect} */ + let from; + + /** @type {DOMRect} */ + let to; + + /** @type {import('#client').Animation | undefined} */ + let animation; + + /** @type {import('#client').AnimationManager} */ + const manager = { + measure() { + from = element.getBoundingClientRect(); + }, + apply() { + to = element.getBoundingClientRect(); + + const options = get_fn()(element, { from, to }, get_params?.(), {}); // TODO what is the last argument? + + animation?.abort(); + + // TODO bail if `from` and `to` match + animation = animate(element, options, undefined, 1, () => { + animation = undefined; + }); + } + }; + + /** @type {import('#client').EachItemBlock} */ (current_each_item_block).a = manager; } /** @@ -195,7 +230,7 @@ function animate(element, options, counterpart, t2, callback) { }; } - dispatch_event(element, t2 === 1 ? 'introstart' : 'outrostart'); + dispatch_event(element, t2 === 1 ? 'introstart' : 'outrostart'); // TODO not for `animate:` var { delay = 0, duration, css, tick, easing = linear } = options; diff --git a/packages/svelte/src/internal/client/types.d.ts b/packages/svelte/src/internal/client/types.d.ts index 92836490fd..e4be91bfc9 100644 --- a/packages/svelte/src/internal/client/types.d.ts +++ b/packages/svelte/src/internal/client/types.d.ts @@ -123,6 +123,8 @@ export type EachBlock = { }; export type EachItemBlock = { + /** animation manager */ + a: AnimationManager | null; /** dom */ d: null | TemplateNode | Array; /** effect */ @@ -162,6 +164,11 @@ export interface TransitionManager { stop: () => void; } +export interface AnimationManager { + measure: () => void; + apply: () => void; +} + export interface Animation { abort: () => void; neuter: () => void; diff --git a/packages/svelte/tests/runtime-legacy/samples/dynamic-element-animation/_config.js b/packages/svelte/tests/runtime-legacy/samples/dynamic-element-animation/_config.js index 6315a32d86..bf906f4da4 100644 --- a/packages/svelte/tests/runtime-legacy/samples/dynamic-element-animation/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/dynamic-element-animation/_config.js @@ -23,7 +23,6 @@ export default test({ `, test({ assert, component, target, raf }) { - raf.tick(0); component.tag = 'p'; assert.equal(target.querySelectorAll('p').length, 5); @@ -52,11 +51,11 @@ export default test({ ]; divs = target.querySelectorAll('div'); - assert.ok(~divs[0].style.transform); - assert.equal(divs[1].style.transform, 'translate(1px, 0px)'); - assert.equal(divs[2].style.transform, 'translate(1px, 0px)'); - assert.equal(divs[3].style.transform, 'translate(1px, 0px)'); - assert.ok(~divs[4].style.transform); + assert.equal(divs[0].style.transform, 'translate(0px, 120px)'); + assert.equal(divs[1].style.transform, 'translate(0px, 0px)'); + assert.equal(divs[2].style.transform, 'translate(0px, 0px)'); + assert.equal(divs[3].style.transform, 'translate(0px, 0px)'); + assert.equal(divs[4].style.transform, 'translate(0px, -120px)'); raf.tick(100); assert.deepEqual([divs[0].style.transform, divs[4].style.transform], ['', '']); diff --git a/packages/svelte/tests/runtime-legacy/samples/dynamic-element-animation/main.svelte b/packages/svelte/tests/runtime-legacy/samples/dynamic-element-animation/main.svelte index 596d12c77a..0fa9fecf30 100644 --- a/packages/svelte/tests/runtime-legacy/samples/dynamic-element-animation/main.svelte +++ b/packages/svelte/tests/runtime-legacy/samples/dynamic-element-animation/main.svelte @@ -8,11 +8,11 @@ return { duration: 100, - css: (t, u) => `transform: translate(${u + dx}px, ${u * dy}px)` + css: (t, u) => `transform: translate(${u * dx}px, ${u * dy}px)` }; } {#each things as thing (thing.id)} {thing.name} -{/each} \ No newline at end of file +{/each}