feat: v-show

This commit is contained in:
三咲智子 Kevin Deng 2023-12-08 17:34:33 +08:00
parent 71bc13575f
commit 5f90f8bef0
No known key found for this signature in database
GPG Key ID: 69992F2250DFD93E
11 changed files with 141 additions and 48 deletions

View File

@ -35,7 +35,7 @@ export function render(_ctx) {
const t0 = _template("<div></div>")
const n0 = t0()
const { 0: [n1],} = _children(n0)
_withDirectives(n1, [[_ctx.vExample, _ctx.msg]])
_withDirectives(n1, [[_ctx.vExample, () => _ctx.msg]])
return n0
}"
`;
@ -47,7 +47,7 @@ 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]])
_withDirectives(n1, [[_ctx.vExample, () => _ctx.msg, _ctx.foo]])
return n0
}"
`;
@ -59,7 +59,7 @@ 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 }]])
_withDirectives(n1, [[_ctx.vExample, () => _ctx.msg, void 0, { bar: true }]])
return n0
}"
`;
@ -83,7 +83,7 @@ export function render(_ctx) {
const t0 = _template("<div></div>")
const n0 = t0()
const { 0: [n1],} = _children(n0)
_withDirectives(n1, [[_ctx.vExample, _ctx.msg, "foo"]])
_withDirectives(n1, [[_ctx.vExample, () => _ctx.msg, "foo"]])
return n0
}"
`;
@ -95,7 +95,7 @@ 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 }]])
_withDirectives(n1, [[_ctx.vExample, () => _ctx.msg, "foo", { bar: true }]])
return n0
}"
`;

View File

@ -20,6 +20,7 @@ import { transformVHtml } from './transforms/vHtml'
import { transformVText } from './transforms/vText'
import { transformVBind } from './transforms/vBind'
import { transformVOn } from './transforms/vOn'
import { transformVShow } from './transforms/vShow'
import { transformInterpolation } from './transforms/transformInterpolation'
import type { HackOptions } from './ir'
@ -97,6 +98,7 @@ export function getBaseTransformPreset(
on: transformVOn,
html: transformVHtml,
text: transformVText,
show: transformVShow,
},
]
}

View File

@ -30,7 +30,7 @@ import {
IRNodeTypes,
} from './ir'
import { SourceMapGenerator } from 'source-map-js'
import { camelize, isString } from '@vue/shared'
import { camelize, isString, makeMap } from '@vue/shared'
import type { Identifier } from '@babel/types'
// remove when stable
@ -475,16 +475,20 @@ function genWithDirective(oper: WithDirectiveIRNode, context: CodegenContext) {
// TODO merge directive for the same node
pushWithNewline(`${vaporHelper('withDirectives')}(n${oper.element}, [[`)
// TODO resolve directive
const directiveReference = camelize(`v-${dir.name}`)
if (bindingMetadata[directiveReference]) {
const directiveExpression = createSimpleExpression(directiveReference)
directiveExpression.ast = null
genExpression(directiveExpression, context)
if (dir.name === 'show') {
push(vaporHelper('vShow'))
} else {
const directiveReference = camelize(`v-${dir.name}`)
// TODO resolve directive
if (bindingMetadata[directiveReference]) {
const directiveExpression = createSimpleExpression(directiveReference)
directiveExpression.ast = null
genExpression(directiveExpression, context)
}
}
if (dir.exp) {
push(', ')
push(', () => ')
genExpression(dir.exp, context)
} else if (dir.arg || dir.modifiers.length) {
push(', void 0')
@ -512,6 +516,8 @@ function genArrayExpression(elements: string[]) {
return `[${elements.map((it) => JSON.stringify(it)).join(', ')}]`
}
const isLiteralWhitelisted = /*#__PURE__*/ makeMap('true,false,null,this')
function genExpression(node: IRExpression, context: CodegenContext): void {
const { push } = context
if (isString(node)) return push(node)
@ -525,7 +531,8 @@ function genExpression(node: IRExpression, context: CodegenContext): void {
!context.prefixIdentifiers ||
!node.content.trim() ||
// there was a parsing error
ast === false
ast === false ||
isLiteralWhitelisted(rawExpr)
) {
return push(rawExpr, NewlineType.None, loc)
}

View File

@ -74,7 +74,7 @@ function transformProp(
type: IRNodeTypes.WITH_DIRECTIVE,
element: context.reference(),
dir: prop,
loc: loc,
loc,
})
}
}

View File

@ -0,0 +1,19 @@
import { DOMErrorCodes, createDOMCompilerError } from '@vue/compiler-dom'
import type { DirectiveTransform } from '../transform'
import { IRNodeTypes } from '../ir'
export const transformVShow: DirectiveTransform = (dir, node, context) => {
const { exp, loc } = dir
if (!exp) {
context.options.onError(
createDOMCompilerError(DOMErrorCodes.X_V_SHOW_NO_EXPRESSION, loc),
)
}
context.registerOperation({
type: IRNodeTypes.WITH_DIRECTIVE,
element: context.reference(),
dir,
loc,
})
}

View File

@ -1,6 +1,6 @@
import { EffectScope } from '@vue/reactivity'
import { Block } from './render'
import { DirectiveBinding } from './directives'
import { type Ref, EffectScope, ref } from '@vue/reactivity'
import type { Block } from './render'
import type { DirectiveBinding } from './directive'
import type { Data } from '@vue/shared'
export type SetupFn = (props: any, ctx: any) => Block | Data
@ -19,7 +19,8 @@ export interface ComponentInternalInstance {
scope: EffectScope
component: FunctionalComponent | ObjectComponent
isMounted: boolean
get isMounted(): boolean
isMountedRef: Ref<boolean>
/** directives */
dirs: Map<Node, DirectiveBinding[]>
@ -44,6 +45,7 @@ let uid = 0
export const createComponentInstance = (
component: ObjectComponent | FunctionalComponent,
): ComponentInternalInstance => {
const isMountedRef = ref(false)
const instance: ComponentInternalInstance = {
uid: uid++,
block: null,
@ -51,7 +53,10 @@ export const createComponentInstance = (
scope: new EffectScope(true /* detached */)!,
component,
isMounted: false,
get isMounted() {
return isMountedRef.value
},
isMountedRef,
dirs: new Map(),
// TODO: registory of provides, appContext, lifecycles, ...

View File

@ -1,5 +1,6 @@
import { isFunction } from '@vue/shared'
import { currentInstance, type ComponentInternalInstance } from './component'
import { effect } from './scheduler'
export type DirectiveModifiers<M extends string = string> = Record<M, boolean>
@ -9,6 +10,7 @@ export interface DirectiveBinding<
M extends string = string,
> {
instance: ComponentInternalInstance | null
source?: () => V
value: V
oldValue: V | null
arg?: A
@ -26,23 +28,24 @@ export type DirectiveHook<
// create node -> `created` -> node operation -> `beforeMount` -> node mounted -> `mounted`
// effect update -> `beforeUpdate` -> node updated -> `updated`
// `beforeUnmount`-> node unmount -> `unmounted`
export interface ObjectDirective<
export type DirectiveHookName =
| 'created'
| 'beforeMount'
| 'mounted'
// | 'beforeUpdate'
| 'updated'
| 'beforeUnmount'
| 'unmounted'
export type ObjectDirective<
T = any,
V = any,
A = string,
M extends string = string,
> {
created?: DirectiveHook<T, V, A, M>
beforeMount?: DirectiveHook<T, V, A, M>
mounted?: DirectiveHook<T, V, A, M>
// beforeUpdate?: DirectiveHook<T, V,A,M>
// updated?: DirectiveHook<T, V,A,M>
beforeUnmount?: DirectiveHook<T, V, A, M>
unmounted?: DirectiveHook<T, V, A, M>
// getSSRProps?: SSRDirectiveHook
// deep?: boolean
> = {
[K in DirectiveHookName]?: DirectiveHook<T, V, A, M> | undefined
} & {
deep?: boolean
}
export type DirectiveHookName = Exclude<keyof ObjectDirective, 'deep'>
export type FunctionDirective<
T = any,
@ -60,11 +63,11 @@ export type Directive<
export type DirectiveArguments = Array<
| [Directive | undefined]
| [Directive | undefined, value: any]
| [Directive | undefined, value: any, argument: string]
| [Directive | undefined, () => any]
| [Directive | undefined, () => any, argument: string]
| [
Directive | undefined,
value: any,
value: () => any,
argument: string,
modifiers: DirectiveModifiers,
]
@ -83,7 +86,7 @@ export function withDirectives<T extends Node>(
const bindings = currentInstance.dirs.get(node)!
for (const directive of directives) {
let [dir, value, arg, modifiers] = directive
let [dir, source, arg, modifiers] = directive
if (!dir) continue
if (isFunction(dir)) {
// TODO function directive
@ -95,13 +98,19 @@ export function withDirectives<T extends Node>(
const binding: DirectiveBinding = {
dir,
instance: currentInstance,
value,
oldValue: void 0,
source,
value: null, // set later
oldValue: null,
arg,
modifiers,
}
if (dir.created) dir.created(node, binding)
bindings.push(binding)
callDirectiveHook(node, binding, 'created')
effect(() => {
if (!currentInstance!.isMountedRef.value) return
callDirectiveHook(node, binding, 'updated')
})
}
return node
@ -113,14 +122,28 @@ export function invokeDirectiveHook(
nodes?: IterableIterator<Node>,
) {
if (!instance) return
if (!nodes) {
nodes = instance.dirs.keys()
}
nodes = nodes || instance.dirs.keys()
for (const node of nodes) {
const directives = instance.dirs.get(node) || []
for (const binding of directives) {
const hook = binding.dir[name]
hook && hook(node, binding)
callDirectiveHook(node, binding, name)
}
}
}
function callDirectiveHook(
node: Node,
binding: DirectiveBinding,
name: DirectiveHookName,
) {
const { dir } = binding
const hook = dir[name]
if (!hook) return
const newValue = binding.source ? binding.source() : undefined
if (name === 'updated' && binding.value === newValue) return
binding.oldValue = binding.value
binding.value = newValue
hook(node, binding)
}

View File

@ -0,0 +1,23 @@
import type { ObjectDirective } from '../directive'
const vShowMap = new WeakMap<HTMLElement, string>()
export const vShow: ObjectDirective<HTMLElement> = {
beforeMount(node, { source: value }) {
vShowMap.set(node, node.style.display === 'none' ? '' : node.style.display)
setDisplay(node, value)
},
updated(node, { value, oldValue }) {
if (!value === !oldValue) return
setDisplay(node, value)
},
beforeUnmount(node, { source: value }) {
setDisplay(node, value)
},
}
function setDisplay(el: HTMLElement, value: unknown): void {
el.style.display = value ? vShowMap.get(el)! : 'none'
}

View File

@ -41,5 +41,6 @@ export * from './on'
export * from './render'
export * from './template'
export * from './scheduler'
export * from './directives'
export * from './directive'
export * from './dom'
export * from './directives/vShow'

View File

@ -7,7 +7,7 @@ import {
setCurrentInstance,
unsetCurrentInstance,
} from './component'
import { invokeDirectiveHook } from './directives'
import { invokeDirectiveHook } from './directive'
import { insert, remove } from './dom'
export type Block = Node | Fragment | Block[]
@ -56,7 +56,7 @@ export function mountComponent(
invokeDirectiveHook(instance, 'beforeMount')
insert(block, instance.container)
instance.isMounted = true
instance.isMountedRef.value = true
invokeDirectiveHook(instance, 'mounted')
// TODO: lifecycle hooks (mounted, ...)
@ -70,7 +70,7 @@ export function unmountComponent(instance: ComponentInternalInstance) {
invokeDirectiveHook(instance, 'beforeUnmount')
scope.stop()
block && remove(block, container)
instance.isMounted = false
instance.isMountedRef.value = false
invokeDirectiveHook(instance, 'unmounted')
unsetCurrentInstance()

13
playground/src/show.vue Normal file
View File

@ -0,0 +1,13 @@
<script setup lang="ts">
import { ref } from '@vue/vapor'
const visible = ref(true)
function handleClick() {
visible.value = !visible.value
}
</script>
<template>
<button @click="handleClick">toggle</button>
<h1 v-show="visible">hello world</h1>
</template>