Sfoglia il codice sorgente

feat(runtime-vapor): runtime for v-on in component (#178)

Co-authored-by: 三咲智子 Kevin Deng <sxzz@sxzz.moe>
Jevon 2 anni fa
parent
commit
37df043adc

+ 103 - 109
packages/runtime-vapor/__tests__/componentEmits.spec.ts

@@ -3,81 +3,91 @@
 // Note: emits and listener fallthrough is tested in
 // ./rendererAttrsFallthrough.spec.ts.
 
-import { nextTick, onBeforeUnmount } from '../src'
+import { toHandlers } from '@vue/runtime-core'
+import {
+  createComponent,
+  defineComponent,
+  nextTick,
+  onBeforeUnmount,
+} from '../src'
 import { isEmitListener } from '../src/componentEmits'
 import { makeRender } from './_utils'
 
-const define = makeRender<any>()
+const define = makeRender()
 
-describe.todo('component: emit', () => {
+describe('component: emit', () => {
   test('trigger handlers', () => {
     const { render } = define({
-      render() {},
-      setup(_: any, { emit }: any) {
+      setup(_, { emit }) {
         emit('foo')
         emit('bar')
         emit('!baz')
       },
     })
-    const onfoo = vi.fn()
+    const onFoo = vi.fn()
     const onBar = vi.fn()
     const onBaz = vi.fn()
     render({
-      get onfoo() {
-        return onfoo
-      },
-      get onBar() {
-        return onBar
-      },
-      get ['on!baz']() {
-        return onBaz
+      onfoo: () => onFoo,
+      onBar: () => onBar,
+      ['on!baz']: () => onBaz,
+    })
+
+    expect(onFoo).not.toHaveBeenCalled()
+    expect(onBar).toHaveBeenCalled()
+    expect(onBaz).toHaveBeenCalled()
+  })
+
+  test('trigger dynamic emits', () => {
+    const { render } = define({
+      setup(_, { emit }) {
+        emit('foo')
+        emit('bar')
+        emit('!baz')
       },
     })
+    const onFoo = vi.fn()
+    const onBar = vi.fn()
+    const onBaz = vi.fn()
+    render(() => ({
+      onfoo: onFoo,
+      onBar,
+      ['on!baz']: onBaz,
+    }))
 
-    expect(onfoo).not.toHaveBeenCalled()
+    expect(onFoo).not.toHaveBeenCalled()
     expect(onBar).toHaveBeenCalled()
     expect(onBaz).toHaveBeenCalled()
   })
 
   test('trigger camelCase handler', () => {
     const { render } = define({
-      render() {},
-      setup(_: any, { emit }: any) {
+      setup(_, { emit }) {
         emit('test-event')
       },
     })
 
     const fooSpy = vi.fn()
-    render({
-      get onTestEvent() {
-        return fooSpy
-      },
-    })
+    render({ onTestEvent: () => fooSpy })
     expect(fooSpy).toHaveBeenCalled()
   })
 
   test('trigger kebab-case handler', () => {
     const { render } = define({
-      render() {},
-      setup(_: any, { emit }: any) {
+      setup(_, { emit }) {
         emit('test-event')
       },
     })
 
     const fooSpy = vi.fn()
-    render({
-      get ['onTest-event']() {
-        return fooSpy
-      },
-    })
+    render({ ['onTest-event']: () => fooSpy })
     expect(fooSpy).toHaveBeenCalledTimes(1)
   })
 
   // #3527
-  test.todo('trigger mixed case handlers', () => {
+  test('trigger mixed case handlers', () => {
     const { render } = define({
-      render() {},
-      setup(_: any, { emit }: any) {
+      setup(_, { emit }) {
         emit('test-event')
         emit('testEvent')
       },
@@ -86,15 +96,10 @@ describe.todo('component: emit', () => {
     const fooSpy = vi.fn()
     const barSpy = vi.fn()
     render(
-      // TODO: impl `toHandlers`
-      {
-        get ['onTest-Event']() {
-          return fooSpy
-        },
-        get onTestEvent() {
-          return barSpy
-        },
-      },
+      toHandlers({
+        'test-event': () => fooSpy,
+        testEvent: () => barSpy,
+      }),
     )
     expect(fooSpy).toHaveBeenCalledTimes(1)
     expect(barSpy).toHaveBeenCalledTimes(1)
@@ -103,8 +108,7 @@ describe.todo('component: emit', () => {
   // for v-model:foo-bar usage in DOM templates
   test('trigger hyphenated events for update:xxx events', () => {
     const { render } = define({
-      render() {},
-      setup(_: any, { emit }: any) {
+      setup(_, { emit }) {
         emit('update:fooProp')
         emit('update:barProp')
       },
@@ -113,12 +117,8 @@ describe.todo('component: emit', () => {
     const fooSpy = vi.fn()
     const barSpy = vi.fn()
     render({
-      get ['onUpdate:fooProp']() {
-        return fooSpy
-      },
-      get ['onUpdate:bar-prop']() {
-        return barSpy
-      },
+      ['onUpdate:fooProp']: () => fooSpy,
+      ['onUpdate:bar-prop']: () => barSpy,
     })
 
     expect(fooSpy).toHaveBeenCalled()
@@ -127,8 +127,7 @@ describe.todo('component: emit', () => {
 
   test('should trigger array of listeners', async () => {
     const { render } = define({
-      render() {},
-      setup(_: any, { emit }: any) {
+      setup(_, { emit }) {
         emit('foo', 1)
       },
     })
@@ -136,29 +135,49 @@ describe.todo('component: emit', () => {
     const fn1 = vi.fn()
     const fn2 = vi.fn()
 
-    render({
-      onFoo: () => [fn1, fn2],
-    })
+    render({ onFoo: () => [fn1, fn2] })
     expect(fn1).toHaveBeenCalledTimes(1)
     expect(fn1).toHaveBeenCalledWith(1)
     expect(fn2).toHaveBeenCalledTimes(1)
     expect(fn2).toHaveBeenCalledWith(1)
   })
 
-  test.todo('warning for undeclared event (array)', () => {
-    // TODO: warning
+  test('warning for undeclared event (array)', () => {
+    const { render } = define({
+      emits: ['foo'],
+
+      setup(_, { emit }) {
+        emit('bar')
+      },
+    })
+    render()
+    expect(
+      `Component emitted event "bar" but it is neither declared`,
+    ).toHaveBeenWarned()
   })
 
-  test.todo('warning for undeclared event (object)', () => {
-    // TODO: warning
+  test('warning for undeclared event (object)', () => {
+    const { render } = define({
+      emits: {
+        foo: null,
+      },
+
+      setup(_, { emit }) {
+        emit('bar')
+      },
+    })
+    render()
+    expect(
+      `Component emitted event "bar" but it is neither declared`,
+    ).toHaveBeenWarned()
   })
 
   test('should not warn if has equivalent onXXX prop', () => {
     define({
       props: ['onFoo'],
       emits: [],
-      render() {},
-      setup(_: any, { emit }: any) {
+
+      setup(_, { emit }) {
         emit('foo')
       },
     }).render()
@@ -182,12 +201,11 @@ describe.todo('component: emit', () => {
 
   test('.once', () => {
     const { render } = define({
-      render() {},
       emits: {
         foo: null,
         bar: null,
       },
-      setup(_: any, { emit }: any) {
+      setup(_, { emit }) {
         emit('foo')
         emit('foo')
         emit('bar')
@@ -197,12 +215,8 @@ describe.todo('component: emit', () => {
     const fn = vi.fn()
     const barFn = vi.fn()
     render({
-      get onFooOnce() {
-        return fn
-      },
-      get onBarOnce() {
-        return barFn
-      },
+      onFooOnce: () => fn,
+      onBarOnce: () => barFn,
     })
     expect(fn).toHaveBeenCalledTimes(1)
     expect(barFn).toHaveBeenCalledTimes(1)
@@ -210,11 +224,10 @@ describe.todo('component: emit', () => {
 
   test('.once with normal listener of the same name', () => {
     const { render } = define({
-      render() {},
       emits: {
         foo: null,
       },
-      setup(_: any, { emit }: any) {
+      setup(_, { emit }) {
         emit('foo')
         emit('foo')
       },
@@ -222,12 +235,8 @@ describe.todo('component: emit', () => {
     const onFoo = vi.fn()
     const onFooOnce = vi.fn()
     render({
-      get onFoo() {
-        return onFoo
-      },
-      get onFooOnce() {
-        return onFooOnce
-      },
+      onFoo: () => onFoo,
+      onFooOnce: () => onFooOnce,
     })
     expect(onFoo).toHaveBeenCalledTimes(2)
     expect(onFooOnce).toHaveBeenCalledTimes(1)
@@ -235,8 +244,7 @@ describe.todo('component: emit', () => {
 
   test('.number modifier should work with v-model on component', () => {
     const { render } = define({
-      render() {},
-      setup(_: any, { emit }: any) {
+      setup(_, { emit }) {
         emit('update:modelValue', '1')
         emit('update:foo', '2')
       },
@@ -244,24 +252,12 @@ describe.todo('component: emit', () => {
     const fn1 = vi.fn()
     const fn2 = vi.fn()
     render({
-      modelValue() {
-        return null
-      },
-      modelModifiers() {
-        return { number: true }
-      },
-      ['onUpdate:modelValue']() {
-        return fn1
-      },
-      foo() {
-        return null
-      },
-      fooModifiers() {
-        return { number: true }
-      },
-      ['onUpdate:foo']() {
-        return fn2
-      },
+      modelValue: () => null,
+      modelModifiers: () => ({ number: true }),
+      ['onUpdate:modelValue']: () => fn1,
+      foo: () => null,
+      fooModifiers: () => ({ number: true }),
+      ['onUpdate:foo']: () => fn2,
     })
     expect(fn1).toHaveBeenCalledTimes(1)
     expect(fn1).toHaveBeenCalledWith(1)
@@ -271,8 +267,7 @@ describe.todo('component: emit', () => {
 
   test('.trim modifier should work with v-model on component', () => {
     const { render } = define({
-      render() {},
-      setup(_: any, { emit }: any) {
+      setup(_, { emit }) {
         emit('update:modelValue', ' one ')
         emit('update:foo', '  two  ')
       },
@@ -307,8 +302,7 @@ describe.todo('component: emit', () => {
 
   test('.trim and .number modifiers should work with v-model on component', () => {
     const { render } = define({
-      render() {},
-      setup(_: any, { emit }: any) {
+      setup(_, { emit }) {
         emit('update:modelValue', '    +01.2    ')
         emit('update:foo', '    1    ')
       },
@@ -343,8 +337,7 @@ describe.todo('component: emit', () => {
 
   test('only trim string parameter when work with v-model on component', () => {
     const { render } = define({
-      render() {},
-      setup(_: any, { emit }: any) {
+      setup(_, { emit }) {
         emit('update:modelValue', ' foo ', { bar: ' bar ' })
       },
     })
@@ -395,20 +388,21 @@ describe.todo('component: emit', () => {
 
   test('does not emit after unmount', async () => {
     const fn = vi.fn()
-    const { app } = define({
+
+    const Foo = defineComponent({
       emits: ['closing'],
-      setup(_: any, { emit }: any) {
+      setup(_, { emit }) {
         onBeforeUnmount(async () => {
           await nextTick()
           emit('closing', true)
         })
       },
-      render() {},
-    }).render({
-      get onClosing() {
-        return fn
-      },
     })
+
+    const { app } = define(() =>
+      createComponent(Foo, { onClosing: () => fn }),
+    ).render()
+
     await nextTick()
     app.unmount()
     await nextTick()

+ 4 - 3
packages/runtime-vapor/src/apiCreateVaporApp.ts

@@ -1,4 +1,4 @@
-import { isObject } from '@vue/shared'
+import { isFunction, isObject } from '@vue/shared'
 import {
   type Component,
   type ComponentInternalInstance,
@@ -14,8 +14,9 @@ export function createVaporApp(
   rootComponent: Component,
   rootProps: RawProps | null = null,
 ): App {
-  if (rootProps != null && !isObject(rootProps)) {
-    __DEV__ && warn(`root props passed to app.mount() must be an object.`)
+  if (rootProps != null && !isObject(rootProps) && !isFunction(rootProps)) {
+    __DEV__ &&
+      warn(`root props passed to app.mount() must be an object or function.`)
     rootProps = null
   }
 

+ 69 - 31
packages/runtime-vapor/src/componentEmits.ts

@@ -1,16 +1,11 @@
-// NOTE: runtime-core/src/componentEmits.ts
-
-// TODO WIP
-// @ts-nocheck
-
 import {
-  EMPTY_OBJ,
   type UnionToIntersection,
   camelize,
   extend,
   hasOwn,
   hyphenate,
   isArray,
+  isFunction,
   isOn,
   isString,
   looseToNumber,
@@ -18,6 +13,8 @@ import {
 } from '@vue/shared'
 import type { Component, ComponentInternalInstance } from './component'
 import { VaporErrorCodes, callWithAsyncErrorHandling } from './errorHandling'
+import { type StaticProps, getDynamicPropValue } from './componentProps'
+import { warn } from './warning'
 
 export type ObjectEmitsOptions = Record<
   string,
@@ -48,21 +45,73 @@ export function emit(
   ...rawArgs: any[]
 ) {
   if (instance.isUnmounted) return
-  // TODO
-  // @ts-expect-error
+
+  if (__DEV__) {
+    const {
+      emitsOptions,
+      propsOptions: [propsOptions],
+    } = instance
+    if (emitsOptions) {
+      if (!(event in emitsOptions)) {
+        if (!propsOptions || !(toHandlerKey(event) in propsOptions)) {
+          warn(
+            `Component emitted event "${event}" but it is neither declared in ` +
+              `the emits option nor as an "${toHandlerKey(event)}" prop.`,
+          )
+        }
+      } else {
+        const validator = emitsOptions[event]
+        if (isFunction(validator)) {
+          const isValid = validator(...rawArgs)
+          if (!isValid) {
+            warn(
+              `Invalid event arguments: event validation failed for event "${event}".`,
+            )
+          }
+        }
+      }
+    }
+  }
+
   const { rawProps } = instance
+  const hasDynamicProps = rawProps.some(isFunction)
 
-  let args = rawArgs
-  const isModelListener = event.startsWith('update:')
+  let handlerName: string
+  let handler: any
+  let onceHandler: any
 
-  // for v-model update:xxx events, apply modifiers on args
+  const isModelListener = event.startsWith('update:')
   const modelArg = isModelListener && event.slice(7)
+  let modifiers: any
+
+  // has v-bind or :[eventName]
+  if (hasDynamicProps) {
+    tryGet(key => getDynamicPropValue(rawProps, key)[0])
+  } else {
+    const staticProps = rawProps[0] as StaticProps
+    tryGet(key => staticProps[key] && staticProps[key]())
+  }
+
+  function tryGet(getter: (key: string) => any) {
+    handler =
+      getter((handlerName = toHandlerKey(event))) ||
+      // also try camelCase event handler (#2249)
+      getter((handlerName = toHandlerKey(camelize(event))))
+    // for v-model update:xxx events, also trigger kebab-case equivalent
+    // for props passed via kebab-case
+    if (!handler && isModelListener) {
+      handler = getter((handlerName = toHandlerKey(hyphenate(event))))
+    }
+    onceHandler = getter(`${handlerName}Once`)
+    modifiers =
+      modelArg &&
+      getter(`${modelArg === 'modelValue' ? 'model' : modelArg}Modifiers`)
+  }
 
-  if (modelArg && modelArg in rawProps) {
-    const modifiersKey = `${
-      modelArg === 'modelValue' ? 'model' : modelArg
-    }Modifiers`
-    const { number, trim } = rawProps[modifiersKey] || EMPTY_OBJ
+  // for v-model update:xxx events, apply modifiers on args
+  let args = rawArgs
+  if (modifiers) {
+    const { number, trim } = modifiers
     if (trim) {
       args = rawArgs.map(a => (isString(a) ? a.trim() : a))
     }
@@ -73,17 +122,6 @@ export function emit(
 
   // TODO: warn
 
-  let handlerName
-  let handler =
-    rawProps[(handlerName = toHandlerKey(event))] ||
-    // also try camelCase event handler (#2249)
-    rawProps[(handlerName = toHandlerKey(camelize(event)))]
-  // for v-model update:xxx events, also trigger kebab-case equivalent
-  // for props passed via kebab-case
-  if (!handler && isModelListener) {
-    handler = rawProps[(handlerName = toHandlerKey(hyphenate(event)))]
-  }
-
   if (handler) {
     callWithAsyncErrorHandling(
       handler,
@@ -93,14 +131,13 @@ export function emit(
     )
   }
 
-  const onceHandler = rawProps[`${handlerName}Once`]
   if (onceHandler) {
     if (!instance.emitted) {
       instance.emitted = {}
-    } else if (instance.emitted[handlerName]) {
+    } else if (instance.emitted[handlerName!]) {
       return
     }
-    instance.emitted[handlerName] = true
+    instance.emitted[handlerName!] = true
     callWithAsyncErrorHandling(
       onceHandler,
       instance,
@@ -116,8 +153,9 @@ export function normalizeEmitsOptions(
   // TODO: caching?
 
   const raw = comp.emits
-  let normalized: ObjectEmitsOptions = {}
+  if (!raw) return null
 
+  let normalized: ObjectEmitsOptions = {}
   if (isArray(raw)) {
     raw.forEach(key => (normalized[key] = null))
   } else {

+ 3 - 3
packages/runtime-vapor/src/componentProps.ts

@@ -72,10 +72,10 @@ export type NormalizedPropsOptions =
   | [props: NormalizedProps, needCastKeys: string[]]
   | []
 
-type StaticProps = Record<string, () => unknown>
+export type StaticProps = Record<string, () => unknown>
 type DynamicProps = () => Data
 export type NormalizedRawProps = Array<StaticProps | DynamicProps>
-export type RawProps = NormalizedRawProps | StaticProps | null
+export type RawProps = NormalizedRawProps | StaticProps | DynamicProps | null
 
 export function initProps(
   instance: ComponentInternalInstance,
@@ -170,7 +170,7 @@ function getRawKey(obj: Data, key: string) {
 }
 
 type DynamicPropResult = [value: unknown, absent: boolean]
-function getDynamicPropValue(
+export function getDynamicPropValue(
   rawProps: NormalizedRawProps,
   key: string,
 ): DynamicPropResult {