wip(vapor): simplified builtin directive v-show

This commit is contained in:
Evan You 2025-01-31 22:18:32 +08:00
parent 9f1025d854
commit e5af194486
No known key found for this signature in database
GPG Key ID: 00E9AB7A6704CE0A
16 changed files with 214 additions and 80 deletions

View File

@ -12,20 +12,20 @@ export function render(_ctx, $props, $emit, $attrs, $slots) {
`;
exports[`compile > custom directive > basic 1`] = `
"import { resolveDirective as _resolveDirective, withDirectives as _withDirectives, template as _template } from 'vue';
"import { resolveDirective as _resolveDirective, withVaporDirectives as _withVaporDirectives, template as _template } from 'vue';
const t0 = _template("<div></div>", true)
export function render(_ctx) {
const _directive_test = _resolveDirective("test")
const _directive_hello = _resolveDirective("hello")
const n0 = t0()
_withDirectives(n0, [[_directive_test], [_directive_hello, void 0, void 0, { world: true }]])
_withVaporDirectives(n0, [[_directive_test], [_directive_hello, void 0, void 0, { world: true }]])
return n0
}"
`;
exports[`compile > custom directive > component 1`] = `
"import { resolveComponent as _resolveComponent, resolveDirective as _resolveDirective, createComponentWithFallback as _createComponentWithFallback, withDirectives as _withDirectives, insert as _insert, createIf as _createIf, template as _template } from 'vue';
"import { resolveComponent as _resolveComponent, resolveDirective as _resolveDirective, createComponentWithFallback as _createComponentWithFallback, withVaporDirectives as _withVaporDirectives, insert as _insert, createIf as _createIf, template as _template } from 'vue';
const t0 = _template("<div></div>")
export function render(_ctx) {
@ -38,91 +38,91 @@ export function render(_ctx) {
const n0 = _createIf(() => (true), () => {
const n3 = t0()
const n2 = _createComponentWithFallback(_component_Bar)
_withDirectives(n2, [[_directive_hello, void 0, void 0, { world: true }]])
_withVaporDirectives(n2, [[_directive_hello, void 0, void 0, { world: true }]])
_insert(n2, n3)
return n3
})
return n0
}
}, true)
_withDirectives(n4, [[_directive_test]])
_withVaporDirectives(n4, [[_directive_test]])
return n4
}"
`;
exports[`compile > directives > custom directive > basic 1`] = `
"import { withDirectives as _withDirectives, template as _template } from 'vue';
"import { withVaporDirectives as _withVaporDirectives, template as _template } from 'vue';
const t0 = _template("<div></div>", true)
export function render(_ctx, $props, $emit, $attrs, $slots) {
const n0 = t0()
_withDirectives(n0, [[_ctx.vExample]])
_withVaporDirectives(n0, [[_ctx.vExample]])
return n0
}"
`;
exports[`compile > directives > custom directive > binding value 1`] = `
"import { withDirectives as _withDirectives, template as _template } from 'vue';
"import { withVaporDirectives as _withVaporDirectives, template as _template } from 'vue';
const t0 = _template("<div></div>", true)
export function render(_ctx, $props, $emit, $attrs, $slots) {
const n0 = t0()
_withDirectives(n0, [[_ctx.vExample, () => _ctx.msg]])
_withVaporDirectives(n0, [[_ctx.vExample, () => _ctx.msg]])
return n0
}"
`;
exports[`compile > directives > custom directive > dynamic parameters 1`] = `
"import { withDirectives as _withDirectives, template as _template } from 'vue';
"import { withVaporDirectives as _withVaporDirectives, template as _template } from 'vue';
const t0 = _template("<div></div>", true)
export function render(_ctx, $props, $emit, $attrs, $slots) {
const n0 = t0()
_withDirectives(n0, [[_ctx.vExample, () => _ctx.msg, _ctx.foo]])
_withVaporDirectives(n0, [[_ctx.vExample, () => _ctx.msg, _ctx.foo]])
return n0
}"
`;
exports[`compile > directives > custom directive > modifiers 1`] = `
"import { withDirectives as _withDirectives, template as _template } from 'vue';
"import { withVaporDirectives as _withVaporDirectives, template as _template } from 'vue';
const t0 = _template("<div></div>", true)
export function render(_ctx, $props, $emit, $attrs, $slots) {
const n0 = t0()
_withDirectives(n0, [[_ctx.vExample, () => _ctx.msg, void 0, { bar: true }]])
_withVaporDirectives(n0, [[_ctx.vExample, () => _ctx.msg, void 0, { bar: true }]])
return n0
}"
`;
exports[`compile > directives > custom directive > modifiers w/o binding 1`] = `
"import { withDirectives as _withDirectives, template as _template } from 'vue';
"import { withVaporDirectives as _withVaporDirectives, template as _template } from 'vue';
const t0 = _template("<div></div>", true)
export function render(_ctx, $props, $emit, $attrs, $slots) {
const n0 = t0()
_withDirectives(n0, [[_ctx.vExample, void 0, void 0, { "foo-bar": true }]])
_withVaporDirectives(n0, [[_ctx.vExample, void 0, void 0, { "foo-bar": true }]])
return n0
}"
`;
exports[`compile > directives > custom directive > static parameters 1`] = `
"import { withDirectives as _withDirectives, template as _template } from 'vue';
"import { withVaporDirectives as _withVaporDirectives, template as _template } from 'vue';
const t0 = _template("<div></div>", true)
export function render(_ctx, $props, $emit, $attrs, $slots) {
const n0 = t0()
_withDirectives(n0, [[_ctx.vExample, () => _ctx.msg, "foo"]])
_withVaporDirectives(n0, [[_ctx.vExample, () => _ctx.msg, "foo"]])
return n0
}"
`;
exports[`compile > directives > custom directive > static parameters and modifiers 1`] = `
"import { withDirectives as _withDirectives, template as _template } from 'vue';
"import { withVaporDirectives as _withVaporDirectives, template as _template } from 'vue';
const t0 = _template("<div></div>", true)
export function render(_ctx, $props, $emit, $attrs, $slots) {
const n0 = t0()
_withDirectives(n0, [[_ctx.vExample, () => _ctx.msg, "foo", { bar: true }]])
_withVaporDirectives(n0, [[_ctx.vExample, () => _ctx.msg, "foo", { bar: true }]])
return n0
}"
`;

View File

@ -1,12 +1,12 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`compiler: v-show transform > simple expression 1`] = `
"import { vShow as _vShow, withDirectives as _withDirectives, template as _template } from 'vue';
"import { applyVShow as _applyVShow, template as _template } from 'vue';
const t0 = _template("<div></div>", true)
export function render(_ctx) {
const n0 = t0()
_withDirectives(n0, [[_vShow, () => _ctx.foo]])
_applyVShow(n0, () => (_ctx.foo))
return n0
}"
`;

View File

@ -14,7 +14,7 @@ const compileWithVModel = makeCompile({
},
})
describe('compiler: vModel transform', () => {
describe.todo('compiler: vModel transform', () => {
test('should support simple expression', () => {
const { code, helpers } = compileWithVModel('<input v-model="model" />')
expect(code).toMatchSnapshot()

View File

@ -14,39 +14,64 @@ import {
genCall,
genMulti,
} from './utils'
import {
IRNodeTypes,
type OperationNode,
type WithDirectiveIRNode,
} from '../ir'
import { type DirectiveIRNode, IRNodeTypes, type OperationNode } from '../ir'
import { genVShow } from './vShow'
import { genVModel } from './vModel'
export function genBuiltinDirective(
oper: DirectiveIRNode,
context: CodegenContext,
): CodeFragment[] {
switch (oper.name) {
case 'show':
return genVShow(oper, context)
case 'model':
return genVModel(oper, context)
default:
return []
}
}
/**
* user directives via `withVaporDirectives`
* TODO the compiler side is implemented but no runtime support yet
* it was removed due to perf issues
*/
export function genDirectivesForElement(
id: number,
context: CodegenContext,
): CodeFragment[] {
const dirs = filterDirectives(id, context.block.operation)
return dirs.length ? genWithDirective(dirs, context) : []
const dirs = filterCustomDirectives(id, context.block.operation)
return dirs.length ? genCustomDirectives(dirs, context) : []
}
export function genWithDirective(
opers: WithDirectiveIRNode[],
function genCustomDirectives(
opers: DirectiveIRNode[],
context: CodegenContext,
): CodeFragment[] {
const { helper } = context
const element = `n${opers[0].element}`
const directiveItems = opers.map(genDirective)
const directiveItems = opers.map(genDirectiveItem)
const directives = genMulti(DELIMITERS_ARRAY, ...directiveItems)
return [NEWLINE, ...genCall(helper('withDirectives'), element, directives)]
return [
NEWLINE,
// @ts-expect-error
...genCall(helper('withVaporDirectives'), element, directives),
]
function genDirective({
function genDirectiveItem({
dir,
name,
builtin,
asset,
}: WithDirectiveIRNode): CodeFragment[] {
const directive = genDirective()
}: DirectiveIRNode): CodeFragment[] {
const directiveVar = asset
? toValidAssetId(name, 'directive')
: genExpression(
extend(createSimpleExpression(name, false), { ast: null }),
context,
)
const value = dir.exp && ['() => ', ...genExpression(dir.exp, context)]
const argument = dir.arg && genExpression(dir.arg, context)
const modifiers = !!dir.modifiers.length && [
@ -57,24 +82,11 @@ export function genWithDirective(
return genMulti(
DELIMITERS_ARRAY.concat('void 0') as CodeFragmentDelimiters,
directive,
directiveVar,
value,
argument,
modifiers,
)
function genDirective() {
if (builtin) {
return helper(name as any)
} else if (asset) {
return toValidAssetId(name, 'directive')
} else {
return genExpression(
extend(createSimpleExpression(name, false), { ast: null }),
context,
)
}
}
}
}
@ -87,12 +99,14 @@ export function genDirectiveModifiers(modifiers: string[]): string {
.join(', ')
}
function filterDirectives(
function filterCustomDirectives(
id: number,
operations: OperationNode[],
): WithDirectiveIRNode[] {
): DirectiveIRNode[] {
return operations.filter(
(oper): oper is WithDirectiveIRNode =>
oper.type === IRNodeTypes.WITH_DIRECTIVE && oper.element === id,
(oper): oper is DirectiveIRNode =>
oper.type === IRNodeTypes.DIRECTIVE &&
oper.element === id &&
!oper.builtin,
)
}

View File

@ -19,6 +19,7 @@ import {
import { genCreateComponent } from './component'
import { genSlotOutlet } from './slotOutlet'
import { processExpressions } from './expression'
import { genBuiltinDirective } from './directive'
export function genOperations(
opers: OperationNode[],
@ -68,8 +69,8 @@ export function genOperation(
return genDeclareOldRef(oper)
case IRNodeTypes.SLOT_OUTLET_NODE:
return genSlotOutlet(oper, context)
case IRNodeTypes.WITH_DIRECTIVE:
return [] // TODO
case IRNodeTypes.DIRECTIVE:
return genBuiltinDirective(oper, context)
default:
const exhaustiveCheck: never = oper
throw new Error(

View File

@ -0,0 +1,48 @@
import type { CodegenContext } from '../generate'
import type { DirectiveIRNode } from '../ir'
import type { CodeFragment } from './utils'
export function genVModel(
oper: DirectiveIRNode,
context: CodegenContext,
): CodeFragment[] {
return []
}
import { camelize } from '@vue/shared'
import { genExpression } from './expression'
import type { SetModelValueIRNode } from '../ir'
import { NEWLINE, genCall } from './utils'
import type { SimpleExpressionNode } from '@vue/compiler-dom'
export function genSetModelValue(
oper: SetModelValueIRNode,
context: CodegenContext,
): CodeFragment[] {
const { helper } = context
const name = oper.key.isStatic
? [JSON.stringify(`update:${camelize(oper.key.content)}`)]
: ['`update:${', ...genExpression(oper.key, context), '}`']
const handler = genModelHandler(oper.value, context)
return [
NEWLINE,
...genCall(helper('delegate'), `n${oper.element}`, name, handler),
]
}
export function genModelHandler(
value: SimpleExpressionNode,
context: CodegenContext,
): CodeFragment[] {
const {
options: { isTS },
} = context
return [
`() => ${isTS ? `($event: any)` : `$event`} => (`,
...genExpression(value, context, '$event'),
')',
]
}

View File

@ -0,0 +1,18 @@
import type { CodegenContext } from '../generate'
import type { DirectiveIRNode } from '../ir'
import { genExpression } from './expression'
import { type CodeFragment, NEWLINE, genCall } from './utils'
export function genVShow(
oper: DirectiveIRNode,
context: CodegenContext,
): CodeFragment[] {
return [
NEWLINE,
...genCall(context.helper('applyVShow'), `n${oper.element}`, [
`() => (`,
...genExpression(oper.dir.exp!, context),
`)`,
]),
]
}

View File

@ -31,7 +31,7 @@ export enum IRNodeTypes {
CREATE_COMPONENT_NODE,
SLOT_OUTLET_NODE,
WITH_DIRECTIVE,
DIRECTIVE,
DECLARE_OLD_REF, // consider make it more general
IF,
@ -183,13 +183,14 @@ export interface PrependNodeIRNode extends BaseIRNode {
parent: number
}
export interface WithDirectiveIRNode extends BaseIRNode {
type: IRNodeTypes.WITH_DIRECTIVE
export interface DirectiveIRNode extends BaseIRNode {
type: IRNodeTypes.DIRECTIVE
element: number
dir: VaporDirectiveNode
name: string
builtin?: boolean
asset?: boolean
modelType?: 'text' | 'dynamic' | 'radio' | 'checkbox' | 'select'
}
export interface CreateComponentIRNode extends BaseIRNode {
@ -230,7 +231,7 @@ export type OperationNode =
| CreateTextNodeIRNode
| InsertNodeIRNode
| PrependNodeIRNode
| WithDirectiveIRNode
| DirectiveIRNode
| IfIRNode
| ForIRNode
| CreateComponentIRNode

View File

@ -381,7 +381,7 @@ function transformProp(
}
context.registerOperation({
type: IRNodeTypes.WITH_DIRECTIVE,
type: IRNodeTypes.DIRECTIVE,
element: context.reference(),
dir: prop,
name,

View File

@ -13,11 +13,11 @@ import {
import type { NodeTransform, TransformContext } from '../transform'
import {
type BlockIRNode,
type DirectiveIRNode,
DynamicFlag,
IRNodeTypes,
type IRProps,
type VaporDirectiveNode,
type WithDirectiveIRNode,
} from '../ir'
import { camelize, extend } from '@vue/shared'
import { newBlock } from './utils'
@ -85,8 +85,8 @@ export const transformSlotOutlet: NodeTransform = (node, context) => {
irProps = isDynamic ? props : [props]
const runtimeDirective = context.block.operation.find(
(oper): oper is WithDirectiveIRNode =>
oper.type === IRNodeTypes.WITH_DIRECTIVE && oper.element === id,
(oper): oper is DirectiveIRNode =>
oper.type === IRNodeTypes.DIRECTIVE && oper.element === id,
)
if (runtimeDirective) {
context.options.onError(

View File

@ -14,7 +14,7 @@ import {
isStaticArgOf,
} from '@vue/compiler-dom'
import type { DirectiveTransform } from '../transform'
import { IRNodeTypes } from '../ir'
import { type DirectiveIRNode, IRNodeTypes } from '../ir'
export const transformVModel: DirectiveTransform = (dir, node, context) => {
const { exp, arg } = dir
@ -79,7 +79,7 @@ export const transformVModel: DirectiveTransform = (dir, node, context) => {
)
const { tag } = node
const isCustomElement = context.options.isCustomElement(tag)
let runtimeDirective: string | undefined = 'vModelText'
let modelType: DirectiveIRNode['modelType'] | undefined = 'text'
// TODO let runtimeDirective: VaporHelper | undefined = 'vModelText'
if (
tag === 'input' ||
@ -92,17 +92,17 @@ export const transformVModel: DirectiveTransform = (dir, node, context) => {
if (type) {
if (type.type === NodeTypes.DIRECTIVE) {
// :type="foo"
runtimeDirective = 'vModelDynamic'
modelType = 'dynamic'
} else if (type.value) {
switch (type.value.content) {
case 'radio':
runtimeDirective = 'vModelRadio'
modelType = 'radio'
break
case 'checkbox':
runtimeDirective = 'vModelCheckbox'
modelType = 'checkbox'
break
case 'file':
runtimeDirective = undefined
modelType = undefined
context.options.onError(
createDOMCompilerError(
DOMErrorCodes.X_V_MODEL_ON_FILE_INPUT_ELEMENT,
@ -119,13 +119,13 @@ export const transformVModel: DirectiveTransform = (dir, node, context) => {
} else if (hasDynamicKeyVBind(node)) {
// element has bindings with dynamic keys, which can possibly contain
// "type".
runtimeDirective = 'vModelDynamic'
modelType = 'dynamic'
} else {
// text type
__DEV__ && checkDuplicatedValue()
}
} else if (tag === 'select') {
runtimeDirective = 'vModelSelect'
modelType = 'select'
} else {
// textarea
__DEV__ && checkDuplicatedValue()
@ -139,6 +139,7 @@ export const transformVModel: DirectiveTransform = (dir, node, context) => {
)
}
// TODO this should no longer be needed
context.registerOperation({
type: IRNodeTypes.SET_MODEL_VALUE,
element: context.reference(),
@ -147,12 +148,13 @@ export const transformVModel: DirectiveTransform = (dir, node, context) => {
isComponent,
})
if (runtimeDirective)
if (modelType)
context.registerOperation({
type: IRNodeTypes.WITH_DIRECTIVE,
type: IRNodeTypes.DIRECTIVE,
element: context.reference(),
dir,
name: runtimeDirective,
name: 'model',
modelType,
builtin: true,
})

View File

@ -1,4 +1,10 @@
import { DOMErrorCodes, createDOMCompilerError } from '@vue/compiler-dom'
import {
DOMErrorCodes,
ElementTypes,
ErrorCodes,
createCompilerError,
createDOMCompilerError,
} from '@vue/compiler-dom'
import type { DirectiveTransform } from '../transform'
import { IRNodeTypes } from '../ir'
@ -8,13 +14,24 @@ export const transformVShow: DirectiveTransform = (dir, node, context) => {
context.options.onError(
createDOMCompilerError(DOMErrorCodes.X_V_SHOW_NO_EXPRESSION, loc),
)
return
}
if (node.tagType === ElementTypes.SLOT) {
context.options.onError(
createCompilerError(
ErrorCodes.X_V_SLOT_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET,
loc,
),
)
return
}
context.registerOperation({
type: IRNodeTypes.WITH_DIRECTIVE,
type: IRNodeTypes.DIRECTIVE,
element: context.reference(),
dir,
name: 'vShow',
name: 'show',
builtin: true,
})
}

View File

@ -1,8 +1,16 @@
import type { ObjectDirective } from '@vue/runtime-core'
/**
* @internal
*/
export const vShowOriginalDisplay: unique symbol = Symbol('_vod')
/**
* @internal
*/
export const vShowHidden: unique symbol = Symbol('_vsh')
/**
* @internal
*/
export interface VShowElement extends HTMLElement {
// _vod = vue original display
[vShowOriginalDisplay]: string

View File

@ -322,3 +322,11 @@ export { patchStyle } from './modules/style'
* @internal
*/
export { shouldSetAsProp } from './patchProp'
/**
* @internal
*/
export {
vShowOriginalDisplay,
vShowHidden,
type VShowElement,
} from './directives/vShow'

View File

@ -0,0 +1,16 @@
import {
type VShowElement,
vShowHidden,
vShowOriginalDisplay,
} from '@vue/runtime-dom'
import { renderEffect } from '../renderEffect'
export function applyVShow(el: VShowElement, source: () => any): void {
el[vShowOriginalDisplay] = el.style.display === 'none' ? '' : el.style.display
renderEffect(() => setDisplay(el, source()))
}
function setDisplay(el: VShowElement, value: unknown): void {
el.style.display = value ? el[vShowOriginalDisplay] : 'none'
el[vShowHidden] = !value
}

View File

@ -30,3 +30,4 @@ export {
} from './apiCreateFor'
export { createTemplateRefSetter } from './apiTemplateRef'
export { createDynamicComponent } from './apiCreateDynamicComponent'
export { applyVShow } from './directives/vShow'