From da7adefa844265eecc9c336abfc727bc05b4f16e Mon Sep 17 00:00:00 2001
From: Vadim Kruglov <49036220+quiteeasy@users.noreply.github.com>
Date: Mon, 22 Apr 2024 19:46:11 +0700
Subject: [PATCH] fix(compiler-core): handle template ref bound via v-bind
object on v-for (#10706)
close #10696
---
.../transformElement.spec.ts.snap | 228 ++++++++++++++++++
.../transforms/transformElement.spec.ts | 39 +++
.../__tests__/transforms/vFor.spec.ts | 2 +-
.../src/transforms/transformElement.ts | 32 +--
4 files changed, 285 insertions(+), 16 deletions(-)
create mode 100644 packages/compiler-core/__tests__/transforms/__snapshots__/transformElement.spec.ts.snap
diff --git a/packages/compiler-core/__tests__/transforms/__snapshots__/transformElement.spec.ts.snap b/packages/compiler-core/__tests__/transforms/__snapshots__/transformElement.spec.ts.snap
new file mode 100644
index 00000000000..3da778eb675
--- /dev/null
+++ b/packages/compiler-core/__tests__/transforms/__snapshots__/transformElement.spec.ts.snap
@@ -0,0 +1,228 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`compiler: v-for > codegen > basic v-for 1`] = `
+"const _Vue = Vue
+
+return function render(_ctx, _cache) {
+ with (_ctx) {
+ const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
+
+ return (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(items, (item) => {
+ return (_openBlock(), _createElementBlock("span"))
+ }), 256 /* UNKEYED_FRAGMENT */))
+ }
+}"
+`;
+
+exports[`compiler: v-for > codegen > keyed template v-for 1`] = `
+"const _Vue = Vue
+
+return function render(_ctx, _cache) {
+ with (_ctx) {
+ const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock, createElementVNode: _createElementVNode } = _Vue
+
+ return (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(items, (item) => {
+ return (_openBlock(), _createElementBlock(_Fragment, { key: item }, [
+ "hello",
+ _createElementVNode("span")
+ ], 64 /* STABLE_FRAGMENT */))
+ }), 128 /* KEYED_FRAGMENT */))
+ }
+}"
+`;
+
+exports[`compiler: v-for > codegen > keyed v-for 1`] = `
+"const _Vue = Vue
+
+return function render(_ctx, _cache) {
+ with (_ctx) {
+ const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
+
+ return (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(items, (item) => {
+ return (_openBlock(), _createElementBlock("span", { key: item }))
+ }), 128 /* KEYED_FRAGMENT */))
+ }
+}"
+`;
+
+exports[`compiler: v-for > codegen > skipped key 1`] = `
+"const _Vue = Vue
+
+return function render(_ctx, _cache) {
+ with (_ctx) {
+ const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
+
+ return (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(items, (item, __, index) => {
+ return (_openBlock(), _createElementBlock("span"))
+ }), 256 /* UNKEYED_FRAGMENT */))
+ }
+}"
+`;
+
+exports[`compiler: v-for > codegen > skipped value & key 1`] = `
+"const _Vue = Vue
+
+return function render(_ctx, _cache) {
+ with (_ctx) {
+ const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
+
+ return (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(items, (_, __, index) => {
+ return (_openBlock(), _createElementBlock("span"))
+ }), 256 /* UNKEYED_FRAGMENT */))
+ }
+}"
+`;
+
+exports[`compiler: v-for > codegen > skipped value 1`] = `
+"const _Vue = Vue
+
+return function render(_ctx, _cache) {
+ with (_ctx) {
+ const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
+
+ return (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(items, (_, key, index) => {
+ return (_openBlock(), _createElementBlock("span"))
+ }), 256 /* UNKEYED_FRAGMENT */))
+ }
+}"
+`;
+
+exports[`compiler: v-for > codegen > template v-for 1`] = `
+"const _Vue = Vue
+
+return function render(_ctx, _cache) {
+ with (_ctx) {
+ const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock, createElementVNode: _createElementVNode } = _Vue
+
+ return (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(items, (item) => {
+ return (_openBlock(), _createElementBlock(_Fragment, null, [
+ "hello",
+ _createElementVNode("span")
+ ], 64 /* STABLE_FRAGMENT */))
+ }), 256 /* UNKEYED_FRAGMENT */))
+ }
+}"
+`;
+
+exports[`compiler: v-for > codegen > template v-for key injection with single child 1`] = `
+"const _Vue = Vue
+
+return function render(_ctx, _cache) {
+ with (_ctx) {
+ const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
+
+ return (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(items, (item) => {
+ return (_openBlock(), _createElementBlock("span", {
+ key: item.id,
+ id: item.id
+ }, null, 8 /* PROPS */, ["id"]))
+ }), 128 /* KEYED_FRAGMENT */))
+ }
+}"
+`;
+
+exports[`compiler: v-for > codegen > template v-for w/ 1`] = `
+"const _Vue = Vue
+
+return function render(_ctx, _cache) {
+ with (_ctx) {
+ const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock, renderSlot: _renderSlot } = _Vue
+
+ return (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(items, (item) => {
+ return _renderSlot($slots, "default")
+ }), 256 /* UNKEYED_FRAGMENT */))
+ }
+}"
+`;
+
+exports[`compiler: v-for > codegen > v-for on 1`] = `
+"const _Vue = Vue
+
+return function render(_ctx, _cache) {
+ with (_ctx) {
+ const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock, renderSlot: _renderSlot } = _Vue
+
+ return (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(items, (item) => {
+ return _renderSlot($slots, "default")
+ }), 256 /* UNKEYED_FRAGMENT */))
+ }
+}"
+`;
+
+exports[`compiler: v-for > codegen > v-for on element with custom directive 1`] = `
+"const _Vue = Vue
+
+return function render(_ctx, _cache) {
+ with (_ctx) {
+ const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock, resolveDirective: _resolveDirective, withDirectives: _withDirectives } = _Vue
+
+ const _directive_foo = _resolveDirective("foo")
+
+ return (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(list, (i) => {
+ return _withDirectives((_openBlock(), _createElementBlock("div", null, null, 512 /* NEED_PATCH */)), [
+ [_directive_foo]
+ ])
+ }), 256 /* UNKEYED_FRAGMENT */))
+ }
+}"
+`;
+
+exports[`compiler: v-for > codegen > v-for with constant expression 1`] = `
+"const _Vue = Vue
+
+return function render(_ctx, _cache) {
+ with (_ctx) {
+ const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock, toDisplayString: _toDisplayString, createElementVNode: _createElementVNode } = _Vue
+
+ return (_openBlock(), _createElementBlock(_Fragment, null, _renderList(10, (item) => {
+ return _createElementVNode("p", null, _toDisplayString(item), 1 /* TEXT */)
+ }), 64 /* STABLE_FRAGMENT */))
+ }
+}"
+`;
+
+exports[`compiler: v-for > codegen > v-if + v-for 1`] = `
+"const _Vue = Vue
+
+return function render(_ctx, _cache) {
+ with (_ctx) {
+ const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock, createCommentVNode: _createCommentVNode } = _Vue
+
+ return ok
+ ? (_openBlock(true), _createElementBlock(_Fragment, { key: 0 }, _renderList(list, (i) => {
+ return (_openBlock(), _createElementBlock("div"))
+ }), 256 /* UNKEYED_FRAGMENT */))
+ : _createCommentVNode("v-if", true)
+ }
+}"
+`;
+
+exports[`compiler: v-for > codegen > v-if + v-for on 1`] = `
+"const _Vue = Vue
+
+return function render(_ctx, _cache) {
+ with (_ctx) {
+ const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock, createCommentVNode: _createCommentVNode } = _Vue
+
+ return ok
+ ? (_openBlock(true), _createElementBlock(_Fragment, { key: 0 }, _renderList(list, (i) => {
+ return (_openBlock(), _createElementBlock(_Fragment, null, [], 64 /* STABLE_FRAGMENT */))
+ }), 256 /* UNKEYED_FRAGMENT */))
+ : _createCommentVNode("v-if", true)
+ }
+}"
+`;
+
+exports[`compiler: v-for > codegen > value + key + index 1`] = `
+"const _Vue = Vue
+
+return function render(_ctx, _cache) {
+ with (_ctx) {
+ const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
+
+ return (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(items, (item, key, index) => {
+ return (_openBlock(), _createElementBlock("span"))
+ }), 256 /* UNKEYED_FRAGMENT */))
+ }
+}"
+`;
diff --git a/packages/compiler-core/__tests__/transforms/transformElement.spec.ts b/packages/compiler-core/__tests__/transforms/transformElement.spec.ts
index c30840a21a6..6c2dab9625f 100644
--- a/packages/compiler-core/__tests__/transforms/transformElement.spec.ts
+++ b/packages/compiler-core/__tests__/transforms/transformElement.spec.ts
@@ -39,6 +39,7 @@ import { transformBind } from '../../src/transforms/vBind'
import { PatchFlags } from '@vue/shared'
import { createObjectMatcher, genFlagText } from '../testUtils'
import { transformText } from '../../src/transforms/transformText'
+import { parseWithForTransform } from './vFor.spec'
function parseWithElementTransform(
template: string,
@@ -1338,4 +1339,42 @@ describe('compiler: element transform', () => {
isBlock: false,
})
})
+
+ test('ref_for marker on static ref', () => {
+ const { node } = parseWithForTransform(``)
+ expect((node.children[0] as any).codegenNode.props).toMatchObject(
+ createObjectMatcher({
+ ref_for: `[true]`,
+ ref: 'x',
+ }),
+ )
+ })
+
+ test('ref_for marker on dynamic ref', () => {
+ const { node } = parseWithForTransform(``)
+ expect((node.children[0] as any).codegenNode.props).toMatchObject(
+ createObjectMatcher({
+ ref_for: `[true]`,
+ ref: '[x]',
+ }),
+ )
+ })
+
+ test('ref_for marker on v-bind', () => {
+ const { node } = parseWithForTransform(``)
+ expect((node.children[0] as any).codegenNode.props).toMatchObject({
+ type: NodeTypes.JS_CALL_EXPRESSION,
+ callee: MERGE_PROPS,
+ arguments: [
+ createObjectMatcher({
+ ref_for: `[true]`,
+ }),
+ {
+ type: NodeTypes.SIMPLE_EXPRESSION,
+ content: 'x',
+ isStatic: false,
+ },
+ ],
+ })
+ })
})
diff --git a/packages/compiler-core/__tests__/transforms/vFor.spec.ts b/packages/compiler-core/__tests__/transforms/vFor.spec.ts
index fb3d7dc7fb8..7fabcbb579c 100644
--- a/packages/compiler-core/__tests__/transforms/vFor.spec.ts
+++ b/packages/compiler-core/__tests__/transforms/vFor.spec.ts
@@ -21,7 +21,7 @@ import { FRAGMENT, RENDER_LIST, RENDER_SLOT } from '../../src/runtimeHelpers'
import { PatchFlags } from '@vue/shared'
import { createObjectMatcher, genFlagText } from '../testUtils'
-function parseWithForTransform(
+export function parseWithForTransform(
template: string,
options: CompilerOptions = {},
) {
diff --git a/packages/compiler-core/src/transforms/transformElement.ts b/packages/compiler-core/src/transforms/transformElement.ts
index ca6e59df32b..5c431f9d4da 100644
--- a/packages/compiler-core/src/transforms/transformElement.ts
+++ b/packages/compiler-core/src/transforms/transformElement.ts
@@ -433,6 +433,18 @@ export function buildProps(
if (arg) mergeArgs.push(arg)
}
+ // mark template ref on v-for
+ const pushRefVForMarker = () => {
+ if (context.scopes.vFor > 0) {
+ properties.push(
+ createObjectProperty(
+ createSimpleExpression('ref_for', true),
+ createSimpleExpression('true'),
+ ),
+ )
+ }
+ }
+
const analyzePatchFlag = ({ key, value }: Property) => {
if (isStaticExp(key)) {
const name = key.content
@@ -502,14 +514,7 @@ export function buildProps(
let isStatic = true
if (name === 'ref') {
hasRef = true
- if (context.scopes.vFor > 0) {
- properties.push(
- createObjectProperty(
- createSimpleExpression('ref_for', true),
- createSimpleExpression('true'),
- ),
- )
- }
+ pushRefVForMarker()
// in inline mode there is no setupState object, so we can't use string
// keys to set the ref. Instead, we need to transform it to pass the
// actual ref instead.
@@ -601,13 +606,8 @@ export function buildProps(
shouldUseBlock = true
}
- if (isVBind && isStaticArgOf(arg, 'ref') && context.scopes.vFor > 0) {
- properties.push(
- createObjectProperty(
- createSimpleExpression('ref_for', true),
- createSimpleExpression('true'),
- ),
- )
+ if (isVBind && isStaticArgOf(arg, 'ref')) {
+ pushRefVForMarker()
}
// special case for v-bind and v-on with no argument
@@ -615,6 +615,8 @@ export function buildProps(
hasDynamicKeys = true
if (exp) {
if (isVBind) {
+ // #10696 in case a v-bind object contains ref
+ pushRefVForMarker()
// have to merge early for compat build check
pushMergeArg()
if (__COMPAT__) {