fix: support dynamic transition functions (#9844)

* fix: support dynamic transition functions

* add test

* lint

* load dynamic code lazily

load dynamic code lazily

load dynamic code lazily
pull/9848/head
Dominic Gannaway 2 years ago committed by GitHub
parent ab21253073
commit fd78acfec9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: support dynamic transition functions

@ -1767,7 +1767,9 @@ export const template_visitors = {
b.call( b.call(
'$.animate', '$.animate',
state.node, state.node,
/** @type {import('estree').Expression} */ (visit(parse_directive_name(node.name))), b.thunk(
/** @type {import('estree').Expression} */ (visit(parse_directive_name(node.name)))
),
expression expression
) )
) )
@ -1791,7 +1793,9 @@ export const template_visitors = {
b.call( b.call(
type, type,
state.node, state.node,
/** @type {import('estree').Expression} */ (visit(parse_directive_name(node.name))), b.thunk(
/** @type {import('estree').Expression} */ (visit(parse_directive_name(node.name)))
),
expression, expression,
node.modifiers.includes('global') ? b.true : b.false node.modifiers.includes('global') ? b.true : b.false
) )

@ -175,10 +175,13 @@ export function create_each_block(flags, anchor) {
*/ */
export function create_each_item_block(item, index, key) { export function create_each_item_block(item, index, key) {
return { return {
// animate transition
a: null,
// dom // dom
d: null, d: null,
// effect // effect
e: null, e: null,
// index
i: index, i: index,
// key // key
k: key, k: key,

@ -686,7 +686,7 @@ function destroy_active_transition_blocks(active_transitions) {
* @param {import('./types.js').Block} block * @param {import('./types.js').Block} block
* @returns {Text | Element | Comment} * @returns {Text | Element | Comment}
*/ */
function get_first_element(block) { export function get_first_element(block) {
const current = block.d; const current = block.d;
if (is_array(current)) { if (is_array(current)) {
@ -717,25 +717,9 @@ function update_each_item_block(block, item, index, type) {
const transitions = block.s; const transitions = block.s;
const index_is_reactive = (type & EACH_INDEX_REACTIVE) !== 0; const index_is_reactive = (type & EACH_INDEX_REACTIVE) !== 0;
// Handle each item animations // Handle each item animations
if (transitions !== null && (type & EACH_KEYED) !== 0) { const each_animation = block.a;
let prev_index = block.i; if (transitions !== null && (type & EACH_KEYED) !== 0 && each_animation !== null) {
if (index_is_reactive) { each_animation(block, transitions, index, index_is_reactive);
prev_index = /** @type {import('./types.js').Signal<number>} */ (prev_index).v;
}
const items = block.p.v;
if (prev_index !== index && /** @type {number} */ (index) < items.length) {
const from_dom = /** @type {Element} */ (get_first_element(block));
const from = from_dom.getBoundingClientRect();
// Cancel any existing key transitions
for (const transition of transitions) {
if (transition.r === 'key') {
transition.c();
}
}
schedule_task(() => {
trigger_transitions(transitions, 'key', from);
});
}
} }
if (index_is_reactive) { if (index_is_reactive) {
set_signal_value(/** @type {import('./types.js').Signal<number>} */ (block.i), index); set_signal_value(/** @type {import('./types.js').Signal<number>} */ (block.i), index);

@ -2086,49 +2086,49 @@ export function html(dom, get_value, svg) {
/** /**
* @template P * @template P
* @param {HTMLElement} dom * @param {HTMLElement} dom
* @param {import('./types.js').TransitionFn<P | undefined>} transition_fn * @param {() => import('./types.js').TransitionFn<P | undefined>} get_transition_fn
* @param {(() => P) | null} props * @param {(() => P) | null} props
* @param {any} global * @param {any} global
* @returns {void} * @returns {void}
*/ */
export function transition(dom, transition_fn, props, global = false) { export function transition(dom, get_transition_fn, props, global = false) {
bind_transition(dom, transition_fn, props, 'both', global); bind_transition(dom, get_transition_fn, props, 'both', global);
} }
/** /**
* @template P * @template P
* @param {HTMLElement} dom * @param {HTMLElement} dom
* @param {import('./types.js').TransitionFn<P | undefined>} transition_fn * @param {() => import('./types.js').TransitionFn<P | undefined>} get_transition_fn
* @param {(() => P) | null} props * @param {(() => P) | null} props
* @returns {void} * @returns {void}
*/ */
export function animate(dom, transition_fn, props) { export function animate(dom, get_transition_fn, props) {
bind_transition(dom, transition_fn, props, 'key', false); bind_transition(dom, get_transition_fn, props, 'key', false);
} }
/** /**
* @template P * @template P
* @param {HTMLElement} dom * @param {HTMLElement} dom
* @param {import('./types.js').TransitionFn<P | undefined>} transition_fn * @param {() => import('./types.js').TransitionFn<P | undefined>} get_transition_fn
* @param {(() => P) | null} props * @param {(() => P) | null} props
* @param {any} global * @param {any} global
* @returns {void} * @returns {void}
*/ */
function in_fn(dom, transition_fn, props, global = false) { function in_fn(dom, get_transition_fn, props, global = false) {
bind_transition(dom, transition_fn, props, 'in', global); bind_transition(dom, get_transition_fn, props, 'in', global);
} }
export { in_fn as in }; export { in_fn as in };
/** /**
* @template P * @template P
* @param {HTMLElement} dom * @param {HTMLElement} dom
* @param {import('./types.js').TransitionFn<P | undefined>} transition_fn * @param {() => import('./types.js').TransitionFn<P | undefined>} get_transition_fn
* @param {(() => P) | null} props * @param {(() => P) | null} props
* @param {any} global * @param {any} global
* @returns {void} * @returns {void}
*/ */
export function out(dom, transition_fn, props, global = false) { export function out(dom, get_transition_fn, props, global = false) {
bind_transition(dom, transition_fn, props, 'out', global); bind_transition(dom, get_transition_fn, props, 'out', global);
} }
/** /**

@ -9,7 +9,7 @@ import {
KEY_BLOCK, KEY_BLOCK,
ROOT_BLOCK ROOT_BLOCK
} from './block.js'; } from './block.js';
import { destroy_each_item_block } from './each.js'; import { destroy_each_item_block, get_first_element } from './each.js';
import { append_child } from './operations.js'; import { append_child } from './operations.js';
import { empty } from './render.js'; import { empty } from './render.js';
import { import {
@ -21,6 +21,7 @@ import {
managed_effect, managed_effect,
managed_pre_effect, managed_pre_effect,
mark_subtree_inert, mark_subtree_inert,
schedule_task,
untrack untrack
} from './runtime.js'; } from './runtime.js';
import { raf } from './timing.js'; import { raf } from './timing.js';
@ -411,13 +412,13 @@ function is_transition_block(block) {
/** /**
* @template P * @template P
* @param {HTMLElement} dom * @param {HTMLElement} dom
* @param {import('./types.js').TransitionFn<P | undefined> | import('./types.js').AnimateFn<P | undefined>} transition_fn * @param {() => import('./types.js').TransitionFn<P | undefined> | import('./types.js').AnimateFn<P | undefined>} get_transition_fn
* @param {(() => P) | null} props_fn * @param {(() => P) | null} props_fn
* @param {'in' | 'out' | 'both' | 'key'} direction * @param {'in' | 'out' | 'both' | 'key'} direction
* @param {boolean} global * @param {boolean} global
* @returns {void} * @returns {void}
*/ */
export function bind_transition(dom, transition_fn, props_fn, direction, global) { export function bind_transition(dom, get_transition_fn, props_fn, direction, global) {
const transition_effect = /** @type {import('./types.js').EffectSignal} */ (current_effect); const transition_effect = /** @type {import('./types.js').EffectSignal} */ (current_effect);
const block = current_block; const block = current_block;
const props = props_fn === null ? {} : props_fn(); const props = props_fn === null ? {} : props_fn();
@ -432,6 +433,7 @@ export function bind_transition(dom, transition_fn, props_fn, direction, global)
if (transition_block.t === EACH_ITEM_BLOCK) { if (transition_block.t === EACH_ITEM_BLOCK) {
// Lazily apply the each block transition // Lazily apply the each block transition
transition_block.r = each_item_transition; transition_block.r = each_item_transition;
transition_block.a = each_item_animate;
transition_block = transition_block.p; transition_block = transition_block.p;
} else if (transition_block.t === AWAIT_BLOCK && transition_block.n /* pending */) { } else if (transition_block.t === AWAIT_BLOCK && transition_block.n /* pending */) {
can_show_intro_on_mount = false; can_show_intro_on_mount = false;
@ -458,6 +460,11 @@ export function bind_transition(dom, transition_fn, props_fn, direction, global)
let transition; let transition;
effect(() => { effect(() => {
if (transition !== undefined) {
// Destroy any existing transitions first
transition.x();
}
const transition_fn = get_transition_fn();
/** @param {DOMRect} [from] */ /** @param {DOMRect} [from] */
const init = (from) => const init = (from) =>
untrack(() => untrack(() =>
@ -641,3 +648,31 @@ function each_item_transition(transition) {
}); });
transitions.add(transition); transitions.add(transition);
} }
/**
*
* @param {import('./types.js').EachItemBlock} block
* @param {Set<import('./types.js').Transition>} transitions
* @param {number} index
* @param {boolean} index_is_reactive
*/
function each_item_animate(block, transitions, index, index_is_reactive) {
let prev_index = block.i;
if (index_is_reactive) {
prev_index = /** @type {import('./types.js').Signal<number>} */ (prev_index).v;
}
const items = block.p.v;
if (prev_index !== index && /** @type {number} */ (index) < items.length) {
const from_dom = /** @type {Element} */ (get_first_element(block));
const from = from_dom.getBoundingClientRect();
// Cancel any existing key transitions
for (const transition of transitions) {
if (transition.r === 'key') {
transition.c();
}
}
schedule_task(() => {
trigger_transitions(transitions, 'key', from);
});
}
}

@ -285,6 +285,15 @@ export type EachBlock = {
}; };
export type EachItemBlock = { export type EachItemBlock = {
/** transition */
a:
| null
| ((
block: EachItemBlock,
transitions: Set<Transition>,
index: number,
index_is_reactive: boolean
) => void);
/** dom */ /** dom */
d: null | TemplateNode | Array<TemplateNode>; d: null | TemplateNode | Array<TemplateNode>;
/** effect */ /** effect */

@ -0,0 +1,25 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
import { log } from './log.js';
export default test({
before_test() {
log.length = 0;
},
async test({ assert, target }) {
const [b1, b2] = target.querySelectorAll('button');
flushSync(() => {
b1.click();
});
assert.deepEqual(log, ['transition 2']);
flushSync(() => {
b2.click();
});
assert.deepEqual(log, ['transition 2', 'transition 1', 'transition 1']);
}
});

@ -0,0 +1,2 @@
/** @type {any[]} */
export const log = [];

@ -0,0 +1,33 @@
<script>
import { log } from './log.js';
function transition1() {
log.push('transition 1')
return {
tick() {
}
}
}
function transition2() {
log.push('transition 2')
return {
tick() {
}
}
}
let toggle = $state(false);
let toggleTransition = $state(false);
const derived = $derived(toggleTransition ? transition1 : transition2)
</script>
<button on:click={() => toggle = !toggle}>{toggle}</button>
<button on:click={() => toggleTransition = !toggleTransition}>{toggleTransition}</button>
{#if toggle}<div transition:derived></div>{/if}
Loading…
Cancel
Save