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

View File

@ -26,6 +26,7 @@ import {
type SetEventIRNode, type SetEventIRNode,
type SetHtmlIRNode, type SetHtmlIRNode,
type SetPropIRNode, type SetPropIRNode,
type SetRefIRNode,
type SetTextIRNode, type SetTextIRNode,
type VaporHelper, type VaporHelper,
type WithDirectiveIRNode, type WithDirectiveIRNode,
@ -386,6 +387,8 @@ function genOperation(oper: OperationNode, context: CodegenContext) {
return genSetEvent(oper, context) return genSetEvent(oper, context)
case IRNodeTypes.SET_HTML: case IRNodeTypes.SET_HTML:
return genSetHtml(oper, context) return genSetHtml(oper, context)
case IRNodeTypes.SET_REF:
return genSetRef(oper, context)
case IRNodeTypes.CREATE_TEXT_NODE: case IRNodeTypes.CREATE_TEXT_NODE:
return genCreateTextNode(oper, context) return genCreateTextNode(oper, context)
case IRNodeTypes.INSERT_NODE: 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( function genCreateTextNode(
oper: CreateTextNodeIRNode, oper: CreateTextNodeIRNode,
context: CodegenContext, context: CodegenContext,

View File

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

View File

@ -4,7 +4,7 @@ import {
ElementTypes, ElementTypes,
NodeTypes, NodeTypes,
} from '@vue/compiler-dom' } from '@vue/compiler-dom'
import { isBuiltInDirective, isVoidTag } from '@vue/shared' import { isBuiltInDirective, isReservedProp, isVoidTag } from '@vue/shared'
import type { NodeTransform, TransformContext } from '../transform' import type { NodeTransform, TransformContext } from '../transform'
import { IRNodeTypes, type VaporDirectiveNode } from '../ir' import { IRNodeTypes, type VaporDirectiveNode } from '../ir'
@ -60,6 +60,8 @@ function transformProp(
context: TransformContext<ElementNode>, context: TransformContext<ElementNode>,
): void { ): void {
const { name, loc } = prop const { name, loc } = prop
if (isReservedProp(name)) return
if (prop.type === NodeTypes.ATTRIBUTE) { if (prop.type === NodeTypes.ATTRIBUTE) {
context.template += ` ${name}` context.template += ` ${name}`
if (prop.value) context.template += `="${prop.value.content}"` 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, createCompilerError,
createSimpleExpression, createSimpleExpression,
} from '@vue/compiler-core' } from '@vue/compiler-core'
import { camelize } from '@vue/shared' import { camelize, isReservedProp } from '@vue/shared'
import { IRNodeTypes } from '../ir' import { IRNodeTypes } from '../ir'
import type { DirectiveTransform } from '../transform' 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) => { export const transformVBind: DirectiveTransform = (dir, node, context) => {
let { arg, exp, loc, modifiers } = dir let { arg, exp, loc, modifiers } = dir
@ -15,12 +25,9 @@ export const transformVBind: DirectiveTransform = (dir, node, context) => {
// TODO support v-bind="{}" // TODO support v-bind="{}"
return return
} }
if (!exp) { if (arg.isStatic && isReservedProp(arg.content)) return
// shorthand syntax https://github.com/vuejs/core/pull/9451
const propName = camelize(arg.content) if (!exp) exp = normalizeBindShorthand(arg)
exp = createSimpleExpression(propName, false, arg.loc)
exp.ast = null
}
let camel = false let camel = false
if (modifiers.includes('camel')) { if (modifiers.includes('camel')) {

View File

@ -2,6 +2,7 @@ import { isArray, toDisplayString } from '@vue/shared'
import type { Block, ParentBlock } from './render' import type { Block, ParentBlock } from './render'
export * from './dom/patchProp' export * from './dom/patchProp'
export * from './dom/templateRef'
export function insert(block: Block, parent: Node, anchor: Node | null = null) { export function insert(block: Block, parent: Node, anchor: Node | null = null) {
// if (!isHydrating) { // 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 { initProps } from './componentProps'
import { invokeDirectiveHook } from './directive' import { invokeDirectiveHook } from './directive'
import { insert, remove } from './dom' import { insert, remove } from './dom'
import { queuePostRenderEffect } from './scheduler'
export type Block = Node | Fragment | Block[] export type Block = Node | Fragment | Block[]
export type ParentBlock = ParentNode | Node[] export type ParentBlock = ParentNode | Node[]
@ -78,8 +79,10 @@ export function mountComponent(
instance.isMounted = true instance.isMounted = true
// hook: mounted // hook: mounted
invokeDirectiveHook(instance, 'mounted') queuePostRenderEffect(() => {
m && invokeArrayFns(m) invokeDirectiveHook(instance, 'mounted')
m && invokeArrayFns(m)
})
reset() reset()
return instance return instance

View File

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