feat: basic template ref

This commit is contained in:
三咲智子 Kevin Deng 2024-01-20 23:48:10 +08:00
parent 6a26db2adc
commit 782d60475d
No known key found for this signature in database
GPG Key ID: 69992F2250DFD93E
10 changed files with 135 additions and 12 deletions

View File

@ -21,6 +21,7 @@ import { transformVText } from './transforms/vText'
import { transformVBind } from './transforms/vBind'
import { transformVOn } from './transforms/vOn'
import { transformVShow } from './transforms/vShow'
import { transformRef } from './transforms/transformRef'
import { transformInterpolation } from './transforms/transformInterpolation'
import type { HackOptions } from './ir'
@ -95,7 +96,7 @@ export function getBaseTransformPreset(
prefixIdentifiers?: boolean,
): TransformPreset {
return [
[transformOnce, transformInterpolation, transformElement],
[transformOnce, transformRef, transformInterpolation, transformElement],
{
bind: transformVBind,
on: transformVOn,

View File

@ -26,6 +26,7 @@ import {
type SetEventIRNode,
type SetHtmlIRNode,
type SetPropIRNode,
type SetRefIRNode,
type SetTextIRNode,
type VaporHelper,
type WithDirectiveIRNode,
@ -386,6 +387,8 @@ function genOperation(oper: OperationNode, context: CodegenContext) {
return genSetEvent(oper, context)
case IRNodeTypes.SET_HTML:
return genSetHtml(oper, context)
case IRNodeTypes.SET_REF:
return genSetRef(oper, context)
case IRNodeTypes.CREATE_TEXT_NODE:
return genCreateTextNode(oper, context)
case IRNodeTypes.INSERT_NODE:
@ -442,6 +445,14 @@ function genSetHtml(oper: SetHtmlIRNode, context: CodegenContext) {
)
}
function genSetRef(oper: SetRefIRNode, context: CodegenContext) {
const { newline, pushFnCall, vaporHelper } = context
newline()
pushFnCall(vaporHelper('setRef'), `n${oper.element}`, () =>
genExpression(oper.value, context),
)
}
function genCreateTextNode(
oper: CreateTextNodeIRNode,
context: CodegenContext,

View File

@ -17,6 +17,7 @@ export enum IRNodeTypes {
SET_TEXT,
SET_EVENT,
SET_HTML,
SET_REF,
INSERT_NODE,
PREPEND_NODE,
@ -93,6 +94,12 @@ export interface SetHtmlIRNode extends BaseIRNode {
value: IRExpression
}
export interface SetRefIRNode extends BaseIRNode {
type: IRNodeTypes.SET_REF
element: number
value: IRExpression
}
export interface CreateTextNodeIRNode extends BaseIRNode {
type: IRNodeTypes.CREATE_TEXT_NODE
id: number
@ -134,6 +141,7 @@ export type OperationNode =
| SetTextIRNode
| SetEventIRNode
| SetHtmlIRNode
| SetRefIRNode
| CreateTextNodeIRNode
| InsertNodeIRNode
| PrependNodeIRNode

View File

@ -4,7 +4,7 @@ import {
ElementTypes,
NodeTypes,
} from '@vue/compiler-dom'
import { isBuiltInDirective, isVoidTag } from '@vue/shared'
import { isBuiltInDirective, isReservedProp, isVoidTag } from '@vue/shared'
import type { NodeTransform, TransformContext } from '../transform'
import { IRNodeTypes, type VaporDirectiveNode } from '../ir'
@ -60,6 +60,8 @@ function transformProp(
context: TransformContext<ElementNode>,
): void {
const { name, loc } = prop
if (isReservedProp(name)) return
if (prop.type === NodeTypes.ATTRIBUTE) {
context.template += ` ${name}`
if (prop.value) context.template += `="${prop.value.content}"`

View File

@ -0,0 +1,31 @@
import {
NodeTypes,
type SimpleExpressionNode,
findProp,
} from '@vue/compiler-dom'
import type { NodeTransform } from '../transform'
import { type IRExpression, IRNodeTypes } from '../ir'
import { normalizeBindShorthand } from './vBind'
export const transformRef: NodeTransform = (node, context) => {
if (node.type !== NodeTypes.ELEMENT) return
const dir = findProp(node, 'ref', false, true)
if (!dir) return
let value: IRExpression
if (dir.type === NodeTypes.DIRECTIVE) {
value =
(dir.exp as SimpleExpressionNode | undefined) ||
normalizeBindShorthand(dir.arg as SimpleExpressionNode)
} else {
value = dir.value ? JSON.stringify(dir.value.content) : '""'
}
context.registerOperation({
type: IRNodeTypes.SET_REF,
element: context.reference(),
value,
loc: dir.loc,
})
}

View File

@ -4,10 +4,20 @@ import {
createCompilerError,
createSimpleExpression,
} from '@vue/compiler-core'
import { camelize } from '@vue/shared'
import { camelize, isReservedProp } from '@vue/shared'
import { IRNodeTypes } from '../ir'
import type { DirectiveTransform } from '../transform'
export function normalizeBindShorthand(
arg: SimpleExpressionNode,
): SimpleExpressionNode {
// shorthand syntax https://github.com/vuejs/core/pull/9451
const propName = camelize(arg.content)
const exp = createSimpleExpression(propName, false, arg.loc)
exp.ast = null
return exp
}
export const transformVBind: DirectiveTransform = (dir, node, context) => {
let { arg, exp, loc, modifiers } = dir
@ -15,12 +25,9 @@ export const transformVBind: DirectiveTransform = (dir, node, context) => {
// TODO support v-bind="{}"
return
}
if (!exp) {
// shorthand syntax https://github.com/vuejs/core/pull/9451
const propName = camelize(arg.content)
exp = createSimpleExpression(propName, false, arg.loc)
exp.ast = null
}
if (arg.isStatic && isReservedProp(arg.content)) return
if (!exp) exp = normalizeBindShorthand(arg)
let camel = false
if (modifiers.includes('camel')) {

View File

@ -2,6 +2,7 @@ import { isArray, toDisplayString } from '@vue/shared'
import type { Block, ParentBlock } from './render'
export * from './dom/patchProp'
export * from './dom/templateRef'
export function insert(block: Block, parent: Node, anchor: Node | null = null) {
// if (!isHydrating) {

View File

@ -0,0 +1,52 @@
import { type Ref, type SchedulerJob, isRef } from '@vue/reactivity'
import { currentInstance } from '../component'
import { VaporErrorCodes, callWithErrorHandling } from '../errorHandling'
import { hasOwn, isFunction, isString } from '@vue/shared'
import { warn } from '../warning'
import { queuePostRenderEffect } from '../scheduler'
export type NodeRef = string | Ref | ((ref: Element) => void)
/**
* Function for handling a template ref
*/
export function setRef(el: Element, ref: NodeRef) {
if (!currentInstance) return
const { setupState, isUnmounted } = currentInstance
if (isFunction(ref)) {
callWithErrorHandling(ref, currentInstance, VaporErrorCodes.FUNCTION_REF, [
el,
// refs,
])
} else {
const _isString = isString(ref)
const _isRef = isRef(ref)
if (_isString || _isRef) {
const doSet = () => {
if (_isString) {
if (hasOwn(setupState, ref)) {
setupState[ref] = el
}
} else if (_isRef) {
ref.value = el
} else if (__DEV__) {
warn('Invalid template ref type:', ref, `(${typeof ref})`)
}
}
// #9908 ref on v-for mutates the same array for both mount and unmount
// and should be done together
if (isUnmounted /* || isVFor */) {
doSet()
} else {
// #1789: set new refs in a post job so that they don't get overwritten
// by unmounting ones.
;(doSet as SchedulerJob).id = -1
queuePostRenderEffect(doSet)
}
} else if (__DEV__) {
warn('Invalid template ref type:', ref, `(${typeof ref})`)
}
}
}

View File

@ -10,6 +10,7 @@ import {
import { initProps } from './componentProps'
import { invokeDirectiveHook } from './directive'
import { insert, remove } from './dom'
import { queuePostRenderEffect } from './scheduler'
export type Block = Node | Fragment | Block[]
export type ParentBlock = ParentNode | Node[]
@ -78,8 +79,10 @@ export function mountComponent(
instance.isMounted = true
// hook: mounted
invokeDirectiveHook(instance, 'mounted')
m && invokeArrayFns(m)
queuePostRenderEffect(() => {
invokeDirectiveHook(instance, 'mounted')
m && invokeArrayFns(m)
})
reset()
return instance

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref } from 'vue/vapor'
import { onMounted, ref } from 'vue/vapor'
interface Task {
title: string
@ -7,6 +7,7 @@ interface Task {
}
const tasks = ref<Task[]>([])
const value = ref('hello')
const inputRef = ref<HTMLInputElement>()
function handleAdd() {
tasks.value.push({
@ -16,6 +17,11 @@ function handleAdd() {
// TODO: clear input
value.value = ''
}
onMounted(() => {
console.log('onMounted')
console.log(inputRef.value)
})
</script>
<template>
@ -41,6 +47,7 @@ function handleAdd() {
<li>
<input
type="text"
:ref="el => (inputRef = el)"
:value="value"
@input="evt => (value = evt.target.value)"
/>