feat: support v-bind="{}" (#106)

Co-authored-by: 三咲智子 Kevin Deng <sxzz@sxzz.moe>
This commit is contained in:
ygj6 2024-02-05 02:44:30 +08:00 committed by GitHub
parent 920b36490e
commit d573a3d2f2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 579 additions and 290 deletions

View File

@ -232,7 +232,7 @@ exports[`compile > expression parsing > v-bind 1`] = `
const n0 = t0()
const { 0: [n1],} = _children(n0)
_renderEffect(() => {
_setDynamicProp(n1, key.value+1, _unref(foo)[key.value+1]())
_setDynamicProps(n1, { [key.value+1]: _unref(foo)[key.value+1]() })
})
return n0
})()"

View File

@ -44,14 +44,14 @@ export function render(_ctx) {
exports[`compiler v-bind > .camel modifier w/ dynamic arg 1`] = `
"import { camelize as _camelize } from 'vue';
import { template as _template, children as _children, renderEffect as _renderEffect, setDynamicProp as _setDynamicProp } from 'vue/vapor';
import { template as _template, children as _children, renderEffect as _renderEffect, setDynamicProps as _setDynamicProps } from 'vue/vapor';
export function render(_ctx) {
const t0 = _template("<div></div>")
const n0 = t0()
const { 0: [n1],} = _children(n0)
_renderEffect(() => {
_setDynamicProp(n1, _camelize(_ctx.foo), _ctx.id)
_setDynamicProps(n1, { [_camelize(_ctx.foo)]: _ctx.id })
})
return n0
}"
@ -114,14 +114,14 @@ export function render(_ctx) {
`;
exports[`compiler v-bind > .prop modifier w/ dynamic arg 1`] = `
"import { template as _template, children as _children, renderEffect as _renderEffect, setDynamicProp as _setDynamicProp } from 'vue/vapor';
"import { template as _template, children as _children, renderEffect as _renderEffect, setDynamicProps as _setDynamicProps } from 'vue/vapor';
export function render(_ctx) {
const t0 = _template("<div></div>")
const n0 = t0()
const { 0: [n1],} = _children(n0)
_renderEffect(() => {
_setDynamicProp(n1, \`.\${_ctx.fooBar}\`, _ctx.id)
_setDynamicProps(n1, { [\`.\${_ctx.fooBar}\`]: _ctx.id })
})
return n0
}"
@ -156,14 +156,14 @@ export function render(_ctx) {
`;
exports[`compiler v-bind > dynamic arg 1`] = `
"import { template as _template, children as _children, renderEffect as _renderEffect, setDynamicProp as _setDynamicProp } from 'vue/vapor';
"import { template as _template, children as _children, renderEffect as _renderEffect, setDynamicProps as _setDynamicProps } from 'vue/vapor';
export function render(_ctx) {
const t0 = _template("<div></div>")
const n0 = t0()
const { 0: [n1],} = _children(n0)
_renderEffect(() => {
_setDynamicProp(n1, _ctx.id, _ctx.id)
_setDynamicProps(n1, { [_ctx.id]: _ctx.id, [_ctx.title]: _ctx.title })
})
return n0
}"

View File

@ -41,25 +41,33 @@ describe('compiler v-bind', () => {
{
type: IRNodeTypes.SET_PROP,
element: 1,
key: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'id',
isStatic: true,
loc: {
start: { line: 1, column: 13, offset: 12 },
end: { line: 1, column: 15, offset: 14 },
source: 'id',
prop: {
key: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'id',
isStatic: true,
loc: {
start: { line: 1, column: 13, offset: 12 },
end: { line: 1, column: 15, offset: 14 },
source: 'id',
},
},
},
value: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'id',
isStatic: false,
loc: {
source: 'id',
start: { line: 1, column: 17, offset: 16 },
end: { line: 1, column: 19, offset: 18 },
value: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'id',
isStatic: false,
loc: {
source: 'id',
start: { line: 1, column: 17, offset: 16 },
end: { line: 1, column: 19, offset: 18 },
},
},
loc: {
start: { column: 6, line: 1, offset: 5 },
end: { column: 20, line: 1, offset: 19 },
source: 'v-bind:id="id"',
},
runtimeCamelize: false,
},
},
],
@ -72,68 +80,91 @@ describe('compiler v-bind', () => {
test('no expression', () => {
const { ir, code } = compileWithVBind(`<div v-bind:id />`)
expect(code).matchSnapshot()
expect(ir.effect[0].operations[0]).toMatchObject({
type: IRNodeTypes.SET_PROP,
key: {
content: `id`,
isStatic: true,
loc: {
start: { line: 1, column: 13, offset: 12 },
end: { line: 1, column: 15, offset: 14 },
prop: {
key: {
content: `id`,
isStatic: true,
loc: {
start: { line: 1, column: 13, offset: 12 },
end: { line: 1, column: 15, offset: 14 },
},
},
},
value: {
content: `id`,
isStatic: false,
loc: {
start: { line: 1, column: 13, offset: 12 },
end: { line: 1, column: 15, offset: 14 },
value: {
content: `id`,
isStatic: false,
loc: {
start: { line: 1, column: 13, offset: 12 },
end: { line: 1, column: 15, offset: 14 },
},
},
},
})
expect(code).matchSnapshot()
expect(code).contains('_setDynamicProp(n1, "id", _ctx.id)')
})
test('no expression (shorthand)', () => {
const { ir, code } = compileWithVBind(`<div :camel-case />`)
expect(code).matchSnapshot()
expect(ir.effect[0].operations[0]).toMatchObject({
type: IRNodeTypes.SET_PROP,
key: {
content: `camel-case`,
isStatic: true,
},
value: {
content: `camelCase`,
isStatic: false,
prop: {
key: {
content: `camel-case`,
isStatic: true,
},
value: {
content: `camelCase`,
isStatic: false,
},
},
})
expect(code).matchSnapshot()
expect(code).contains('_setDynamicProp(n1, "camel-case", _ctx.camelCase)')
})
test('dynamic arg', () => {
const { ir, code } = compileWithVBind(`<div v-bind:[id]="id"/>`)
expect(ir.effect[0].operations[0]).toMatchObject({
type: IRNodeTypes.SET_PROP,
element: 1,
key: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'id',
isStatic: false,
},
value: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'id',
isStatic: false,
},
})
const { ir, code } = compileWithVBind(
`<div v-bind:[id]="id" v-bind:[title]="title" />`,
)
expect(code).matchSnapshot()
expect(code).contains('_setDynamicProp(n1, _ctx.id, _ctx.id)')
expect(ir.effect[0].operations[0]).toMatchObject({
type: IRNodeTypes.SET_DYNAMIC_PROPS,
element: 1,
props: [
[
{
key: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'id',
isStatic: false,
},
value: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'id',
isStatic: false,
},
},
{
key: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'title',
isStatic: false,
},
value: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'title',
isStatic: false,
},
},
],
],
})
expect(code).contains(
'_setDynamicProps(n1, { [_ctx.id]: _ctx.id, [_ctx.title]: _ctx.title })',
)
})
test('should error if empty expression', () => {
@ -162,16 +193,18 @@ describe('compiler v-bind', () => {
const { ir, code } = compileWithVBind(`<div v-bind:foo-bar.camel="id"/>`)
expect(ir.effect[0].operations[0]).toMatchObject({
key: {
content: `fooBar`,
isStatic: true,
prop: {
key: {
content: `fooBar`,
isStatic: true,
},
value: {
content: `id`,
isStatic: false,
},
runtimeCamelize: false,
modifier: undefined,
},
value: {
content: `id`,
isStatic: false,
},
runtimeCamelize: false,
modifier: undefined,
})
expect(code).matchSnapshot()
@ -181,20 +214,21 @@ describe('compiler v-bind', () => {
test('.camel modifier w/ no expression', () => {
const { ir, code } = compileWithVBind(`<div v-bind:foo-bar.camel />`)
expect(ir.effect[0].operations[0]).toMatchObject({
key: {
content: `fooBar`,
isStatic: true,
},
value: {
content: `fooBar`,
isStatic: false,
},
runtimeCamelize: false,
modifier: undefined,
})
expect(code).matchSnapshot()
expect(ir.effect[0].operations[0]).toMatchObject({
prop: {
key: {
content: `fooBar`,
isStatic: true,
},
value: {
content: `fooBar`,
isStatic: false,
},
runtimeCamelize: false,
modifier: undefined,
},
})
expect(code).contains('renderEffect')
expect(code).contains('_setDynamicProp(n1, "fooBar", _ctx.fooBar)')
})
@ -203,21 +237,30 @@ describe('compiler v-bind', () => {
const { ir, code } = compileWithVBind(`<div v-bind:[foo].camel="id"/>`)
expect(ir.effect[0].operations[0]).toMatchObject({
key: {
content: `foo`,
isStatic: false,
},
value: {
content: `id`,
isStatic: false,
},
runtimeCamelize: true,
modifier: undefined,
type: IRNodeTypes.SET_DYNAMIC_PROPS,
props: [
[
{
key: {
content: `foo`,
isStatic: false,
},
value: {
content: `id`,
isStatic: false,
},
runtimeCamelize: true,
modifier: undefined,
},
],
],
})
expect(code).matchSnapshot()
expect(code).contains('renderEffect')
expect(code).contains(`_setDynamicProp(n1, _camelize(_ctx.foo), _ctx.id)`)
expect(code).contains(
`_setDynamicProps(n1, { [_camelize(_ctx.foo)]: _ctx.id })`,
)
})
test.todo('.camel modifier w/ dynamic arg + prefixIdentifiers')
@ -225,20 +268,21 @@ describe('compiler v-bind', () => {
test('.prop modifier', () => {
const { ir, code } = compileWithVBind(`<div v-bind:fooBar.prop="id"/>`)
expect(ir.effect[0].operations[0]).toMatchObject({
key: {
content: `fooBar`,
isStatic: true,
},
value: {
content: `id`,
isStatic: false,
},
runtimeCamelize: false,
modifier: '.',
})
expect(code).matchSnapshot()
expect(ir.effect[0].operations[0]).toMatchObject({
prop: {
key: {
content: `fooBar`,
isStatic: true,
},
value: {
content: `id`,
isStatic: false,
},
runtimeCamelize: false,
modifier: '.',
},
})
expect(code).contains('renderEffect')
expect(code).contains('_setDOMProp(n1, "fooBar", _ctx.id)')
})
@ -246,20 +290,21 @@ describe('compiler v-bind', () => {
test('.prop modifier w/ no expression', () => {
const { ir, code } = compileWithVBind(`<div v-bind:fooBar.prop />`)
expect(ir.effect[0].operations[0]).toMatchObject({
key: {
content: `fooBar`,
isStatic: true,
},
value: {
content: `fooBar`,
isStatic: false,
},
runtimeCamelize: false,
modifier: '.',
})
expect(code).matchSnapshot()
expect(ir.effect[0].operations[0]).toMatchObject({
prop: {
key: {
content: `fooBar`,
isStatic: true,
},
value: {
content: `fooBar`,
isStatic: false,
},
runtimeCamelize: false,
modifier: '.',
},
})
expect(code).contains('renderEffect')
expect(code).contains('_setDOMProp(n1, "fooBar", _ctx.fooBar)')
})
@ -267,22 +312,30 @@ describe('compiler v-bind', () => {
test('.prop modifier w/ dynamic arg', () => {
const { ir, code } = compileWithVBind(`<div v-bind:[fooBar].prop="id"/>`)
expect(ir.effect[0].operations[0]).toMatchObject({
key: {
content: `fooBar`,
isStatic: false,
},
value: {
content: `id`,
isStatic: false,
},
runtimeCamelize: false,
modifier: '.',
})
expect(code).matchSnapshot()
expect(ir.effect[0].operations[0]).toMatchObject({
type: IRNodeTypes.SET_DYNAMIC_PROPS,
props: [
[
{
key: {
content: `fooBar`,
isStatic: false,
},
value: {
content: `id`,
isStatic: false,
},
runtimeCamelize: false,
modifier: '.',
},
],
],
})
expect(code).contains('renderEffect')
expect(code).contains('_setDynamicProp(n1, `.${_ctx.fooBar}`, _ctx.id)')
expect(code).contains(
'_setDynamicProps(n1, { [`.${_ctx.fooBar}`]: _ctx.id })',
)
})
test.todo('.prop modifier w/ dynamic arg + prefixIdentifiers')
@ -290,20 +343,21 @@ describe('compiler v-bind', () => {
test('.prop modifier (shorthand)', () => {
const { ir, code } = compileWithVBind(`<div .fooBar="id"/>`)
expect(ir.effect[0].operations[0]).toMatchObject({
key: {
content: `fooBar`,
isStatic: true,
},
value: {
content: `id`,
isStatic: false,
},
runtimeCamelize: false,
modifier: '.',
})
expect(code).matchSnapshot()
expect(ir.effect[0].operations[0]).toMatchObject({
prop: {
key: {
content: `fooBar`,
isStatic: true,
},
value: {
content: `id`,
isStatic: false,
},
runtimeCamelize: false,
modifier: '.',
},
})
expect(code).contains('renderEffect')
expect(code).contains('_setDOMProp(n1, "fooBar", _ctx.id)')
})
@ -311,20 +365,21 @@ describe('compiler v-bind', () => {
test('.prop modifier (shortband) w/ no expression', () => {
const { ir, code } = compileWithVBind(`<div .fooBar />`)
expect(ir.effect[0].operations[0]).toMatchObject({
key: {
content: `fooBar`,
isStatic: true,
},
value: {
content: `fooBar`,
isStatic: false,
},
runtimeCamelize: false,
modifier: '.',
})
expect(code).matchSnapshot()
expect(ir.effect[0].operations[0]).toMatchObject({
prop: {
key: {
content: `fooBar`,
isStatic: true,
},
value: {
content: `fooBar`,
isStatic: false,
},
runtimeCamelize: false,
modifier: '.',
},
})
expect(code).contains('renderEffect')
expect(code).contains('_setDOMProp(n1, "fooBar", _ctx.fooBar)')
})
@ -332,20 +387,21 @@ describe('compiler v-bind', () => {
test('.attr modifier', () => {
const { ir, code } = compileWithVBind(`<div v-bind:foo-bar.attr="id"/>`)
expect(ir.effect[0].operations[0]).toMatchObject({
key: {
content: `foo-bar`,
isStatic: true,
},
value: {
content: `id`,
isStatic: false,
},
runtimeCamelize: false,
modifier: '^',
})
expect(code).matchSnapshot()
expect(ir.effect[0].operations[0]).toMatchObject({
prop: {
key: {
content: `foo-bar`,
isStatic: true,
},
value: {
content: `id`,
isStatic: false,
},
runtimeCamelize: false,
modifier: '^',
},
})
expect(code).contains('renderEffect')
expect(code).contains('_setAttr(n1, "foo-bar", _ctx.id)')
})
@ -353,20 +409,22 @@ describe('compiler v-bind', () => {
test('.attr modifier w/ no expression', () => {
const { ir, code } = compileWithVBind(`<div v-bind:foo-bar.attr />`)
expect(code).matchSnapshot()
expect(ir.effect[0].operations[0]).toMatchObject({
key: {
content: `foo-bar`,
isStatic: true,
prop: {
key: {
content: `foo-bar`,
isStatic: true,
},
value: {
content: `fooBar`,
isStatic: false,
},
runtimeCamelize: false,
modifier: '^',
},
value: {
content: `fooBar`,
isStatic: false,
},
runtimeCamelize: false,
modifier: '^',
})
expect(code).matchSnapshot()
expect(code).contains('renderEffect')
expect(code).contains('_setAttr(n1, "foo-bar", _ctx.fooBar)')
})

View File

@ -43,16 +43,18 @@ describe('compiler: v-once', () => {
},
{
element: 2,
key: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'class',
isStatic: true,
},
type: IRNodeTypes.SET_PROP,
value: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'clz',
isStatic: false,
prop: {
key: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'class',
isStatic: true,
},
value: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'clz',
isStatic: false,
},
},
},
{
@ -73,15 +75,17 @@ describe('compiler: v-once', () => {
{
type: IRNodeTypes.SET_PROP,
element: 1,
key: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'id',
isStatic: true,
},
value: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'foo',
isStatic: false,
prop: {
key: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'id',
isStatic: true,
},
value: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'foo',
isStatic: false,
},
},
},
])
@ -100,16 +104,18 @@ describe('compiler: v-once', () => {
{
type: IRNodeTypes.SET_PROP,
element: 1,
runtimeCamelize: false,
key: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'id',
isStatic: true,
},
value: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'foo',
isStatic: false,
prop: {
runtimeCamelize: false,
key: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'id',
isStatic: true,
},
value: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'foo',
isStatic: false,
},
},
},
])

View File

@ -13,7 +13,7 @@ import { genFor } from './for'
import { genSetHtml } from './html'
import { genIf } from './if'
import { genSetModelValue } from './modelValue'
import { genSetProp } from './prop'
import { genDynamicProps, genSetProp } from './prop'
import { genSetRef } from './ref'
import { genCreateTextNode, genSetText } from './text'
@ -32,6 +32,8 @@ export function genOperation(
switch (oper.type) {
case IRNodeTypes.SET_PROP:
return genSetProp(oper, context)
case IRNodeTypes.SET_DYNAMIC_PROPS:
return genDynamicProps(oper, context)
case IRNodeTypes.SET_TEXT:
return genSetText(oper, context)
case IRNodeTypes.SET_EVENT:

View File

@ -1,54 +1,107 @@
import { type CodeFragment, type CodegenContext, NEWLINE } from '../generate'
import type { SetPropIRNode, VaporHelper } from '../ir'
import type { SetDynamicPropsIRNode, SetPropIRNode, VaporHelper } from '../ir'
import { genExpression } from './expression'
import { isString } from '@vue/shared'
import type { DirectiveTransformResult } from '../transform'
import { isSimpleIdentifier } from '@vue/compiler-core'
// only the static key prop will reach here
export function genSetProp(
oper: SetPropIRNode,
context: CodegenContext,
): CodeFragment[] {
const { call, vaporHelper, helper } = context
const { call, vaporHelper } = context
const {
prop: { key, value, modifier },
} = oper
const element = `n${oper.element}`
const key = genExpression(oper.key, context)
const value = genExpression(oper.value, context)
const keyName = key.content
// fast path for static props
if (isString(oper.key) || oper.key.isStatic) {
const keyName = isString(oper.key) ? oper.key : oper.key.content
let helperName: VaporHelper | undefined
let omitKey = false
if (keyName === 'class') {
helperName = 'setClass'
omitKey = true
} else if (keyName === 'style') {
helperName = 'setStyle'
omitKey = true
} else if (oper.modifier) {
helperName = oper.modifier === '.' ? 'setDOMProp' : 'setAttr'
}
if (helperName) {
return [
NEWLINE,
...call(vaporHelper(helperName), element, omitKey ? false : key, value),
]
}
let helperName: VaporHelper
let omitKey = false
if (keyName === 'class') {
helperName = 'setClass'
omitKey = true
} else if (keyName === 'style') {
helperName = 'setStyle'
omitKey = true
} else if (modifier) {
helperName = modifier === '.' ? 'setDOMProp' : 'setAttr'
} else {
helperName = 'setDynamicProp'
}
return [
NEWLINE,
...call(vaporHelper('setDynamicProp'), element, genDynamicKey(), value),
...call(
vaporHelper(helperName),
`n${oper.element}`,
omitKey ? false : genExpression(key, context),
genExpression(value, context),
),
]
function genDynamicKey(): CodeFragment[] {
if (oper.runtimeCamelize) {
return call(helper('camelize'), key)
} else if (oper.modifier) {
return [`\`${oper.modifier}\${`, ...key, `}\``]
} else {
return key
}
}
}
// dynamic key props and v-bind="{}" will reach here
export function genDynamicProps(
oper: SetDynamicPropsIRNode,
context: CodegenContext,
): CodeFragment[] {
const { call, vaporHelper } = context
return [
NEWLINE,
...call(
vaporHelper('setDynamicProps'),
`n${oper.element}`,
...oper.props.map(
props =>
Array.isArray(props)
? genLiteralObjectProps(props, context) // static and dynamic arg props
: genExpression(props, context), // v-bind="{}"
),
),
]
}
function genLiteralObjectProps(
props: DirectiveTransformResult[],
context: CodegenContext,
): CodeFragment[] {
const { multi } = context
return multi(
['{ ', ' }', ', '],
...props.map(prop => [
...genPropertyKey(prop, context),
`: `,
...genExpression(prop.value, context),
]),
)
}
function genPropertyKey(
{ key: node, runtimeCamelize, modifier }: DirectiveTransformResult,
context: CodegenContext,
): CodeFragment[] {
const { call, helper } = context
// static arg was transformed by v-bind transformer
if (node.isStatic) {
// only quote keys if necessary
const keyName = node.content
return [isSimpleIdentifier(keyName) ? keyName : JSON.stringify(keyName)]
}
const key = genExpression(node, context)
if (runtimeCamelize && modifier) {
return [`[\`${modifier}\${`, ...call(helper('camelize'), key), `}\`]`]
}
if (runtimeCamelize) {
return [`[`, ...call(helper('camelize'), key), `]`]
}
if (modifier) {
return [`[\`${modifier}\${`, ...key, `}\`]`]
}
return [`[`, ...key, `]`]
}

View File

@ -7,7 +7,11 @@ import type {
TemplateChildNode,
} from '@vue/compiler-dom'
import type { Prettify } from '@vue/shared'
import type { DirectiveTransform, NodeTransform } from './transform'
import type {
DirectiveTransform,
DirectiveTransformResult,
NodeTransform,
} from './transform'
export enum IRNodeTypes {
ROOT,
@ -17,6 +21,7 @@ export enum IRNodeTypes {
FRAGMENT_FACTORY,
SET_PROP,
SET_DYNAMIC_PROPS,
SET_TEXT,
SET_EVENT,
SET_HTML,
@ -86,10 +91,15 @@ export interface FragmentFactoryIRNode extends BaseIRNode {
export interface SetPropIRNode extends BaseIRNode {
type: IRNodeTypes.SET_PROP
element: number
key: IRExpression
value: IRExpression
modifier?: '.' | '^'
runtimeCamelize: boolean
prop: DirectiveTransformResult
}
export type PropsExpression = DirectiveTransformResult[] | SimpleExpressionNode
export interface SetDynamicPropsIRNode extends BaseIRNode {
type: IRNodeTypes.SET_DYNAMIC_PROPS
element: number
props: PropsExpression[]
}
export interface SetTextIRNode extends BaseIRNode {
@ -174,6 +184,7 @@ export type IRNode =
| FragmentFactoryIRNode
export type OperationNode =
| SetPropIRNode
| SetDynamicPropsIRNode
| SetTextIRNode
| SetEventIRNode
| SetHtmlIRNode

View File

@ -9,6 +9,8 @@ import {
NodeTypes,
type ParentNode,
type RootNode,
type SimpleExpressionNode,
type SourceLocation,
type TemplateChildNode,
type TemplateNode,
defaultOnError,
@ -39,7 +41,15 @@ export type DirectiveTransform = (
dir: VaporDirectiveNode,
node: ElementNode,
context: TransformContext<ElementNode>,
) => void
) => DirectiveTransformResult | void
export interface DirectiveTransformResult {
key: SimpleExpressionNode
value: SimpleExpressionNode
loc: SourceLocation
modifier?: '.' | '^'
runtimeCamelize?: boolean
}
// A structural directive transform is technically also a NodeTransform;
// Only v-if and v-for fall into this category.

View File

@ -2,11 +2,22 @@ import {
type AttributeNode,
type ElementNode,
ElementTypes,
ErrorCodes,
NodeTypes,
type SimpleExpressionNode,
createCompilerError,
} from '@vue/compiler-dom'
import { isBuiltInDirective, isReservedProp, isVoidTag } from '@vue/shared'
import type { NodeTransform, TransformContext } from '../transform'
import { IRNodeTypes, type VaporDirectiveNode } from '../ir'
import type {
DirectiveTransformResult,
NodeTransform,
TransformContext,
} from '../transform'
import {
IRNodeTypes,
type PropsExpression,
type VaporDirectiveNode,
} from '../ir'
export const transformElement: NodeTransform = (node, context) => {
return function postTransformElement() {
@ -49,8 +60,75 @@ function buildProps(
props: ElementNode['props'] = node.props,
isComponent: boolean,
) {
for (const prop of props) {
transformProp(prop as VaporDirectiveNode | AttributeNode, node, context)
const dynamicArgs: PropsExpression[] = []
const dynamicExpr: SimpleExpressionNode[] = []
let results: DirectiveTransformResult[] = []
function pushExpressions(...exprs: SimpleExpressionNode[]) {
for (const expr of exprs) {
if (!expr.isStatic) dynamicExpr.push(expr)
}
}
function pushMergeArg() {
if (results.length) {
dynamicArgs.push(results)
results = []
}
}
for (const prop of props as (VaporDirectiveNode | AttributeNode)[]) {
if (
prop.type === NodeTypes.DIRECTIVE &&
prop.name === 'bind' &&
!prop.arg
) {
if (prop.exp) {
pushExpressions(prop.exp)
pushMergeArg()
dynamicArgs.push(prop.exp)
} else {
context.options.onError(
createCompilerError(ErrorCodes.X_V_BIND_NO_EXPRESSION, prop.loc),
)
}
continue
}
const result = transformProp(prop, node, context)
if (result) {
results.push(result)
pushExpressions(result.key, result.value)
}
}
// take rest of props as dynamic props
if (dynamicArgs.length || results.some(({ key }) => !key.isStatic)) {
pushMergeArg()
}
// has dynamic key or v-bind="{}"
if (dynamicArgs.length) {
context.registerEffect(dynamicExpr, [
{
type: IRNodeTypes.SET_DYNAMIC_PROPS,
element: context.reference(),
props: dynamicArgs,
},
])
} else {
for (const result of results) {
context.registerEffect(
[result.value],
[
{
type: IRNodeTypes.SET_PROP,
element: context.reference(),
prop: result,
},
],
)
}
}
}
@ -58,7 +136,7 @@ function transformProp(
prop: VaporDirectiveNode | AttributeNode,
node: ElementNode,
context: TransformContext<ElementNode>,
): void {
): DirectiveTransformResult | void {
const { name } = prop
if (isReservedProp(name)) return
@ -70,7 +148,7 @@ function transformProp(
const directiveTransform = context.options.directiveTransforms[name]
if (directiveTransform) {
directiveTransform(prop, node, context)
return directiveTransform(prop, node, context)
} else if (!isBuiltInDirective(name)) {
context.registerOperation({
type: IRNodeTypes.WITH_DIRECTIVE,

View File

@ -5,7 +5,6 @@ import {
createSimpleExpression,
} from '@vue/compiler-dom'
import { camelize, isReservedProp } from '@vue/shared'
import { IRNodeTypes } from '../ir'
import type { DirectiveTransform } from '../transform'
export function normalizeBindShorthand(
@ -19,12 +18,9 @@ export function normalizeBindShorthand(
}
export const transformVBind: DirectiveTransform = (dir, node, context) => {
let { arg, exp, loc, modifiers } = dir
let { exp, loc, modifiers } = dir
const arg = dir.arg!
if (!arg) {
// TODO support v-bind="{}"
return
}
if (arg.isStatic && isReservedProp(arg.content)) return
if (!exp) exp = normalizeBindShorthand(arg)
@ -46,21 +42,15 @@ export const transformVBind: DirectiveTransform = (dir, node, context) => {
return
}
context.registerEffect(
[exp],
[
{
type: IRNodeTypes.SET_PROP,
element: context.reference(),
key: arg,
value: exp,
runtimeCamelize: camel,
modifier: modifiers.includes('prop')
? '.'
: modifiers.includes('attr')
? '^'
: undefined,
},
],
)
return {
key: arg,
value: exp,
loc,
runtimeCamelize: camel,
modifier: modifiers.includes('prop')
? '.'
: modifiers.includes('attr')
? '^'
: undefined,
}
}

View File

@ -1,5 +1,8 @@
import {
type Data,
isArray,
isFunction,
isOn,
isString,
normalizeClass,
normalizeStyle,
@ -80,6 +83,47 @@ export function setDynamicProp(el: Element, key: string, value: any) {
}
}
export function setDynamicProps(el: Element, ...args: any) {
const props = args.length > 1 ? mergeProps(...args) : args[0]
// TODO remove all of old props before set new props since there is containing dynamic key
for (const key in props) {
setDynamicProp(el, key, props[key])
}
}
// TODO copied from runtime-core
function mergeProps(...args: Data[]) {
const ret: Data = {}
for (let i = 0; i < args.length; i++) {
const toMerge = args[i]
for (const key in toMerge) {
if (key === 'class') {
if (ret.class !== toMerge.class) {
ret.class = normalizeClass([ret.class, toMerge.class])
}
} else if (key === 'style') {
ret.style = normalizeStyle([ret.style, toMerge.style])
} else if (isOn(key)) {
const existing = ret[key]
const incoming = toMerge[key]
if (
incoming &&
existing !== incoming &&
!(isArray(existing) && existing.includes(incoming))
) {
ret[key] = existing
? [].concat(existing as any, incoming as any)
: incoming
}
} else if (key !== '') {
ret[key] = toMerge[key]
}
}
}
return ret
}
export function setText(el: Node, value: any) {
const oldVal = recordPropMetadata(
el,

37
playground/src/v-bind.vue Normal file
View File

@ -0,0 +1,37 @@
<script setup lang="ts">
import { computed, ref } from '@vue/vapor'
const count = ref(1)
const obj = computed(() => ({ id: String(count.value), subObj: { a: 'xxx' } }))
const key = ref('id')
const handleClick = () => {
count.value++
}
</script>
<template>
<button @click="handleClick">{{ count }}</button>
<!-- prop id's value should update reactively -->
<button :id="'before'" :[key]="'dynamic key after' + count">
{{ count }}
</button>
<!-- prop id's value should update only once -->
<button :[key]="'dynamic key before' + count" :id="'before'">
{{ count }}
</button>
<!-- object props should update reactively -->
<button v-bind="obj">{{ count }}</button>
<button v-bind="{ id: `${count}`, subObj: { a: 'xxx' } }">
{{ count }}
</button>
<!-- prop id's value should update reactively since it was override by object props -->
<button :id="'before'" v-bind="obj">{{ count }}</button>
<button :[key]="'dynamic key before'" v-bind="obj">
{{ count }}
</button>
<!-- prop id's value should update only once since the prop id in object props was override -->
<button v-bind="obj" :id="'after'">{{ count }}</button>
<button v-bind="obj" :[key]="'dynamic key after'">{{ count }}</button>
</template>