feat(compiler-vapor): support custom directives argument & modifiers (#34)

Co-authored-by: 三咲智子 Kevin Deng <sxzz@sxzz.moe>
This commit is contained in:
白雾三语 2023-12-07 01:41:17 +08:00 committed by GitHub
parent 924b9ad732
commit 0cca23f574
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 201 additions and 16 deletions

View File

@ -16,6 +16,90 @@ export function render(_ctx) {
}" }"
`; `;
exports[`compile > directives > custom directive > basic 1`] = `
"import { template as _template, children as _children, withDirectives as _withDirectives } from 'vue/vapor';
export function render(_ctx) {
const t0 = _template("<div></div>")
const n0 = t0()
const { 0: [n1],} = _children(n0)
_withDirectives(n1, [[_ctx.vExample]])
return n0
}"
`;
exports[`compile > directives > custom directive > binding value 1`] = `
"import { template as _template, children as _children, withDirectives as _withDirectives } from 'vue/vapor';
export function render(_ctx) {
const t0 = _template("<div></div>")
const n0 = t0()
const { 0: [n1],} = _children(n0)
_withDirectives(n1, [[_ctx.vExample, _ctx.msg]])
return n0
}"
`;
exports[`compile > directives > custom directive > dynamic parameters 1`] = `
"import { template as _template, children as _children, withDirectives as _withDirectives } from 'vue/vapor';
export function render(_ctx) {
const t0 = _template("<div></div>")
const n0 = t0()
const { 0: [n1],} = _children(n0)
_withDirectives(n1, [[_ctx.vExample, _ctx.msg, _ctx.foo]])
return n0
}"
`;
exports[`compile > directives > custom directive > modifiers 1`] = `
"import { template as _template, children as _children, withDirectives as _withDirectives } from 'vue/vapor';
export function render(_ctx) {
const t0 = _template("<div></div>")
const n0 = t0()
const { 0: [n1],} = _children(n0)
_withDirectives(n1, [[_ctx.vExample, _ctx.msg, void 0, { bar: true }]])
return n0
}"
`;
exports[`compile > directives > custom directive > modifiers w/o binding 1`] = `
"import { template as _template, children as _children, withDirectives as _withDirectives } from 'vue/vapor';
export function render(_ctx) {
const t0 = _template("<div></div>")
const n0 = t0()
const { 0: [n1],} = _children(n0)
_withDirectives(n1, [[_ctx.vExample, void 0, void 0, { "foo-bar": true }]])
return n0
}"
`;
exports[`compile > directives > custom directive > static parameters 1`] = `
"import { template as _template, children as _children, withDirectives as _withDirectives } from 'vue/vapor';
export function render(_ctx) {
const t0 = _template("<div></div>")
const n0 = t0()
const { 0: [n1],} = _children(n0)
_withDirectives(n1, [[_ctx.vExample, _ctx.msg, "foo"]])
return n0
}"
`;
exports[`compile > directives > custom directive > static parameters and modifiers 1`] = `
"import { template as _template, children as _children, withDirectives as _withDirectives } from 'vue/vapor';
export function render(_ctx) {
const t0 = _template("<div></div>")
const n0 = t0()
const { 0: [n1],} = _children(n0)
_withDirectives(n1, [[_ctx.vExample, _ctx.msg, "foo", { bar: true }]])
return n0
}"
`;
exports[`compile > directives > v-bind > .camel modifier 1`] = ` exports[`compile > directives > v-bind > .camel modifier 1`] = `
"import { template as _template, children as _children, effect as _effect, setAttr as _setAttr } from 'vue/vapor'; "import { template as _template, children as _children, effect as _effect, setAttr as _setAttr } from 'vue/vapor';

View File

@ -332,6 +332,76 @@ describe('compile', () => {
expect(code).not.contains('v-cloak') expect(code).not.contains('v-cloak')
}) })
}) })
describe('custom directive', () => {
test('basic', async () => {
const code = await compile(`<div v-example></div>`, {
bindingMetadata: {
vExample: BindingTypes.SETUP_CONST,
},
})
expect(code).matchSnapshot()
})
test('binding value', async () => {
const code = await compile(`<div v-example="msg"></div>`, {
bindingMetadata: {
msg: BindingTypes.SETUP_REF,
vExample: BindingTypes.SETUP_CONST,
},
})
expect(code).matchSnapshot()
})
test('static parameters', async () => {
const code = await compile(`<div v-example:foo="msg"></div>`, {
bindingMetadata: {
msg: BindingTypes.SETUP_REF,
vExample: BindingTypes.SETUP_CONST,
},
})
expect(code).matchSnapshot()
})
test('modifiers', async () => {
const code = await compile(`<div v-example.bar="msg"></div>`, {
bindingMetadata: {
msg: BindingTypes.SETUP_REF,
vExample: BindingTypes.SETUP_CONST,
},
})
expect(code).matchSnapshot()
})
test('modifiers w/o binding', async () => {
const code = await compile(`<div v-example.foo-bar></div>`, {
bindingMetadata: {
vExample: BindingTypes.SETUP_CONST,
},
})
expect(code).matchSnapshot()
})
test('static parameters and modifiers', async () => {
const code = await compile(`<div v-example:foo.bar="msg"></div>`, {
bindingMetadata: {
msg: BindingTypes.SETUP_REF,
vExample: BindingTypes.SETUP_CONST,
},
})
expect(code).matchSnapshot()
})
test('dynamic parameters', async () => {
const code = await compile(`<div v-example:[foo]="msg"></div>`, {
bindingMetadata: {
foo: BindingTypes.SETUP_REF,
vExample: BindingTypes.SETUP_CONST,
},
})
expect(code).matchSnapshot()
})
})
}) })
describe('expression parsing', () => { describe('expression parsing', () => {

View File

@ -10,6 +10,7 @@ import {
createSimpleExpression, createSimpleExpression,
walkIdentifiers, walkIdentifiers,
advancePositionWithClone, advancePositionWithClone,
isSimpleIdentifier,
} from '@vue/compiler-dom' } from '@vue/compiler-dom'
import { import {
type IRDynamicChildren, type IRDynamicChildren,
@ -469,21 +470,38 @@ function genSetEvent(oper: SetEventIRNode, context: CodegenContext) {
function genWithDirective(oper: WithDirectiveIRNode, context: CodegenContext) { function genWithDirective(oper: WithDirectiveIRNode, context: CodegenContext) {
const { push, pushWithNewline, vaporHelper, bindingMetadata } = context const { push, pushWithNewline, vaporHelper, bindingMetadata } = context
const { dir } = oper
// TODO merge directive for the same node // TODO merge directive for the same node
pushWithNewline(`${vaporHelper('withDirectives')}(n${oper.element}, [[`) pushWithNewline(`${vaporHelper('withDirectives')}(n${oper.element}, [[`)
// TODO resolve directive // TODO resolve directive
const directiveReference = camelize(`v-${oper.name}`) const directiveReference = camelize(`v-${dir.name}`)
if (bindingMetadata[directiveReference]) { if (bindingMetadata[directiveReference]) {
const directiveExpression = createSimpleExpression(directiveReference) const directiveExpression = createSimpleExpression(directiveReference)
directiveExpression.ast = null directiveExpression.ast = null
genExpression(directiveExpression, context) genExpression(directiveExpression, context)
} }
if (oper.binding) { if (dir.exp) {
push(', ') push(', ')
genExpression(oper.binding, context) genExpression(dir.exp, context)
} else if (dir.arg || dir.modifiers.length) {
push(', void 0')
}
if (dir.arg) {
push(', ')
genExpression(dir.arg, context)
} else if (dir.modifiers.length) {
push(', void 0')
}
if (dir.modifiers.length) {
push(', ')
push('{ ')
push(genDirectiveModifiers(dir.modifiers))
push(' }')
} }
push(']])') push(']])')
return return
@ -576,3 +594,12 @@ function genIdentifier(
} }
push(id, NewlineType.None, loc, name) push(id, NewlineType.None, loc, name)
} }
function genDirectiveModifiers(modifiers: string[]) {
return modifiers
.map(
(value) =>
`${isSimpleIdentifier(value) ? value : JSON.stringify(value)}: true`,
)
.join(', ')
}

View File

@ -117,8 +117,7 @@ export interface AppendNodeIRNode extends BaseIRNode {
export interface WithDirectiveIRNode extends BaseIRNode { export interface WithDirectiveIRNode extends BaseIRNode {
type: IRNodeTypes.WITH_DIRECTIVE type: IRNodeTypes.WITH_DIRECTIVE
element: number element: number
name: string dir: VaporDirectiveNode
binding: IRExpression | undefined
} }
export type IRNode = export type IRNode =

View File

@ -59,7 +59,7 @@ function transformProp(
node: ElementNode, node: ElementNode,
context: TransformContext<ElementNode>, context: TransformContext<ElementNode>,
): void { ): void {
const { name } = prop const { name, loc } = prop
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}"`
@ -70,13 +70,11 @@ function transformProp(
if (directiveTransform) { if (directiveTransform) {
directiveTransform(prop, node, context) directiveTransform(prop, node, context)
} else if (!isBuiltInDirective(name)) { } else if (!isBuiltInDirective(name)) {
// custom directive
context.registerOperation({ context.registerOperation({
type: IRNodeTypes.WITH_DIRECTIVE, type: IRNodeTypes.WITH_DIRECTIVE,
element: context.reference(), element: context.reference(),
name, dir: prop,
binding: prop.exp, loc: loc,
loc: prop.loc,
}) })
} }
} }

View File

@ -279,7 +279,8 @@ export type {
DirectiveHook, DirectiveHook,
ObjectDirective, ObjectDirective,
FunctionDirective, FunctionDirective,
DirectiveArguments DirectiveArguments,
DirectiveModifiers
} from './directives' } from './directives'
export type { SuspenseBoundary } from './components/Suspense' export type { SuspenseBoundary } from './components/Suspense'
export type { export type {

View File

@ -1,13 +1,12 @@
import { isFunction } from '@vue/shared' import { isFunction } from '@vue/shared'
import { currentInstance, type ComponentInternalInstance } from './component' import { currentInstance, type ComponentInternalInstance } from './component'
import type { DirectiveModifiers } from '@vue/runtime-dom'
export interface DirectiveBinding<V = any> { export interface DirectiveBinding<V = any> {
instance: ComponentInternalInstance | null instance: ComponentInternalInstance | null
value: V value: V
oldValue: V | null oldValue: V | null
arg?: string arg?: string
// TODO: should we support modifiers for custom directives? modifiers?: DirectiveModifiers
// modifiers: DirectiveModifiers
dir: ObjectDirective<any, V> dir: ObjectDirective<any, V>
} }
@ -41,6 +40,12 @@ export type DirectiveArguments = Array<
| [Directive | undefined] | [Directive | undefined]
| [Directive | undefined, value: any] | [Directive | undefined, value: any]
| [Directive | undefined, value: any, argument: string] | [Directive | undefined, value: any, argument: string]
| [
Directive | undefined,
value: any,
argument: string,
modifiers: DirectiveModifiers,
]
> >
export function withDirectives<T extends Node>( export function withDirectives<T extends Node>(
@ -56,7 +61,7 @@ export function withDirectives<T extends Node>(
const bindings = currentInstance.dirs.get(node)! const bindings = currentInstance.dirs.get(node)!
for (const directive of directives) { for (const directive of directives) {
let [dir, value, arg] = directive let [dir, value, arg, modifiers] = directive
if (!dir) continue if (!dir) continue
if (isFunction(dir)) { if (isFunction(dir)) {
// TODO function directive // TODO function directive
@ -71,6 +76,7 @@ export function withDirectives<T extends Node>(
value, value,
oldValue: void 0, oldValue: void 0,
arg, arg,
modifiers,
} }
if (dir.created) dir.created(node, binding) if (dir.created) dir.created(node, binding)
bindings.push(binding) bindings.push(binding)

View File

@ -21,5 +21,5 @@ const vDirective: ObjectDirective<HTMLDivElement, undefined> = {
</script> </script>
<template> <template>
<div v-directive v-text="text" /> <div v-directive:foo.bar="text" v-text="text" />
</template> </template>