refactor(template-ref): improve template ref handling
close #836, close #839
This commit is contained in:
parent
8a58dce603
commit
9ad65b1653
@ -1,49 +0,0 @@
|
|||||||
import { baseParse as parse } from '../../src/parse'
|
|
||||||
import { transform } from '../../src/transform'
|
|
||||||
import { transformRef } from '../../src/transforms/transformRef'
|
|
||||||
import { ElementNode, NodeTypes } from '../../src/ast'
|
|
||||||
|
|
||||||
function transformWithRef(template: string) {
|
|
||||||
const ast = parse(template)
|
|
||||||
transform(ast, {
|
|
||||||
nodeTransforms: [transformRef]
|
|
||||||
})
|
|
||||||
return ast.children[0] as ElementNode
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('compiler: transform ref', () => {
|
|
||||||
const getExpected = (key: any) => ({
|
|
||||||
type: NodeTypes.DIRECTIVE,
|
|
||||||
name: 'bind',
|
|
||||||
arg: {
|
|
||||||
type: NodeTypes.SIMPLE_EXPRESSION,
|
|
||||||
content: `ref`
|
|
||||||
},
|
|
||||||
exp: {
|
|
||||||
type: NodeTypes.COMPOUND_EXPRESSION,
|
|
||||||
children: [`[_ctx, `, key, `]`]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test('static', () => {
|
|
||||||
const node = transformWithRef(`<div ref="test"/>`)
|
|
||||||
expect(node.props[0]).toMatchObject(
|
|
||||||
getExpected({
|
|
||||||
type: NodeTypes.SIMPLE_EXPRESSION,
|
|
||||||
content: `test`,
|
|
||||||
isStatic: true
|
|
||||||
})
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('dynamic', () => {
|
|
||||||
const node = transformWithRef(`<div :ref="test"/>`)
|
|
||||||
expect(node.props[0]).toMatchObject(
|
|
||||||
getExpected({
|
|
||||||
type: NodeTypes.SIMPLE_EXPRESSION,
|
|
||||||
content: `test`,
|
|
||||||
isStatic: false
|
|
||||||
})
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
@ -4,7 +4,6 @@ import { transform, NodeTransform, DirectiveTransform } from './transform'
|
|||||||
import { generate, CodegenResult } from './codegen'
|
import { generate, CodegenResult } from './codegen'
|
||||||
import { RootNode } from './ast'
|
import { RootNode } from './ast'
|
||||||
import { isString } from '@vue/shared'
|
import { isString } from '@vue/shared'
|
||||||
import { transformRef } from './transforms/transformRef'
|
|
||||||
import { transformIf } from './transforms/vIf'
|
import { transformIf } from './transforms/vIf'
|
||||||
import { transformFor } from './transforms/vFor'
|
import { transformFor } from './transforms/vFor'
|
||||||
import { transformExpression } from './transforms/transformExpression'
|
import { transformExpression } from './transforms/transformExpression'
|
||||||
@ -28,7 +27,6 @@ export function getBaseTransformPreset(
|
|||||||
): TransformPreset {
|
): TransformPreset {
|
||||||
return [
|
return [
|
||||||
[
|
[
|
||||||
transformRef,
|
|
||||||
transformOnce,
|
transformOnce,
|
||||||
transformIf,
|
transformIf,
|
||||||
transformFor,
|
transformFor,
|
||||||
|
@ -1,40 +0,0 @@
|
|||||||
import { NodeTransform } from '../transform'
|
|
||||||
import {
|
|
||||||
NodeTypes,
|
|
||||||
ElementTypes,
|
|
||||||
createSimpleExpression,
|
|
||||||
createCompoundExpression
|
|
||||||
} from '../ast'
|
|
||||||
import { findProp } from '../utils'
|
|
||||||
|
|
||||||
// Convert ref="foo" to `:ref="[_ctx, 'foo']"` so that the ref contains the
|
|
||||||
// correct owner instance even inside slots.
|
|
||||||
export const transformRef: NodeTransform = node => {
|
|
||||||
if (
|
|
||||||
!(
|
|
||||||
node.type === NodeTypes.ELEMENT &&
|
|
||||||
(node.tagType === ElementTypes.ELEMENT ||
|
|
||||||
node.tagType === ElementTypes.COMPONENT)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const ref = findProp(node, 'ref')
|
|
||||||
if (!ref) return
|
|
||||||
const refKey =
|
|
||||||
ref.type === NodeTypes.ATTRIBUTE
|
|
||||||
? ref.value
|
|
||||||
? createSimpleExpression(ref.value.content, true, ref.value.loc)
|
|
||||||
: null
|
|
||||||
: ref.exp
|
|
||||||
if (refKey) {
|
|
||||||
node.props[node.props.indexOf(ref)] = {
|
|
||||||
type: NodeTypes.DIRECTIVE,
|
|
||||||
name: `bind`,
|
|
||||||
arg: createSimpleExpression(`ref`, true, ref.loc),
|
|
||||||
exp: createCompoundExpression([`[_ctx, `, refKey, `]`]),
|
|
||||||
modifiers: [],
|
|
||||||
loc: ref.loc
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -22,9 +22,7 @@ describe('api: template refs', () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
render() {
|
render() {
|
||||||
// Note: string refs are compiled into [ctx, key] tuples by the compiler
|
return h('div', { ref: 'refKey' })
|
||||||
// to ensure correct context.
|
|
||||||
return h('div', { ref: [this, 'refKey'] as any })
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
render(h(Comp), root)
|
render(h(Comp), root)
|
||||||
@ -45,7 +43,7 @@ describe('api: template refs', () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
render() {
|
render() {
|
||||||
return h('div', { ref: [this, refKey.value] as any })
|
return h('div', { ref: refKey.value })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
render(h(Comp), root)
|
render(h(Comp), root)
|
||||||
@ -70,7 +68,7 @@ describe('api: template refs', () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
render() {
|
render() {
|
||||||
return toggle.value ? h('div', { ref: [this, 'refKey'] as any }) : null
|
return toggle.value ? h('div', { ref: 'refKey' }) : null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
render(h(Comp), root)
|
render(h(Comp), root)
|
||||||
@ -178,4 +176,28 @@ describe('api: template refs', () => {
|
|||||||
await nextTick()
|
await nextTick()
|
||||||
expect(el.value).toBe(null)
|
expect(el.value).toBe(null)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('string ref inside slots', async () => {
|
||||||
|
const root = nodeOps.createElement('div')
|
||||||
|
const spy = jest.fn()
|
||||||
|
const Child = {
|
||||||
|
render(this: any) {
|
||||||
|
return this.$slots.default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const Comp = {
|
||||||
|
render() {
|
||||||
|
return h(Child, () => {
|
||||||
|
return h('div', { ref: 'foo' })
|
||||||
|
})
|
||||||
|
},
|
||||||
|
mounted(this: any) {
|
||||||
|
spy(this.$refs.foo.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
render(h(Comp), root)
|
||||||
|
|
||||||
|
expect(spy).toHaveBeenCalledWith('div')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -8,7 +8,8 @@ import {
|
|||||||
VNodeArrayChildren,
|
VNodeArrayChildren,
|
||||||
createVNode,
|
createVNode,
|
||||||
isSameVNodeType,
|
isSameVNodeType,
|
||||||
Static
|
Static,
|
||||||
|
VNodeNormalizedRef
|
||||||
} from './vnode'
|
} from './vnode'
|
||||||
import {
|
import {
|
||||||
ComponentInternalInstance,
|
ComponentInternalInstance,
|
||||||
@ -30,8 +31,7 @@ import {
|
|||||||
isFunction,
|
isFunction,
|
||||||
PatchFlags,
|
PatchFlags,
|
||||||
ShapeFlags,
|
ShapeFlags,
|
||||||
NOOP,
|
NOOP
|
||||||
isArray
|
|
||||||
} from '@vue/shared'
|
} from '@vue/shared'
|
||||||
import {
|
import {
|
||||||
queueJob,
|
queueJob,
|
||||||
@ -44,7 +44,6 @@ import {
|
|||||||
stop,
|
stop,
|
||||||
ReactiveEffectOptions,
|
ReactiveEffectOptions,
|
||||||
isRef,
|
isRef,
|
||||||
Ref,
|
|
||||||
toRaw,
|
toRaw,
|
||||||
DebuggerEvent
|
DebuggerEvent
|
||||||
} from '@vue/reactivity'
|
} from '@vue/reactivity'
|
||||||
@ -1789,21 +1788,22 @@ function baseCreateRenderer<
|
|||||||
}
|
}
|
||||||
|
|
||||||
const setRef = (
|
const setRef = (
|
||||||
ref: string | Function | Ref | [ComponentPublicInstance, string],
|
rawRef: VNodeNormalizedRef,
|
||||||
oldRef: string | Function | Ref | [ComponentPublicInstance, string] | null,
|
oldRawRef: VNodeNormalizedRef | null,
|
||||||
parent: ComponentInternalInstance,
|
parent: ComponentInternalInstance,
|
||||||
value: HostNode | ComponentPublicInstance | null
|
value: HostNode | ComponentPublicInstance | null
|
||||||
) => {
|
) => {
|
||||||
if (isArray(ref)) {
|
const [owner, ref] = rawRef
|
||||||
// template string refs are compiled into tuples like [ctx, key] to
|
if (__DEV__ && !owner) {
|
||||||
// ensure refs inside slots are set on the correct owner instance.
|
warn(
|
||||||
const [{ $: owner }, key] = ref
|
`Missing ref owner context. ref cannot be used on hoisted vnodes. ` +
|
||||||
setRef(key, oldRef && (oldRef as any[])[1], owner, value)
|
`A vnode with ref must be created inside the render function.`
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
const oldRef = oldRawRef && oldRawRef[1]
|
||||||
const refs = parent.refs === EMPTY_OBJ ? (parent.refs = {}) : parent.refs
|
const refs = owner.refs === EMPTY_OBJ ? (owner.refs = {}) : owner.refs
|
||||||
const renderContext = toRaw(parent.renderContext)
|
const renderContext = toRaw(owner.renderContext)
|
||||||
|
|
||||||
// unset old ref
|
// unset old ref
|
||||||
if (oldRef !== null && oldRef !== ref) {
|
if (oldRef !== null && oldRef !== ref) {
|
||||||
@ -1827,7 +1827,7 @@ function baseCreateRenderer<
|
|||||||
} else if (isRef(ref)) {
|
} else if (isRef(ref)) {
|
||||||
ref.value = value
|
ref.value = value
|
||||||
} else if (isFunction(ref)) {
|
} else if (isFunction(ref)) {
|
||||||
callWithErrorHandling(ref, parent, ErrorCodes.FUNCTION_REF, [value])
|
callWithErrorHandling(ref, parent, ErrorCodes.FUNCTION_REF, [value, refs])
|
||||||
} else if (__DEV__) {
|
} else if (__DEV__) {
|
||||||
warn('Invalid template ref type:', value, `(${typeof value})`)
|
warn('Invalid template ref type:', value, `(${typeof value})`)
|
||||||
}
|
}
|
||||||
|
@ -52,10 +52,17 @@ export type VNodeTypes =
|
|||||||
| typeof PortalImpl
|
| typeof PortalImpl
|
||||||
| typeof SuspenseImpl
|
| typeof SuspenseImpl
|
||||||
|
|
||||||
|
export type VNodeRef =
|
||||||
|
| string
|
||||||
|
| Ref
|
||||||
|
| ((ref: object | null, refs: Record<string, any>) => void)
|
||||||
|
|
||||||
|
export type VNodeNormalizedRef = [ComponentInternalInstance, VNodeRef]
|
||||||
|
|
||||||
export interface VNodeProps {
|
export interface VNodeProps {
|
||||||
[key: string]: any
|
[key: string]: any
|
||||||
key?: string | number
|
key?: string | number
|
||||||
ref?: string | Ref | ((ref: object | null) => void)
|
ref?: VNodeRef
|
||||||
|
|
||||||
// vnode hooks
|
// vnode hooks
|
||||||
onVnodeBeforeMount?: (vnode: VNode) => void
|
onVnodeBeforeMount?: (vnode: VNode) => void
|
||||||
@ -95,7 +102,7 @@ export interface VNode<HostNode = any, HostElement = any> {
|
|||||||
type: VNodeTypes
|
type: VNodeTypes
|
||||||
props: VNodeProps | null
|
props: VNodeProps | null
|
||||||
key: string | number | null
|
key: string | number | null
|
||||||
ref: string | Ref | ((ref: object | null) => void) | null
|
ref: VNodeNormalizedRef | null
|
||||||
scopeId: string | null // SFC only
|
scopeId: string | null // SFC only
|
||||||
children: VNodeNormalizedChildren<HostNode, HostElement>
|
children: VNodeNormalizedChildren<HostNode, HostElement>
|
||||||
component: ComponentInternalInstance | null
|
component: ComponentInternalInstance | null
|
||||||
@ -261,7 +268,10 @@ export function createVNode(
|
|||||||
type,
|
type,
|
||||||
props,
|
props,
|
||||||
key: props !== null && props.key !== undefined ? props.key : null,
|
key: props !== null && props.key !== undefined ? props.key : null,
|
||||||
ref: (props !== null && props.ref) || null,
|
ref:
|
||||||
|
props !== null && props.ref !== undefined
|
||||||
|
? [currentRenderingInstance!, props.ref]
|
||||||
|
: null,
|
||||||
scopeId: currentScopeId,
|
scopeId: currentScopeId,
|
||||||
children: null,
|
children: null,
|
||||||
component: null,
|
component: null,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user