feat: support ref in v-for, remove compat deprecation warnings
This commit is contained in:
parent
a1167c57e5
commit
41c18effea
@ -936,7 +936,7 @@ describe('compiler: element transform', () => {
|
|||||||
expect(node.patchFlag).toBe(genFlagText(PatchFlags.NEED_PATCH))
|
expect(node.patchFlag).toBe(genFlagText(PatchFlags.NEED_PATCH))
|
||||||
})
|
})
|
||||||
|
|
||||||
test('the binding exists (inline ref input)', () => {
|
test('script setup inline mode template ref (binding exists)', () => {
|
||||||
const { node } = parseWithElementTransform(`<input ref="input"/>`, {
|
const { node } = parseWithElementTransform(`<input ref="input"/>`, {
|
||||||
inline: true,
|
inline: true,
|
||||||
bindingMetadata: {
|
bindingMetadata: {
|
||||||
@ -949,31 +949,30 @@ describe('compiler: element transform', () => {
|
|||||||
{
|
{
|
||||||
type: NodeTypes.JS_PROPERTY,
|
type: NodeTypes.JS_PROPERTY,
|
||||||
key: {
|
key: {
|
||||||
type: NodeTypes.SIMPLE_EXPRESSION,
|
content: 'ref_key',
|
||||||
|
isStatic: true
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
content: 'input',
|
||||||
|
isStatic: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: NodeTypes.JS_PROPERTY,
|
||||||
|
key: {
|
||||||
content: 'ref',
|
content: 'ref',
|
||||||
isStatic: true
|
isStatic: true
|
||||||
},
|
},
|
||||||
value: {
|
value: {
|
||||||
type: NodeTypes.JS_FUNCTION_EXPRESSION,
|
content: 'input',
|
||||||
params: ['_value', '_refs'],
|
isStatic: false
|
||||||
body: {
|
|
||||||
type: NodeTypes.JS_BLOCK_STATEMENT,
|
|
||||||
body: [
|
|
||||||
{
|
|
||||||
content: `_refs['input'] = _value`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
content: 'input.value = _value'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test('the binding not exists (inline ref input)', () => {
|
test('script setup inline mode template ref (binding does not exist)', () => {
|
||||||
const { node } = parseWithElementTransform(`<input ref="input"/>`, {
|
const { node } = parseWithElementTransform(`<input ref="input"/>`, {
|
||||||
inline: true
|
inline: true
|
||||||
})
|
})
|
||||||
@ -983,96 +982,12 @@ describe('compiler: element transform', () => {
|
|||||||
{
|
{
|
||||||
type: NodeTypes.JS_PROPERTY,
|
type: NodeTypes.JS_PROPERTY,
|
||||||
key: {
|
key: {
|
||||||
type: NodeTypes.SIMPLE_EXPRESSION,
|
|
||||||
content: 'ref',
|
content: 'ref',
|
||||||
isStatic: true
|
isStatic: true
|
||||||
},
|
},
|
||||||
value: {
|
value: {
|
||||||
type: NodeTypes.JS_FUNCTION_EXPRESSION,
|
content: 'input',
|
||||||
params: ['_value', '_refs'],
|
|
||||||
body: {
|
|
||||||
type: NodeTypes.JS_BLOCK_STATEMENT,
|
|
||||||
body: [
|
|
||||||
{
|
|
||||||
content: `_refs['input'] = _value`
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('the binding not exists (inline maybe ref input)', () => {
|
|
||||||
const { node } = parseWithElementTransform(`<input ref="input"/>`, {
|
|
||||||
inline: true,
|
|
||||||
bindingMetadata: {
|
|
||||||
input: BindingTypes.SETUP_MAYBE_REF
|
|
||||||
}
|
|
||||||
})
|
|
||||||
expect(node.props).toMatchObject({
|
|
||||||
type: NodeTypes.JS_OBJECT_EXPRESSION,
|
|
||||||
properties: [
|
|
||||||
{
|
|
||||||
type: NodeTypes.JS_PROPERTY,
|
|
||||||
key: {
|
|
||||||
type: NodeTypes.SIMPLE_EXPRESSION,
|
|
||||||
content: 'ref',
|
|
||||||
isStatic: true
|
isStatic: true
|
||||||
},
|
|
||||||
value: {
|
|
||||||
type: NodeTypes.JS_FUNCTION_EXPRESSION,
|
|
||||||
params: ['_value', '_refs'],
|
|
||||||
body: {
|
|
||||||
type: NodeTypes.JS_BLOCK_STATEMENT,
|
|
||||||
body: [
|
|
||||||
{
|
|
||||||
content: `_refs['input'] = _value`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
content: '_isRef(input) && (input.value = _value)'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('the binding not exists (inline let ref input)', () => {
|
|
||||||
const { node } = parseWithElementTransform(`<input ref="input"/>`, {
|
|
||||||
inline: true,
|
|
||||||
bindingMetadata: {
|
|
||||||
input: BindingTypes.SETUP_LET
|
|
||||||
}
|
|
||||||
})
|
|
||||||
expect(node.props).toMatchObject({
|
|
||||||
type: NodeTypes.JS_OBJECT_EXPRESSION,
|
|
||||||
properties: [
|
|
||||||
{
|
|
||||||
type: NodeTypes.JS_PROPERTY,
|
|
||||||
key: {
|
|
||||||
type: NodeTypes.SIMPLE_EXPRESSION,
|
|
||||||
content: 'ref',
|
|
||||||
isStatic: true
|
|
||||||
},
|
|
||||||
value: {
|
|
||||||
type: NodeTypes.JS_FUNCTION_EXPRESSION,
|
|
||||||
params: ['_value', '_refs'],
|
|
||||||
body: {
|
|
||||||
type: NodeTypes.JS_BLOCK_STATEMENT,
|
|
||||||
body: [
|
|
||||||
{
|
|
||||||
content: `_refs['input'] = _value`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
content:
|
|
||||||
'_isRef(input) ? input.value = _value : input = _value'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -20,7 +20,6 @@ export const enum CompilerDeprecationTypes {
|
|||||||
COMPILER_V_BIND_OBJECT_ORDER = 'COMPILER_V_BIND_OBJECT_ORDER',
|
COMPILER_V_BIND_OBJECT_ORDER = 'COMPILER_V_BIND_OBJECT_ORDER',
|
||||||
COMPILER_V_ON_NATIVE = 'COMPILER_V_ON_NATIVE',
|
COMPILER_V_ON_NATIVE = 'COMPILER_V_ON_NATIVE',
|
||||||
COMPILER_V_IF_V_FOR_PRECEDENCE = 'COMPILER_V_IF_V_FOR_PRECEDENCE',
|
COMPILER_V_IF_V_FOR_PRECEDENCE = 'COMPILER_V_IF_V_FOR_PRECEDENCE',
|
||||||
COMPILER_V_FOR_REF = 'COMPILER_V_FOR_REF',
|
|
||||||
COMPILER_NATIVE_TEMPLATE = 'COMPILER_NATIVE_TEMPLATE',
|
COMPILER_NATIVE_TEMPLATE = 'COMPILER_NATIVE_TEMPLATE',
|
||||||
COMPILER_INLINE_TEMPLATE = 'COMPILER_INLINE_TEMPLATE',
|
COMPILER_INLINE_TEMPLATE = 'COMPILER_INLINE_TEMPLATE',
|
||||||
COMPILER_FILTERS = 'COMPILER_FILTER'
|
COMPILER_FILTERS = 'COMPILER_FILTER'
|
||||||
@ -79,13 +78,6 @@ const deprecationData: Record<CompilerDeprecationTypes, DeprecationData> = {
|
|||||||
link: `https://v3.vuejs.org/guide/migration/v-if-v-for.html`
|
link: `https://v3.vuejs.org/guide/migration/v-if-v-for.html`
|
||||||
},
|
},
|
||||||
|
|
||||||
[CompilerDeprecationTypes.COMPILER_V_FOR_REF]: {
|
|
||||||
message:
|
|
||||||
`Ref usage on v-for no longer creates array ref values in Vue 3. ` +
|
|
||||||
`Consider using function refs or refactor to avoid ref usage altogether.`,
|
|
||||||
link: `https://v3.vuejs.org/guide/migration/array-refs.html`
|
|
||||||
},
|
|
||||||
|
|
||||||
[CompilerDeprecationTypes.COMPILER_NATIVE_TEMPLATE]: {
|
[CompilerDeprecationTypes.COMPILER_NATIVE_TEMPLATE]: {
|
||||||
message:
|
message:
|
||||||
`<template> with no special directives will render as a native template ` +
|
`<template> with no special directives will render as a native template ` +
|
||||||
|
@ -19,10 +19,7 @@ import {
|
|||||||
TemplateTextChildNode,
|
TemplateTextChildNode,
|
||||||
DirectiveArguments,
|
DirectiveArguments,
|
||||||
createVNodeCall,
|
createVNodeCall,
|
||||||
ConstantTypes,
|
ConstantTypes
|
||||||
JSChildNode,
|
|
||||||
createFunctionExpression,
|
|
||||||
createBlockStatement
|
|
||||||
} from '../ast'
|
} from '../ast'
|
||||||
import {
|
import {
|
||||||
PatchFlags,
|
PatchFlags,
|
||||||
@ -48,8 +45,7 @@ import {
|
|||||||
KEEP_ALIVE,
|
KEEP_ALIVE,
|
||||||
SUSPENSE,
|
SUSPENSE,
|
||||||
UNREF,
|
UNREF,
|
||||||
GUARD_REACTIVE_PROPS,
|
GUARD_REACTIVE_PROPS
|
||||||
IS_REF
|
|
||||||
} from '../runtimeHelpers'
|
} from '../runtimeHelpers'
|
||||||
import {
|
import {
|
||||||
getInnerRange,
|
getInnerRange,
|
||||||
@ -467,20 +463,32 @@ export function buildProps(
|
|||||||
const prop = props[i]
|
const prop = props[i]
|
||||||
if (prop.type === NodeTypes.ATTRIBUTE) {
|
if (prop.type === NodeTypes.ATTRIBUTE) {
|
||||||
const { loc, name, value } = prop
|
const { loc, name, value } = prop
|
||||||
let valueNode = createSimpleExpression(
|
let isStatic = true
|
||||||
value ? value.content : '',
|
|
||||||
true,
|
|
||||||
value ? value.loc : loc
|
|
||||||
) as JSChildNode
|
|
||||||
if (name === 'ref') {
|
if (name === 'ref') {
|
||||||
hasRef = true
|
hasRef = true
|
||||||
|
if (context.scopes.vFor > 0) {
|
||||||
|
properties.push(
|
||||||
|
createObjectProperty(
|
||||||
|
createSimpleExpression('ref_for', true),
|
||||||
|
createSimpleExpression('true')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
// in inline mode there is no setupState object, so we can't use string
|
// 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
|
// keys to set the ref. Instead, we need to transform it to pass the
|
||||||
// actual ref instead.
|
// actual ref instead.
|
||||||
if (!__BROWSER__ && context.inline && value?.content) {
|
if (
|
||||||
valueNode = createFunctionExpression(['_value', '_refs'])
|
!__BROWSER__ &&
|
||||||
valueNode.body = createBlockStatement(
|
value &&
|
||||||
processInlineRef(context, value.content)
|
context.inline &&
|
||||||
|
context.bindingMetadata[value.content]
|
||||||
|
) {
|
||||||
|
isStatic = false
|
||||||
|
properties.push(
|
||||||
|
createObjectProperty(
|
||||||
|
createSimpleExpression('ref_key', true),
|
||||||
|
createSimpleExpression(value.content, true, value.loc)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -504,7 +512,11 @@ export function buildProps(
|
|||||||
true,
|
true,
|
||||||
getInnerRange(loc, 0, name.length)
|
getInnerRange(loc, 0, name.length)
|
||||||
),
|
),
|
||||||
valueNode
|
createSimpleExpression(
|
||||||
|
value ? value.content : '',
|
||||||
|
isStatic,
|
||||||
|
value ? value.loc : loc
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@ -555,6 +567,15 @@ export function buildProps(
|
|||||||
shouldUseBlock = true
|
shouldUseBlock = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isVBind && isStaticArgOf(arg, 'ref') && context.scopes.vFor > 0) {
|
||||||
|
properties.push(
|
||||||
|
createObjectProperty(
|
||||||
|
createSimpleExpression('ref_for', true),
|
||||||
|
createSimpleExpression('true')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// special case for v-bind and v-on with no argument
|
// special case for v-bind and v-on with no argument
|
||||||
if (!arg && (isVBind || isVOn)) {
|
if (!arg && (isVBind || isVOn)) {
|
||||||
hasDynamicKeys = true
|
hasDynamicKeys = true
|
||||||
@ -654,25 +675,6 @@ export function buildProps(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
|
||||||
__COMPAT__ &&
|
|
||||||
prop.type === NodeTypes.ATTRIBUTE &&
|
|
||||||
prop.name === 'ref' &&
|
|
||||||
context.scopes.vFor > 0 &&
|
|
||||||
checkCompatEnabled(
|
|
||||||
CompilerDeprecationTypes.COMPILER_V_FOR_REF,
|
|
||||||
context,
|
|
||||||
prop.loc
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
properties.push(
|
|
||||||
createObjectProperty(
|
|
||||||
createSimpleExpression('refInFor', true),
|
|
||||||
createSimpleExpression('true', false)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let propsExpression: PropsExpression | undefined = undefined
|
let propsExpression: PropsExpression | undefined = undefined
|
||||||
@ -914,30 +916,3 @@ function stringifyDynamicPropNames(props: string[]): string {
|
|||||||
function isComponentTag(tag: string) {
|
function isComponentTag(tag: string) {
|
||||||
return tag === 'component' || tag === 'Component'
|
return tag === 'component' || tag === 'Component'
|
||||||
}
|
}
|
||||||
|
|
||||||
function processInlineRef(
|
|
||||||
context: TransformContext,
|
|
||||||
raw: string
|
|
||||||
): JSChildNode[] {
|
|
||||||
const body = [createSimpleExpression(`_refs['${raw}'] = _value`)]
|
|
||||||
const { bindingMetadata, helperString } = context
|
|
||||||
const type = bindingMetadata[raw]
|
|
||||||
if (type === BindingTypes.SETUP_REF) {
|
|
||||||
body.push(createSimpleExpression(`${raw}.value = _value`))
|
|
||||||
} else if (type === BindingTypes.SETUP_MAYBE_REF) {
|
|
||||||
body.push(
|
|
||||||
createSimpleExpression(
|
|
||||||
`${helperString(IS_REF)}(${raw}) && (${raw}.value = _value)`
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else if (type === BindingTypes.SETUP_LET) {
|
|
||||||
body.push(
|
|
||||||
createSimpleExpression(
|
|
||||||
`${helperString(
|
|
||||||
IS_REF
|
|
||||||
)}(${raw}) ? ${raw}.value = _value : ${raw} = _value`
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return body
|
|
||||||
}
|
|
||||||
|
@ -365,4 +365,81 @@ describe('api: template refs', () => {
|
|||||||
expect(elRef1.value).toBeNull()
|
expect(elRef1.value).toBeNull()
|
||||||
expect(elRef1.value).toBe(elRef2.value)
|
expect(elRef1.value).toBe(elRef2.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// compiled output of <script setup> inline mode
|
||||||
|
test('raw ref with ref_key', () => {
|
||||||
|
let refs: any
|
||||||
|
|
||||||
|
const el = ref()
|
||||||
|
|
||||||
|
const App = {
|
||||||
|
mounted() {
|
||||||
|
refs = (this as any).$refs
|
||||||
|
},
|
||||||
|
render() {
|
||||||
|
return h(
|
||||||
|
'div',
|
||||||
|
{
|
||||||
|
ref: el,
|
||||||
|
ref_key: 'el'
|
||||||
|
},
|
||||||
|
'hello'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const root = nodeOps.createElement('div')
|
||||||
|
render(h(App), root)
|
||||||
|
|
||||||
|
expect(serializeInner(el.value)).toBe('hello')
|
||||||
|
expect(serializeInner(refs.el)).toBe('hello')
|
||||||
|
})
|
||||||
|
|
||||||
|
// compiled output of v-for + template ref
|
||||||
|
test('ref in v-for', async () => {
|
||||||
|
const show = ref(true)
|
||||||
|
const list = reactive([1, 2, 3])
|
||||||
|
const listRefs = ref([])
|
||||||
|
const mapRefs = () => listRefs.value.map(n => serializeInner(n))
|
||||||
|
|
||||||
|
const App = {
|
||||||
|
render() {
|
||||||
|
return show.value
|
||||||
|
? h(
|
||||||
|
'ul',
|
||||||
|
list.map(i =>
|
||||||
|
h(
|
||||||
|
'li',
|
||||||
|
{
|
||||||
|
ref: listRefs,
|
||||||
|
ref_for: true
|
||||||
|
},
|
||||||
|
i
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const root = nodeOps.createElement('div')
|
||||||
|
render(h(App), root)
|
||||||
|
|
||||||
|
expect(mapRefs()).toMatchObject(['1', '2', '3'])
|
||||||
|
|
||||||
|
list.push(4)
|
||||||
|
await nextTick()
|
||||||
|
expect(mapRefs()).toMatchObject(['1', '2', '3', '4'])
|
||||||
|
|
||||||
|
list.shift()
|
||||||
|
await nextTick()
|
||||||
|
expect(mapRefs()).toMatchObject(['2', '3', '4'])
|
||||||
|
|
||||||
|
show.value = !show.value
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
expect(mapRefs()).toMatchObject([])
|
||||||
|
|
||||||
|
show.value = !show.value
|
||||||
|
await nextTick()
|
||||||
|
expect(mapRefs()).toMatchObject(['2', '3', '4'])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -12,7 +12,7 @@ import {
|
|||||||
} from '../src/vnode'
|
} from '../src/vnode'
|
||||||
import { Data } from '../src/component'
|
import { Data } from '../src/component'
|
||||||
import { ShapeFlags, PatchFlags } from '@vue/shared'
|
import { ShapeFlags, PatchFlags } from '@vue/shared'
|
||||||
import { h, reactive, isReactive, setBlockTracking } from '../src'
|
import { h, reactive, isReactive, setBlockTracking, ref } from '../src'
|
||||||
import { createApp, nodeOps, serializeInner } from '@vue/runtime-test'
|
import { createApp, nodeOps, serializeInner } from '@vue/runtime-test'
|
||||||
import { setCurrentRenderingInstance } from '../src/componentRenderContext'
|
import { setCurrentRenderingInstance } from '../src/componentRenderContext'
|
||||||
|
|
||||||
@ -236,20 +236,24 @@ describe('vnode', () => {
|
|||||||
|
|
||||||
setCurrentRenderingInstance(mockInstance1)
|
setCurrentRenderingInstance(mockInstance1)
|
||||||
const original = createVNode('div', { ref: 'foo' })
|
const original = createVNode('div', { ref: 'foo' })
|
||||||
expect(original.ref).toStrictEqual({ i: mockInstance1, r: 'foo' })
|
expect(original.ref).toMatchObject({
|
||||||
|
i: mockInstance1,
|
||||||
|
r: 'foo',
|
||||||
|
f: false
|
||||||
|
})
|
||||||
|
|
||||||
// clone and preserve original ref
|
// clone and preserve original ref
|
||||||
const cloned1 = cloneVNode(original)
|
const cloned1 = cloneVNode(original)
|
||||||
expect(cloned1.ref).toStrictEqual({ i: mockInstance1, r: 'foo' })
|
expect(cloned1.ref).toMatchObject({ i: mockInstance1, r: 'foo', f: false })
|
||||||
|
|
||||||
// cloning with new ref, but with same context instance
|
// cloning with new ref, but with same context instance
|
||||||
const cloned2 = cloneVNode(original, { ref: 'bar' })
|
const cloned2 = cloneVNode(original, { ref: 'bar' })
|
||||||
expect(cloned2.ref).toStrictEqual({ i: mockInstance1, r: 'bar' })
|
expect(cloned2.ref).toMatchObject({ i: mockInstance1, r: 'bar', f: false })
|
||||||
|
|
||||||
// cloning and adding ref to original that has no ref
|
// cloning and adding ref to original that has no ref
|
||||||
const original2 = createVNode('div')
|
const original2 = createVNode('div')
|
||||||
const cloned3 = cloneVNode(original2, { ref: 'bar' })
|
const cloned3 = cloneVNode(original2, { ref: 'bar' })
|
||||||
expect(cloned3.ref).toStrictEqual({ i: mockInstance1, r: 'bar' })
|
expect(cloned3.ref).toMatchObject({ i: mockInstance1, r: 'bar', f: false })
|
||||||
|
|
||||||
// cloning with different context instance
|
// cloning with different context instance
|
||||||
setCurrentRenderingInstance(mockInstance2)
|
setCurrentRenderingInstance(mockInstance2)
|
||||||
@ -257,16 +261,35 @@ describe('vnode', () => {
|
|||||||
// clone and preserve original ref
|
// clone and preserve original ref
|
||||||
const cloned4 = cloneVNode(original)
|
const cloned4 = cloneVNode(original)
|
||||||
// #1311 should preserve original context instance!
|
// #1311 should preserve original context instance!
|
||||||
expect(cloned4.ref).toStrictEqual({ i: mockInstance1, r: 'foo' })
|
expect(cloned4.ref).toMatchObject({ i: mockInstance1, r: 'foo', f: false })
|
||||||
|
|
||||||
// cloning with new ref, but with same context instance
|
// cloning with new ref, but with same context instance
|
||||||
const cloned5 = cloneVNode(original, { ref: 'bar' })
|
const cloned5 = cloneVNode(original, { ref: 'bar' })
|
||||||
// new ref should use current context instance and overwrite original
|
// new ref should use current context instance and overwrite original
|
||||||
expect(cloned5.ref).toStrictEqual({ i: mockInstance2, r: 'bar' })
|
expect(cloned5.ref).toMatchObject({ i: mockInstance2, r: 'bar', f: false })
|
||||||
|
|
||||||
// cloning and adding ref to original that has no ref
|
// cloning and adding ref to original that has no ref
|
||||||
const cloned6 = cloneVNode(original2, { ref: 'bar' })
|
const cloned6 = cloneVNode(original2, { ref: 'bar' })
|
||||||
expect(cloned6.ref).toStrictEqual({ i: mockInstance2, r: 'bar' })
|
expect(cloned6.ref).toMatchObject({ i: mockInstance2, r: 'bar', f: false })
|
||||||
|
|
||||||
|
const original3 = createVNode('div', { ref: 'foo', ref_for: true })
|
||||||
|
expect(original3.ref).toMatchObject({
|
||||||
|
i: mockInstance2,
|
||||||
|
r: 'foo',
|
||||||
|
f: true
|
||||||
|
})
|
||||||
|
const cloned7 = cloneVNode(original3, { ref: 'bar', ref_for: true })
|
||||||
|
expect(cloned7.ref).toMatchObject({ i: mockInstance2, r: 'bar', f: true })
|
||||||
|
|
||||||
|
const r = ref()
|
||||||
|
const original4 = createVNode('div', { ref: r, ref_key: 'foo' })
|
||||||
|
expect(original4.ref).toMatchObject({
|
||||||
|
i: mockInstance2,
|
||||||
|
r,
|
||||||
|
k: 'foo'
|
||||||
|
})
|
||||||
|
const cloned8 = cloneVNode(original4)
|
||||||
|
expect(cloned8.ref).toMatchObject({ i: mockInstance2, r, k: 'foo' })
|
||||||
|
|
||||||
setCurrentRenderingInstance(null)
|
setCurrentRenderingInstance(null)
|
||||||
})
|
})
|
||||||
@ -277,14 +300,14 @@ describe('vnode', () => {
|
|||||||
|
|
||||||
setCurrentRenderingInstance(mockInstance1)
|
setCurrentRenderingInstance(mockInstance1)
|
||||||
const original = createVNode('div', { ref: 'foo' })
|
const original = createVNode('div', { ref: 'foo' })
|
||||||
expect(original.ref).toStrictEqual({ i: mockInstance1, r: 'foo' })
|
expect(original.ref).toMatchObject({ i: mockInstance1, r: 'foo', f: false })
|
||||||
|
|
||||||
// clone and preserve original ref
|
// clone and preserve original ref
|
||||||
setCurrentRenderingInstance(mockInstance2)
|
setCurrentRenderingInstance(mockInstance2)
|
||||||
const cloned1 = cloneVNode(original, { ref: 'bar' }, true)
|
const cloned1 = cloneVNode(original, { ref: 'bar' }, true)
|
||||||
expect(cloned1.ref).toStrictEqual([
|
expect(cloned1.ref).toMatchObject([
|
||||||
{ i: mockInstance1, r: 'foo' },
|
{ i: mockInstance1, r: 'foo', f: false },
|
||||||
{ i: mockInstance2, r: 'bar' }
|
{ i: mockInstance2, r: 'bar', f: false }
|
||||||
])
|
])
|
||||||
|
|
||||||
setCurrentRenderingInstance(null)
|
setCurrentRenderingInstance(null)
|
||||||
|
@ -46,7 +46,6 @@ export const enum DeprecationTypes {
|
|||||||
WATCH_ARRAY = 'WATCH_ARRAY',
|
WATCH_ARRAY = 'WATCH_ARRAY',
|
||||||
PROPS_DEFAULT_THIS = 'PROPS_DEFAULT_THIS',
|
PROPS_DEFAULT_THIS = 'PROPS_DEFAULT_THIS',
|
||||||
|
|
||||||
V_FOR_REF = 'V_FOR_REF',
|
|
||||||
V_ON_KEYCODE_MODIFIER = 'V_ON_KEYCODE_MODIFIER',
|
V_ON_KEYCODE_MODIFIER = 'V_ON_KEYCODE_MODIFIER',
|
||||||
CUSTOM_DIR = 'CUSTOM_DIR',
|
CUSTOM_DIR = 'CUSTOM_DIR',
|
||||||
|
|
||||||
@ -298,13 +297,6 @@ export const deprecationData: Record<DeprecationTypes, DeprecationData> = {
|
|||||||
link: `https://v3.vuejs.org/guide/migration/custom-directives.html`
|
link: `https://v3.vuejs.org/guide/migration/custom-directives.html`
|
||||||
},
|
},
|
||||||
|
|
||||||
[DeprecationTypes.V_FOR_REF]: {
|
|
||||||
message:
|
|
||||||
`Ref usage on v-for no longer creates array ref values in Vue 3. ` +
|
|
||||||
`Consider using function refs or refactor to avoid ref usage altogether.`,
|
|
||||||
link: `https://v3.vuejs.org/guide/migration/array-refs.html`
|
|
||||||
},
|
|
||||||
|
|
||||||
[DeprecationTypes.V_ON_KEYCODE_MODIFIER]: {
|
[DeprecationTypes.V_ON_KEYCODE_MODIFIER]: {
|
||||||
message:
|
message:
|
||||||
`Using keyCode as v-on modifier is no longer supported. ` +
|
`Using keyCode as v-on modifier is no longer supported. ` +
|
||||||
|
@ -1,45 +0,0 @@
|
|||||||
import { isArray, remove } from '@vue/shared'
|
|
||||||
import { ComponentInternalInstance, Data } from '../component'
|
|
||||||
import { VNode } from '../vnode'
|
|
||||||
import { DeprecationTypes, warnDeprecation } from './compatConfig'
|
|
||||||
|
|
||||||
export function convertLegacyRefInFor(vnode: VNode) {
|
|
||||||
// refInFor
|
|
||||||
if (vnode.props && vnode.props.refInFor) {
|
|
||||||
delete vnode.props.refInFor
|
|
||||||
if (vnode.ref) {
|
|
||||||
if (isArray(vnode.ref)) {
|
|
||||||
vnode.ref.forEach(r => (r.f = true))
|
|
||||||
} else {
|
|
||||||
vnode.ref.f = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function registerLegacyRef(
|
|
||||||
refs: Data,
|
|
||||||
key: string,
|
|
||||||
value: any,
|
|
||||||
owner: ComponentInternalInstance,
|
|
||||||
isInFor: boolean | undefined,
|
|
||||||
isUnmount: boolean
|
|
||||||
) {
|
|
||||||
const existing = refs[key]
|
|
||||||
if (isUnmount) {
|
|
||||||
if (isArray(existing)) {
|
|
||||||
remove(existing, value)
|
|
||||||
} else {
|
|
||||||
refs[key] = null
|
|
||||||
}
|
|
||||||
} else if (isInFor) {
|
|
||||||
__DEV__ && warnDeprecation(DeprecationTypes.V_FOR_REF, owner)
|
|
||||||
if (!isArray(existing)) {
|
|
||||||
refs[key] = [value]
|
|
||||||
} else if (!existing.includes(value)) {
|
|
||||||
existing.push(value)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
refs[key] = value
|
|
||||||
}
|
|
||||||
}
|
|
@ -40,7 +40,8 @@ import {
|
|||||||
hasOwn,
|
hasOwn,
|
||||||
invokeArrayFns,
|
invokeArrayFns,
|
||||||
isArray,
|
isArray,
|
||||||
getGlobalThis
|
getGlobalThis,
|
||||||
|
remove
|
||||||
} from '@vue/shared'
|
} from '@vue/shared'
|
||||||
import {
|
import {
|
||||||
queueJob,
|
queueJob,
|
||||||
@ -86,7 +87,6 @@ import { initFeatureFlags } from './featureFlags'
|
|||||||
import { isAsyncWrapper } from './apiAsyncComponent'
|
import { isAsyncWrapper } from './apiAsyncComponent'
|
||||||
import { isCompatEnabled } from './compat/compatConfig'
|
import { isCompatEnabled } from './compat/compatConfig'
|
||||||
import { DeprecationTypes } from './compat/compatConfig'
|
import { DeprecationTypes } from './compat/compatConfig'
|
||||||
import { registerLegacyRef } from './compat/ref'
|
|
||||||
|
|
||||||
export interface Renderer<HostElement = RendererElement> {
|
export interface Renderer<HostElement = RendererElement> {
|
||||||
render: RootRenderFunction<HostElement>
|
render: RootRenderFunction<HostElement>
|
||||||
@ -2407,40 +2407,53 @@ export function setRef(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isString(ref)) {
|
if (isFunction(ref)) {
|
||||||
const doSet = () => {
|
|
||||||
if (__COMPAT__ && isCompatEnabled(DeprecationTypes.V_FOR_REF, owner)) {
|
|
||||||
registerLegacyRef(refs, ref, refValue, owner, rawRef.f, isUnmount)
|
|
||||||
} else {
|
|
||||||
refs[ref] = value
|
|
||||||
}
|
|
||||||
if (hasOwn(setupState, ref)) {
|
|
||||||
setupState[ref] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// #1789: for non-null values, set them after render
|
|
||||||
// null values means this is unmount and it should not overwrite another
|
|
||||||
// ref with the same key
|
|
||||||
if (value) {
|
|
||||||
;(doSet as SchedulerJob).id = -1
|
|
||||||
queuePostRenderEffect(doSet, parentSuspense)
|
|
||||||
} else {
|
|
||||||
doSet()
|
|
||||||
}
|
|
||||||
} else if (isRef(ref)) {
|
|
||||||
const doSet = () => {
|
|
||||||
ref.value = value
|
|
||||||
}
|
|
||||||
if (value) {
|
|
||||||
;(doSet as SchedulerJob).id = -1
|
|
||||||
queuePostRenderEffect(doSet, parentSuspense)
|
|
||||||
} else {
|
|
||||||
doSet()
|
|
||||||
}
|
|
||||||
} else if (isFunction(ref)) {
|
|
||||||
callWithErrorHandling(ref, owner, ErrorCodes.FUNCTION_REF, [value, refs])
|
callWithErrorHandling(ref, owner, ErrorCodes.FUNCTION_REF, [value, refs])
|
||||||
} else if (__DEV__) {
|
} else {
|
||||||
warn('Invalid template ref type:', value, `(${typeof value})`)
|
const _isString = isString(ref)
|
||||||
|
const _isRef = isRef(ref)
|
||||||
|
if (_isString || _isRef) {
|
||||||
|
const doSet = () => {
|
||||||
|
if (rawRef.f) {
|
||||||
|
const existing = _isString ? refs[ref] : ref.value
|
||||||
|
if (isUnmount) {
|
||||||
|
isArray(existing) && remove(existing, refValue)
|
||||||
|
} else {
|
||||||
|
if (!isArray(existing)) {
|
||||||
|
if (_isString) {
|
||||||
|
refs[ref] = [refValue]
|
||||||
|
} else {
|
||||||
|
ref.value = [refValue]
|
||||||
|
if (rawRef.k) refs[rawRef.k] = ref.value
|
||||||
|
}
|
||||||
|
} else if (!existing.includes(refValue)) {
|
||||||
|
existing.push(refValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (_isString) {
|
||||||
|
refs[ref] = value
|
||||||
|
if (hasOwn(setupState, ref)) {
|
||||||
|
setupState[ref] = value
|
||||||
|
}
|
||||||
|
} else if (isRef(ref)) {
|
||||||
|
ref.value = value
|
||||||
|
if (rawRef.k) refs[rawRef.k] = value
|
||||||
|
} else if (__DEV__) {
|
||||||
|
warn('Invalid template ref type:', ref, `(${typeof ref})`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (value) {
|
||||||
|
// #1789: for non-null values, set them after render
|
||||||
|
// null values means this is unmount and it should not overwrite another
|
||||||
|
// ref with the same key
|
||||||
|
;(doSet as SchedulerJob).id = -1
|
||||||
|
queuePostRenderEffect(doSet, parentSuspense)
|
||||||
|
} else {
|
||||||
|
doSet()
|
||||||
|
}
|
||||||
|
} else if (__DEV__) {
|
||||||
|
warn('Invalid template ref type:', ref, `(${typeof ref})`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,7 +42,6 @@ import { hmrDirtyComponents } from './hmr'
|
|||||||
import { convertLegacyComponent } from './compat/component'
|
import { convertLegacyComponent } from './compat/component'
|
||||||
import { convertLegacyVModelProps } from './compat/componentVModel'
|
import { convertLegacyVModelProps } from './compat/componentVModel'
|
||||||
import { defineLegacyVNodeProperties } from './compat/renderFn'
|
import { defineLegacyVNodeProperties } from './compat/renderFn'
|
||||||
import { convertLegacyRefInFor } from './compat/ref'
|
|
||||||
|
|
||||||
export const Fragment = Symbol(__DEV__ ? 'Fragment' : undefined) as any as {
|
export const Fragment = Symbol(__DEV__ ? 'Fragment' : undefined) as any as {
|
||||||
__isFragment: true
|
__isFragment: true
|
||||||
@ -73,7 +72,8 @@ export type VNodeRef =
|
|||||||
export type VNodeNormalizedRefAtom = {
|
export type VNodeNormalizedRefAtom = {
|
||||||
i: ComponentInternalInstance
|
i: ComponentInternalInstance
|
||||||
r: VNodeRef
|
r: VNodeRef
|
||||||
f?: boolean // v2 compat only, refInFor marker
|
k?: string // setup ref key
|
||||||
|
f?: boolean // refInFor marker
|
||||||
}
|
}
|
||||||
|
|
||||||
export type VNodeNormalizedRef =
|
export type VNodeNormalizedRef =
|
||||||
@ -92,6 +92,8 @@ export type VNodeHook =
|
|||||||
export type VNodeProps = {
|
export type VNodeProps = {
|
||||||
key?: string | number | symbol
|
key?: string | number | symbol
|
||||||
ref?: VNodeRef
|
ref?: VNodeRef
|
||||||
|
ref_for?: boolean
|
||||||
|
ref_key?: string
|
||||||
|
|
||||||
// vnode hooks
|
// vnode hooks
|
||||||
onVnodeBeforeMount?: VNodeMountHook | VNodeMountHook[]
|
onVnodeBeforeMount?: VNodeMountHook | VNodeMountHook[]
|
||||||
@ -380,11 +382,15 @@ export const InternalObjectKey = `__vInternal`
|
|||||||
const normalizeKey = ({ key }: VNodeProps): VNode['key'] =>
|
const normalizeKey = ({ key }: VNodeProps): VNode['key'] =>
|
||||||
key != null ? key : null
|
key != null ? key : null
|
||||||
|
|
||||||
const normalizeRef = ({ ref }: VNodeProps): VNodeNormalizedRefAtom | null => {
|
const normalizeRef = ({
|
||||||
|
ref,
|
||||||
|
ref_key,
|
||||||
|
ref_for
|
||||||
|
}: VNodeProps): VNodeNormalizedRefAtom | null => {
|
||||||
return (
|
return (
|
||||||
ref != null
|
ref != null
|
||||||
? isString(ref) || isRef(ref) || isFunction(ref)
|
? isString(ref) || isRef(ref) || isFunction(ref)
|
||||||
? { i: currentRenderingInstance, r: ref }
|
? { i: currentRenderingInstance, r: ref, k: ref_key, f: !!ref_for }
|
||||||
: ref
|
: ref
|
||||||
: null
|
: null
|
||||||
) as any
|
) as any
|
||||||
@ -468,7 +474,6 @@ function createBaseVNode(
|
|||||||
|
|
||||||
if (__COMPAT__) {
|
if (__COMPAT__) {
|
||||||
convertLegacyVModelProps(vnode)
|
convertLegacyVModelProps(vnode)
|
||||||
convertLegacyRefInFor(vnode)
|
|
||||||
defineLegacyVNodeProperties(vnode)
|
defineLegacyVNodeProperties(vnode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,7 +83,7 @@ export const isIntegerKey = (key: unknown) =>
|
|||||||
|
|
||||||
export const isReservedProp = /*#__PURE__*/ makeMap(
|
export const isReservedProp = /*#__PURE__*/ makeMap(
|
||||||
// the leading comma is intentional so empty string "" is also included
|
// the leading comma is intentional so empty string "" is also included
|
||||||
',key,ref,' +
|
',key,ref,ref_for,ref_key,' +
|
||||||
'onVnodeBeforeMount,onVnodeMounted,' +
|
'onVnodeBeforeMount,onVnodeMounted,' +
|
||||||
'onVnodeBeforeUpdate,onVnodeUpdated,' +
|
'onVnodeBeforeUpdate,onVnodeUpdated,' +
|
||||||
'onVnodeBeforeUnmount,onVnodeUnmounted'
|
'onVnodeBeforeUnmount,onVnodeUnmounted'
|
||||||
|
@ -1,57 +0,0 @@
|
|||||||
import Vue from '@vue/compat'
|
|
||||||
import { nextTick } from '../../runtime-core/src/scheduler'
|
|
||||||
import {
|
|
||||||
DeprecationTypes,
|
|
||||||
deprecationData,
|
|
||||||
toggleDeprecationWarning
|
|
||||||
} from '../../runtime-core/src/compat/compatConfig'
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
toggleDeprecationWarning(true)
|
|
||||||
Vue.configureCompat({
|
|
||||||
MODE: 2,
|
|
||||||
GLOBAL_MOUNT: 'suppress-warning'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
toggleDeprecationWarning(false)
|
|
||||||
Vue.configureCompat({ MODE: 3 })
|
|
||||||
})
|
|
||||||
|
|
||||||
test('V_FOR_REF', async () => {
|
|
||||||
const vm = new Vue({
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
ok: true,
|
|
||||||
list: [1, 2, 3]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
template: `
|
|
||||||
<template v-if="ok">
|
|
||||||
<li v-for="i in list" ref="list">{{ i }}</li>
|
|
||||||
</template>
|
|
||||||
`
|
|
||||||
}).$mount() as any
|
|
||||||
|
|
||||||
const mapRefs = () => vm.$refs.list.map((el: HTMLElement) => el.textContent)
|
|
||||||
expect(mapRefs()).toMatchObject(['1', '2', '3'])
|
|
||||||
|
|
||||||
expect(deprecationData[DeprecationTypes.V_FOR_REF].message).toHaveBeenWarned()
|
|
||||||
|
|
||||||
vm.list.push(4)
|
|
||||||
await nextTick()
|
|
||||||
expect(mapRefs()).toMatchObject(['1', '2', '3', '4'])
|
|
||||||
|
|
||||||
vm.list.shift()
|
|
||||||
await nextTick()
|
|
||||||
expect(mapRefs()).toMatchObject(['2', '3', '4'])
|
|
||||||
|
|
||||||
vm.ok = !vm.ok
|
|
||||||
await nextTick()
|
|
||||||
expect(mapRefs()).toMatchObject([])
|
|
||||||
|
|
||||||
vm.ok = !vm.ok
|
|
||||||
await nextTick()
|
|
||||||
expect(mapRefs()).toMatchObject(['2', '3', '4'])
|
|
||||||
})
|
|
Loading…
Reference in New Issue
Block a user