vue3-core/packages/runtime-vapor/src/apiTemplateRef.ts

169 lines
4.4 KiB
TypeScript

import { type Ref, isRef, onScopeDispose } from '@vue/reactivity'
import {
type VaporComponentInstance,
currentInstance,
getExposed,
isVaporComponent,
} from './component'
import {
ErrorCodes,
type SchedulerJob,
callWithErrorHandling,
queuePostFlushCb,
warn,
} from '@vue/runtime-dom'
import {
EMPTY_OBJ,
hasOwn,
isArray,
isFunction,
isString,
remove,
} from '@vue/shared'
import { DynamicFragment, isFragment } from './block'
export type NodeRef =
| string
| Ref
| ((ref: Element | VaporComponentInstance, refs: Record<string, any>) => void)
export type RefEl = Element | VaporComponentInstance
export type setRefFn = (
el: RefEl,
ref: NodeRef,
oldRef?: NodeRef,
refFor?: boolean,
) => NodeRef | undefined
export function createTemplateRefSetter(): setRefFn {
const instance = currentInstance as VaporComponentInstance
return (...args) => setRef(instance, ...args)
}
/**
* Function for handling a template ref
*/
export function setRef(
instance: VaporComponentInstance,
el: RefEl,
ref: NodeRef,
oldRef?: NodeRef,
refFor = false,
refKey?: string,
): NodeRef | undefined {
if (!instance || instance.isUnmounted) return
// vdom interop
if (isFragment(el) && el.setRef) {
el.setRef(instance, ref, refFor, refKey)
return
}
const setupState: any = __DEV__ ? instance.setupState || {} : null
const refValue = getRefValue(el)
const refs =
instance.refs === EMPTY_OBJ ? (instance.refs = {}) : instance.refs
// dynamic ref changed. unset old ref
if (oldRef != null && oldRef !== ref) {
if (isString(oldRef)) {
refs[oldRef] = null
if (__DEV__ && hasOwn(setupState, oldRef)) {
setupState[oldRef] = null
}
} else if (isRef(oldRef)) {
oldRef.value = null
}
}
if (isFunction(ref)) {
const invokeRefSetter = (value?: Element | Record<string, any>) => {
callWithErrorHandling(ref, currentInstance, ErrorCodes.FUNCTION_REF, [
value,
refs,
])
}
invokeRefSetter(refValue)
// TODO this gets called repeatedly in renderEffect when it's dynamic ref?
onScopeDispose(() => invokeRefSetter())
} else {
const _isString = isString(ref)
const _isRef = isRef(ref)
let existing: unknown
if (_isString || _isRef) {
const doSet: SchedulerJob = () => {
if (refFor) {
existing = _isString
? __DEV__ && hasOwn(setupState, ref)
? setupState[ref]
: refs[ref]
: ref.value
if (!isArray(existing)) {
existing = [refValue]
if (_isString) {
refs[ref] = existing
if (__DEV__ && hasOwn(setupState, ref)) {
setupState[ref] = refs[ref]
// if setupState[ref] is a reactivity ref,
// the existing will also become reactivity too
// need to get the Proxy object by resetting
existing = setupState[ref]
}
} else {
ref.value = existing
if (refKey) refs[refKey] = existing
}
} else if (!existing.includes(refValue)) {
existing.push(refValue)
}
} else if (_isString) {
refs[ref] = refValue
if (__DEV__ && hasOwn(setupState, ref)) {
setupState[ref] = refValue
}
} else if (_isRef) {
ref.value = refValue
if (refKey) refs[refKey] = refValue
} else if (__DEV__) {
warn('Invalid template ref type:', ref, `(${typeof ref})`)
}
}
doSet.id = -1
queuePostFlushCb(doSet)
// TODO this gets called repeatedly in renderEffect when it's dynamic ref?
onScopeDispose(() => {
queuePostFlushCb(() => {
if (isArray(existing)) {
remove(existing, refValue)
} else if (_isString) {
refs[ref] = null
if (__DEV__ && hasOwn(setupState, ref)) {
setupState[ref] = null
}
} else if (_isRef) {
ref.value = null
if (refKey) refs[refKey] = null
}
})
})
} else if (__DEV__) {
warn('Invalid template ref type:', ref, `(${typeof ref})`)
}
}
return ref
}
const getRefValue = (el: RefEl) => {
if (isVaporComponent(el)) {
return getExposed(el) || el
} else if (el instanceof DynamicFragment) {
return getRefValue(el.nodes as RefEl)
}
return el
}