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"/>`, {
inline: true,
bindingMetadata: {
{
type: NodeTypes.JS_PROPERTY,
key: {
- type: NodeTypes.SIMPLE_EXPRESSION,
- content: 'ref',
+ content: 'ref_key',
isStatic: true
},
value: {
- type: NodeTypes.JS_FUNCTION_EXPRESSION,
- params: ['_value', '_refs'],
- body: {
- type: NodeTypes.JS_BLOCK_STATEMENT,
- body: [
- {
- content: `_refs['input'] = _value`
- },
- {
- content: 'input.value = _value'
- }
- ]
- }
- }
- }
- ]
- })
- })
-
- test('the binding not exists (inline ref input)', () => {
- const { node } = parseWithElementTransform(`<input ref="input"/>`, {
- inline: true
- })
- expect(node.props).toMatchObject({
- type: NodeTypes.JS_OBJECT_EXPRESSION,
- properties: [
- {
- type: NodeTypes.JS_PROPERTY,
- key: {
- type: NodeTypes.SIMPLE_EXPRESSION,
- content: 'ref',
+ content: 'input',
isStatic: true
- },
- value: {
- type: NodeTypes.JS_FUNCTION_EXPRESSION,
- 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
},
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)'
- }
- ]
- }
+ content: 'input',
+ isStatic: false
}
}
]
})
})
- test('the binding not exists (inline let ref input)', () => {
+ test('script setup inline mode template ref (binding does not exist)', () => {
const { node } = parseWithElementTransform(`<input ref="input"/>`, {
- inline: true,
- bindingMetadata: {
- input: BindingTypes.SETUP_LET
- }
+ inline: true
})
expect(node.props).toMatchObject({
type: NodeTypes.JS_OBJECT_EXPRESSION,
{
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'
- }
- ]
- }
+ content: 'input',
+ isStatic: true
}
}
]
COMPILER_V_BIND_OBJECT_ORDER = 'COMPILER_V_BIND_OBJECT_ORDER',
COMPILER_V_ON_NATIVE = 'COMPILER_V_ON_NATIVE',
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_INLINE_TEMPLATE = 'COMPILER_INLINE_TEMPLATE',
COMPILER_FILTERS = 'COMPILER_FILTER'
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]: {
message:
`<template> with no special directives will render as a native template ` +
TemplateTextChildNode,
DirectiveArguments,
createVNodeCall,
- ConstantTypes,
- JSChildNode,
- createFunctionExpression,
- createBlockStatement
+ ConstantTypes
} from '../ast'
import {
PatchFlags,
KEEP_ALIVE,
SUSPENSE,
UNREF,
- GUARD_REACTIVE_PROPS,
- IS_REF
+ GUARD_REACTIVE_PROPS
} from '../runtimeHelpers'
import {
getInnerRange,
const prop = props[i]
if (prop.type === NodeTypes.ATTRIBUTE) {
const { loc, name, value } = prop
- let valueNode = createSimpleExpression(
- value ? value.content : '',
- true,
- value ? value.loc : loc
- ) as JSChildNode
+ let isStatic = true
if (name === 'ref') {
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
// keys to set the ref. Instead, we need to transform it to pass the
// actual ref instead.
- if (!__BROWSER__ && context.inline && value?.content) {
- valueNode = createFunctionExpression(['_value', '_refs'])
- valueNode.body = createBlockStatement(
- processInlineRef(context, value.content)
+ if (
+ !__BROWSER__ &&
+ value &&
+ context.inline &&
+ context.bindingMetadata[value.content]
+ ) {
+ isStatic = false
+ properties.push(
+ createObjectProperty(
+ createSimpleExpression('ref_key', true),
+ createSimpleExpression(value.content, true, value.loc)
+ )
)
}
}
true,
getInnerRange(loc, 0, name.length)
),
- valueNode
+ createSimpleExpression(
+ value ? value.content : '',
+ isStatic,
+ value ? value.loc : loc
+ )
)
)
} else {
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
if (!arg && (isVBind || isVOn)) {
hasDynamicKeys = true
}
}
}
-
- 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
function isComponentTag(tag: string) {
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
-}
expect(elRef1.value).toBeNull()
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'])
+ })
})
} from '../src/vnode'
import { Data } from '../src/component'
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 { setCurrentRenderingInstance } from '../src/componentRenderContext'
setCurrentRenderingInstance(mockInstance1)
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
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
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
const original2 = createVNode('div')
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
setCurrentRenderingInstance(mockInstance2)
// clone and preserve original ref
const cloned4 = cloneVNode(original)
// #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
const cloned5 = cloneVNode(original, { ref: 'bar' })
// 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
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(mockInstance1)
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
setCurrentRenderingInstance(mockInstance2)
const cloned1 = cloneVNode(original, { ref: 'bar' }, true)
- expect(cloned1.ref).toStrictEqual([
- { i: mockInstance1, r: 'foo' },
- { i: mockInstance2, r: 'bar' }
+ expect(cloned1.ref).toMatchObject([
+ { i: mockInstance1, r: 'foo', f: false },
+ { i: mockInstance2, r: 'bar', f: false }
])
setCurrentRenderingInstance(null)
WATCH_ARRAY = 'WATCH_ARRAY',
PROPS_DEFAULT_THIS = 'PROPS_DEFAULT_THIS',
- V_FOR_REF = 'V_FOR_REF',
V_ON_KEYCODE_MODIFIER = 'V_ON_KEYCODE_MODIFIER',
CUSTOM_DIR = 'CUSTOM_DIR',
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]: {
message:
`Using keyCode as v-on modifier is no longer supported. ` +
+++ /dev/null
-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
- }
-}
hasOwn,
invokeArrayFns,
isArray,
- getGlobalThis
+ getGlobalThis,
+ remove
} from '@vue/shared'
import {
queueJob,
import { isAsyncWrapper } from './apiAsyncComponent'
import { isCompatEnabled } from './compat/compatConfig'
import { DeprecationTypes } from './compat/compatConfig'
-import { registerLegacyRef } from './compat/ref'
export interface Renderer<HostElement = RendererElement> {
render: RootRenderFunction<HostElement>
}
}
- if (isString(ref)) {
- const doSet = () => {
- if (__COMPAT__ && isCompatEnabled(DeprecationTypes.V_FOR_REF, owner)) {
- registerLegacyRef(refs, ref, refValue, owner, rawRef.f, isUnmount)
- } else {
- refs[ref] = value
+ if (isFunction(ref)) {
+ callWithErrorHandling(ref, owner, ErrorCodes.FUNCTION_REF, [value, refs])
+ } else {
+ 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 (hasOwn(setupState, ref)) {
- setupState[ref] = value
+ 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})`)
}
- // #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])
- } else if (__DEV__) {
- warn('Invalid template ref type:', value, `(${typeof value})`)
}
}
import { convertLegacyComponent } from './compat/component'
import { convertLegacyVModelProps } from './compat/componentVModel'
import { defineLegacyVNodeProperties } from './compat/renderFn'
-import { convertLegacyRefInFor } from './compat/ref'
export const Fragment = Symbol(__DEV__ ? 'Fragment' : undefined) as any as {
__isFragment: true
export type VNodeNormalizedRefAtom = {
i: ComponentInternalInstance
r: VNodeRef
- f?: boolean // v2 compat only, refInFor marker
+ k?: string // setup ref key
+ f?: boolean // refInFor marker
}
export type VNodeNormalizedRef =
export type VNodeProps = {
key?: string | number | symbol
ref?: VNodeRef
+ ref_for?: boolean
+ ref_key?: string
// vnode hooks
onVnodeBeforeMount?: VNodeMountHook | VNodeMountHook[]
const normalizeKey = ({ key }: VNodeProps): VNode['key'] =>
key != null ? key : null
-const normalizeRef = ({ ref }: VNodeProps): VNodeNormalizedRefAtom | null => {
+const normalizeRef = ({
+ ref,
+ ref_key,
+ ref_for
+}: VNodeProps): VNodeNormalizedRefAtom | null => {
return (
ref != null
? isString(ref) || isRef(ref) || isFunction(ref)
- ? { i: currentRenderingInstance, r: ref }
+ ? { i: currentRenderingInstance, r: ref, k: ref_key, f: !!ref_for }
: ref
: null
) as any
if (__COMPAT__) {
convertLegacyVModelProps(vnode)
- convertLegacyRefInFor(vnode)
defineLegacyVNodeProperties(vnode)
}
export const isReservedProp = /*#__PURE__*/ makeMap(
// the leading comma is intentional so empty string "" is also included
- ',key,ref,' +
+ ',key,ref,ref_for,ref_key,' +
'onVnodeBeforeMount,onVnodeMounted,' +
'onVnodeBeforeUpdate,onVnodeUpdated,' +
'onVnodeBeforeUnmount,onVnodeUnmounted'
+++ /dev/null
-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'])
-})