]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat: v-show
author三咲智子 Kevin Deng <sxzz@sxzz.moe>
Fri, 8 Dec 2023 09:34:33 +0000 (17:34 +0800)
committer三咲智子 Kevin Deng <sxzz@sxzz.moe>
Fri, 8 Dec 2023 09:37:21 +0000 (17:37 +0800)
packages/compiler-vapor/__tests__/__snapshots__/compile.test.ts.snap
packages/compiler-vapor/src/compile.ts
packages/compiler-vapor/src/generate.ts
packages/compiler-vapor/src/transforms/transformElement.ts
packages/compiler-vapor/src/transforms/vShow.ts [new file with mode: 0644]
packages/runtime-vapor/src/component.ts
packages/runtime-vapor/src/directive.ts [moved from packages/runtime-vapor/src/directives.ts with 65% similarity]
packages/runtime-vapor/src/directives/vShow.ts [new file with mode: 0644]
packages/runtime-vapor/src/index.ts
packages/runtime-vapor/src/render.ts
playground/src/show.vue [new file with mode: 0644]

index 11119db643b58ecd645a5b196cb88cb28443c81b..5e3b020fa497c0e0bf1733719149405b42a75023 100644 (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
 }"
 `;
index e386de3b3944e5d782bb52d4664e8c32f13edc4a..0d37278d2c0fed3f7fa2bdbe83591809014c3875 100644 (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,
     },
   ]
 }
index fee38022b12546411080ddbbde0e493579e319f7..bdb4f882098d17f5635f639f6baca3e5943cf313 100644 (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)
   }
index ea468d29badf8804ee231ef61e5cf023d5b99dba..4bca80478d621ca371d1b6574079a2030ffb746b 100644 (file)
@@ -74,7 +74,7 @@ function transformProp(
       type: IRNodeTypes.WITH_DIRECTIVE,
       element: context.reference(),
       dir: prop,
-      loc: loc,
+      loc,
     })
   }
 }
diff --git a/packages/compiler-vapor/src/transforms/vShow.ts b/packages/compiler-vapor/src/transforms/vShow.ts
new file mode 100644 (file)
index 0000000..1604b27
--- /dev/null
@@ -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,
+  })
+}
index 421b2bdfb30503ed85b959fb2636c7f511baea41..26518598d4ff0e9812b586c87ee35a4138398b25 100644 (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, ...
similarity index 65%
rename from packages/runtime-vapor/src/directives.ts
rename to packages/runtime-vapor/src/directive.ts
index 70833e97320a5705928a55d28aab3c58b623b157..2eeb9277c294906bc39888f5b3d7105f9bd0aad7 100644 (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)
+}
diff --git a/packages/runtime-vapor/src/directives/vShow.ts b/packages/runtime-vapor/src/directives/vShow.ts
new file mode 100644 (file)
index 0000000..f419053
--- /dev/null
@@ -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'
+}
index 63985f7ec0af0175512691bb5be5d15292b40778..0c56476ad8b9bcc06b3caf9711358f8d1e4c2316 100644 (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'
index 74e88eaceb470e89973d3cd37769685d4befaf9b..b03e56ae8e32426b04d60c44e853c462b5e86934 100644 (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()
 
diff --git a/playground/src/show.vue b/playground/src/show.vue
new file mode 100644 (file)
index 0000000..23b5991
--- /dev/null
@@ -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>