mirror of https://github.com/vuejs/core.git
feat(runtime-vapor): runtime for v-on in component (#178)
Co-authored-by: 三咲智子 Kevin Deng <sxzz@sxzz.moe>
This commit is contained in:
parent
7cacb655e0
commit
37df043adc
|
@ -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(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(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()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
const { rawProps } = instance
|
||||
|
||||
let args = rawArgs
|
||||
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 handlerName: string
|
||||
let handler: any
|
||||
let onceHandler: any
|
||||
|
||||
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`)
|
||||
}
|
||||
|
||||
// for v-model update:xxx events, apply modifiers on args
|
||||
const modelArg = isModelListener && event.slice(7)
|
||||
|
||||
if (modelArg && modelArg in rawProps) {
|
||||
const modifiersKey = `${
|
||||
modelArg === 'modelValue' ? 'model' : modelArg
|
||||
}Modifiers`
|
||||
const { number, trim } = rawProps[modifiersKey] || EMPTY_OBJ
|
||||
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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue