diff --git a/.changeset/sixty-grapes-dream.md b/.changeset/sixty-grapes-dream.md new file mode 100644 index 000000000000..8d30d6b8e99e --- /dev/null +++ b/.changeset/sixty-grapes-dream.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: enable `animate:` directive for snippets diff --git a/documentation/docs/98-reference/.generated/compile-errors.md b/documentation/docs/98-reference/.generated/compile-errors.md index 298363f78d38..f940281009cf 100644 --- a/documentation/docs/98-reference/.generated/compile-errors.md +++ b/documentation/docs/98-reference/.generated/compile-errors.md @@ -9,7 +9,7 @@ An element can only have one 'animate' directive ### animation_invalid_placement ``` -An element that uses the `animate:` directive must be the only child of a keyed `{#each ...}` block +An element that uses the `animate:` directive must be the only child of a keyed `{#each ...}` block, or an only child of a snippet ``` ### animation_missing_key diff --git a/packages/svelte/messages/compile-errors/template.md b/packages/svelte/messages/compile-errors/template.md index 02961b61fccc..872a2fca4145 100644 --- a/packages/svelte/messages/compile-errors/template.md +++ b/packages/svelte/messages/compile-errors/template.md @@ -4,7 +4,7 @@ ## animation_invalid_placement -> An element that uses the `animate:` directive must be the only child of a keyed `{#each ...}` block +> An element that uses the `animate:` directive must be the only child of a keyed `{#each ...}` block, or an only child of a snippet ## animation_missing_key diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index 9ea13e811e5f..c1e56ac36817 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -625,12 +625,12 @@ export function animation_duplicate(node) { } /** - * An element that uses the `animate:` directive must be the only child of a keyed `{#each ...}` block + * An element that uses the `animate:` directive must be the only child of a keyed `{#each ...}` block, or an only child of a snippet * @param {null | number | NodeLike} node * @returns {never} */ export function animation_invalid_placement(node) { - e(node, "animation_invalid_placement", `An element that uses the \`animate:\` directive must be the only child of a keyed \`{#each ...}\` block\nhttps://svelte.dev/e/animation_invalid_placement`); + e(node, "animation_invalid_placement", `An element that uses the \`animate:\` directive must be the only child of a keyed \`{#each ...}\` block, or an only child of a snippet\nhttps://svelte.dev/e/animation_invalid_placement`); } /** diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js index 045224276a2e..0f99428ab17e 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js @@ -27,9 +27,13 @@ export function RenderTag(node, context) { */ let resolved = callee.type === 'Identifier' && is_resolved_snippet(binding); + /** @type {AST.SnippetBlock | undefined} */ + let snippet; + if (binding?.initial?.type === 'SnippetBlock') { // if this render tag unambiguously references a local snippet, our job is easy - node.metadata.snippets.add(binding.initial); + snippet = binding.initial; + node.metadata.snippets.add(snippet); } context.state.analysis.snippet_renderers.set(node, resolved); @@ -50,7 +54,44 @@ export function RenderTag(node, context) { e.render_tag_invalid_call_expression(node); } + const parent = context.path.at(-2); + const is_animated = snippet?.body.nodes.some((n) => is_animate_directive(n)); + + if (is_animated) { + if (parent?.type !== 'EachBlock') { + e.animation_invalid_placement(node); + } else if (!parent.key) { + e.animation_missing_key(parent); + } else if ( + parent.body.nodes.filter( + (n) => + n.type !== 'Comment' && + n.type !== 'ConstTag' && + (n.type !== 'Text' || n.data.trim() !== '') + ).length > 1 + ) { + e.animation_invalid_placement(node); + } + } + mark_subtree_dynamic(context.path); context.next({ ...context.state, render_tag: node }); } + +/** + * @param {AST.Text | AST.Tag | AST.ElementLike | AST.Comment | AST.Block} child + * @param {boolean} render_snippet + * @returns {boolean} + */ +function is_animate_directive(child, render_snippet = false) { + if (child.type === 'RenderTag') { + if (render_snippet) return false; // Prevent infinite recursion + for (const snippet_block of child.metadata.snippets) { + if (snippet_block.body.nodes.includes(child)) break; + return snippet_block.body.nodes.some((n) => is_animate_directive(n, true)); + } + } + if (child.type !== 'RegularElement' && child.type !== 'SvelteElement') return false; + return child.attributes.some((attr) => attr.type === 'AnimateDirective'); +} diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/element.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/element.js index 725a4aded89d..306bbad985e1 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/element.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/element.js @@ -91,9 +91,9 @@ export function validate_element(node, context) { validate_attribute_name(attribute); } else if (attribute.type === 'AnimateDirective') { const parent = context.path.at(-2); - if (parent?.type !== 'EachBlock') { + if (parent?.type !== 'EachBlock' && parent?.type !== 'SnippetBlock') { e.animation_invalid_placement(attribute); - } else if (!parent.key) { + } else if (parent.type === 'EachBlock' && !parent.key) { e.animation_missing_key(attribute); } else if ( parent.body.nodes.filter( diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js index 9f70981205a1..6aca3baaf9a4 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js @@ -83,13 +83,7 @@ export function EachBlock(node, context) { // Since `animate:` can only appear on elements that are the sole child of a keyed each block, // we can determine at compile time whether the each block is animated or not (in which // case it should measure animated elements before and after reconciliation). - 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'); - }) - ) { + if (node.key && node.body.nodes.some(is_animate_directive)) { flags |= EACH_IS_ANIMATED; } @@ -348,3 +342,14 @@ function collect_transitive_dependencies(binding, seen = new Set()) { return [...seen]; } + +/** @param {AST.Text | AST.Tag | AST.ElementLike | AST.Comment | AST.Block} child */ +function is_animate_directive(child) { + if (child.type === 'RenderTag') { + for (const snippet_block of child.metadata.snippets) { + return snippet_block.body.nodes.some(is_animate_directive); + } + } + if (child.type !== 'RegularElement' && child.type !== 'SvelteElement') return false; + return child.attributes.some((attr) => attr.type === 'AnimateDirective'); +} diff --git a/packages/svelte/tests/validator/samples/animation-not-in-each/errors.json b/packages/svelte/tests/validator/samples/animation-not-in-each/errors.json index 2b5f73585dc3..c30abe9189f2 100644 --- a/packages/svelte/tests/validator/samples/animation-not-in-each/errors.json +++ b/packages/svelte/tests/validator/samples/animation-not-in-each/errors.json @@ -1,7 +1,7 @@ [ { "code": "animation_invalid_placement", - "message": "An element that uses the `animate:` directive must be the only child of a keyed `{#each ...}` block", + "message": "An element that uses the `animate:` directive must be the only child of a keyed `{#each ...}` block, or an only child of a snippet", "start": { "line": 5, "column": 5 diff --git a/packages/svelte/tests/validator/samples/animation-siblings/errors.json b/packages/svelte/tests/validator/samples/animation-siblings/errors.json index b6412ebca464..b05ee902be58 100644 --- a/packages/svelte/tests/validator/samples/animation-siblings/errors.json +++ b/packages/svelte/tests/validator/samples/animation-siblings/errors.json @@ -1,7 +1,7 @@ [ { "code": "animation_invalid_placement", - "message": "An element that uses the `animate:` directive must be the only child of a keyed `{#each ...}` block", + "message": "An element that uses the `animate:` directive must be the only child of a keyed `{#each ...}` block, or an only child of a snippet", "start": { "line": 6, "column": 6