mirror of https://github.com/vuejs/core.git
feat(runtime-vapor): component emits (#103)
Co-authored-by: 三咲智子 Kevin Deng <sxzz@sxzz.moe>
This commit is contained in:
parent
ea5f7ec076
commit
cde91e4fb5
|
@ -72,6 +72,7 @@ The code provided here is a duplicate from `runtime-core` as Vapor cannot import
|
||||||
|
|
||||||
- packages/runtime-vapor/src/apiWatch.ts
|
- packages/runtime-vapor/src/apiWatch.ts
|
||||||
- packages/runtime-vapor/src/component.ts
|
- packages/runtime-vapor/src/component.ts
|
||||||
|
- packages/runtime-vapor/src/componentEmits.ts
|
||||||
- packages/runtime-vapor/src/componentProps.ts
|
- packages/runtime-vapor/src/componentProps.ts
|
||||||
- packages/runtime-vapor/src/enums.ts
|
- packages/runtime-vapor/src/enums.ts
|
||||||
- packages/runtime-vapor/src/errorHandling.ts
|
- packages/runtime-vapor/src/errorHandling.ts
|
||||||
|
|
|
@ -0,0 +1,488 @@
|
||||||
|
// NOTE: this test cases are based on paclages/runtime-core/__tests__/componentEmits.spec.ts
|
||||||
|
|
||||||
|
// Note: emits and listener fallthrough is tested in
|
||||||
|
// ./rendererAttrsFallthrough.spec.ts.
|
||||||
|
|
||||||
|
import {
|
||||||
|
defineComponent,
|
||||||
|
nextTick,
|
||||||
|
onBeforeUnmount,
|
||||||
|
render,
|
||||||
|
unmountComponent,
|
||||||
|
} from '../src'
|
||||||
|
import { isEmitListener } from '../src/componentEmits'
|
||||||
|
|
||||||
|
let host: HTMLElement
|
||||||
|
|
||||||
|
const initHost = () => {
|
||||||
|
host = document.createElement('div')
|
||||||
|
host.setAttribute('id', 'host')
|
||||||
|
document.body.appendChild(host)
|
||||||
|
}
|
||||||
|
beforeEach(() => initHost())
|
||||||
|
afterEach(() => host.remove())
|
||||||
|
|
||||||
|
describe('component: emit', () => {
|
||||||
|
test('trigger handlers', () => {
|
||||||
|
const Foo = defineComponent({
|
||||||
|
render() {},
|
||||||
|
setup(_: any, { emit }: any) {
|
||||||
|
emit('foo')
|
||||||
|
emit('bar')
|
||||||
|
emit('!baz')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const onfoo = vi.fn()
|
||||||
|
const onBar = vi.fn()
|
||||||
|
const onBaz = vi.fn()
|
||||||
|
render(
|
||||||
|
Foo,
|
||||||
|
{
|
||||||
|
get onfoo() {
|
||||||
|
return onfoo
|
||||||
|
},
|
||||||
|
get onBar() {
|
||||||
|
return onBar
|
||||||
|
},
|
||||||
|
get ['on!baz']() {
|
||||||
|
return onBaz
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'#host',
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(onfoo).not.toHaveBeenCalled()
|
||||||
|
expect(onBar).toHaveBeenCalled()
|
||||||
|
expect(onBaz).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('trigger camelCase handler', () => {
|
||||||
|
const Foo = defineComponent({
|
||||||
|
render() {},
|
||||||
|
setup(_: any, { emit }: any) {
|
||||||
|
emit('test-event')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const fooSpy = vi.fn()
|
||||||
|
render(
|
||||||
|
Foo,
|
||||||
|
{
|
||||||
|
get onTestEvent() {
|
||||||
|
return fooSpy
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'#host',
|
||||||
|
)
|
||||||
|
expect(fooSpy).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('trigger kebab-case handler', () => {
|
||||||
|
const Foo = defineComponent({
|
||||||
|
render() {},
|
||||||
|
setup(_: any, { emit }: any) {
|
||||||
|
emit('test-event')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const fooSpy = vi.fn()
|
||||||
|
render(
|
||||||
|
Foo,
|
||||||
|
{
|
||||||
|
get ['onTest-event']() {
|
||||||
|
return fooSpy
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'#host',
|
||||||
|
)
|
||||||
|
expect(fooSpy).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
// #3527
|
||||||
|
test.todo('trigger mixed case handlers', () => {
|
||||||
|
const Foo = defineComponent({
|
||||||
|
render() {},
|
||||||
|
setup(_: any, { emit }: any) {
|
||||||
|
emit('test-event')
|
||||||
|
emit('testEvent')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const fooSpy = vi.fn()
|
||||||
|
const barSpy = vi.fn()
|
||||||
|
render(
|
||||||
|
Foo,
|
||||||
|
// TODO: impl `toHandlers`
|
||||||
|
{
|
||||||
|
get ['onTest-Event']() {
|
||||||
|
return fooSpy
|
||||||
|
},
|
||||||
|
get onTestEvent() {
|
||||||
|
return barSpy
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'#host',
|
||||||
|
)
|
||||||
|
expect(fooSpy).toHaveBeenCalledTimes(1)
|
||||||
|
expect(barSpy).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
// for v-model:foo-bar usage in DOM templates
|
||||||
|
test('trigger hyphenated events for update:xxx events', () => {
|
||||||
|
const Foo = defineComponent({
|
||||||
|
render() {},
|
||||||
|
setup(_: any, { emit }: any) {
|
||||||
|
emit('update:fooProp')
|
||||||
|
emit('update:barProp')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const fooSpy = vi.fn()
|
||||||
|
const barSpy = vi.fn()
|
||||||
|
render(
|
||||||
|
Foo,
|
||||||
|
{
|
||||||
|
get ['onUpdate:fooProp']() {
|
||||||
|
return fooSpy
|
||||||
|
},
|
||||||
|
get ['onUpdate:bar-prop']() {
|
||||||
|
return barSpy
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'#host',
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(fooSpy).toHaveBeenCalled()
|
||||||
|
expect(barSpy).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should trigger array of listeners', async () => {
|
||||||
|
const App = defineComponent({
|
||||||
|
render() {},
|
||||||
|
setup(_: any, { emit }: any) {
|
||||||
|
emit('foo', 1)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const fn1 = vi.fn()
|
||||||
|
const fn2 = vi.fn()
|
||||||
|
|
||||||
|
render(
|
||||||
|
App,
|
||||||
|
{
|
||||||
|
get onFoo() {
|
||||||
|
return [fn1, fn2]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'#host',
|
||||||
|
)
|
||||||
|
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.todo('warning for undeclared event (object)', () => {
|
||||||
|
// TODO: warning
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should not warn if has equivalent onXXX prop', () => {
|
||||||
|
const Foo = defineComponent({
|
||||||
|
props: ['onFoo'],
|
||||||
|
emits: [],
|
||||||
|
render() {},
|
||||||
|
setup(_: any, { emit }: any) {
|
||||||
|
emit('foo')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
render(Foo, {}, '#host')
|
||||||
|
expect(
|
||||||
|
`Component emitted event "foo" but it is neither declared`,
|
||||||
|
).not.toHaveBeenWarned()
|
||||||
|
})
|
||||||
|
|
||||||
|
test.todo('validator warning', () => {
|
||||||
|
// TODO: warning validator
|
||||||
|
})
|
||||||
|
|
||||||
|
// NOTE: not supported mixins
|
||||||
|
// test.todo('merging from mixins', () => {})
|
||||||
|
|
||||||
|
// #2651
|
||||||
|
// test.todo(
|
||||||
|
// 'should not attach normalized object when mixins do not contain emits',
|
||||||
|
// () => {},
|
||||||
|
// )
|
||||||
|
|
||||||
|
test('.once', () => {
|
||||||
|
const Foo = defineComponent({
|
||||||
|
render() {},
|
||||||
|
emits: {
|
||||||
|
foo: null,
|
||||||
|
bar: null,
|
||||||
|
},
|
||||||
|
setup(_: any, { emit }: any) {
|
||||||
|
emit('foo')
|
||||||
|
emit('foo')
|
||||||
|
emit('bar')
|
||||||
|
emit('bar')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const fn = vi.fn()
|
||||||
|
const barFn = vi.fn()
|
||||||
|
render(
|
||||||
|
Foo,
|
||||||
|
{
|
||||||
|
get onFooOnce() {
|
||||||
|
return fn
|
||||||
|
},
|
||||||
|
get onBarOnce() {
|
||||||
|
return barFn
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'#host',
|
||||||
|
)
|
||||||
|
expect(fn).toHaveBeenCalledTimes(1)
|
||||||
|
expect(barFn).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('.once with normal listener of the same name', () => {
|
||||||
|
const Foo = defineComponent({
|
||||||
|
render() {},
|
||||||
|
emits: {
|
||||||
|
foo: null,
|
||||||
|
},
|
||||||
|
setup(_: any, { emit }: any) {
|
||||||
|
emit('foo')
|
||||||
|
emit('foo')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const onFoo = vi.fn()
|
||||||
|
const onFooOnce = vi.fn()
|
||||||
|
render(
|
||||||
|
Foo,
|
||||||
|
{
|
||||||
|
get onFoo() {
|
||||||
|
return onFoo
|
||||||
|
},
|
||||||
|
get onFooOnce() {
|
||||||
|
return onFooOnce
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'#host',
|
||||||
|
)
|
||||||
|
expect(onFoo).toHaveBeenCalledTimes(2)
|
||||||
|
expect(onFooOnce).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('.number modifier should work with v-model on component', () => {
|
||||||
|
const Foo = defineComponent({
|
||||||
|
render() {},
|
||||||
|
setup(_: any, { emit }: any) {
|
||||||
|
emit('update:modelValue', '1')
|
||||||
|
emit('update:foo', '2')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const fn1 = vi.fn()
|
||||||
|
const fn2 = vi.fn()
|
||||||
|
render(
|
||||||
|
Foo,
|
||||||
|
{
|
||||||
|
get modelValue() {
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
get modelModifiers() {
|
||||||
|
return { number: true }
|
||||||
|
},
|
||||||
|
get ['onUpdate:modelValue']() {
|
||||||
|
return fn1
|
||||||
|
},
|
||||||
|
get foo() {
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
get fooModifiers() {
|
||||||
|
return { number: true }
|
||||||
|
},
|
||||||
|
get ['onUpdate:foo']() {
|
||||||
|
return fn2
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'#host',
|
||||||
|
)
|
||||||
|
expect(fn1).toHaveBeenCalledTimes(1)
|
||||||
|
expect(fn1).toHaveBeenCalledWith(1)
|
||||||
|
expect(fn2).toHaveBeenCalledTimes(1)
|
||||||
|
expect(fn2).toHaveBeenCalledWith(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('.trim modifier should work with v-model on component', () => {
|
||||||
|
const Foo = defineComponent({
|
||||||
|
render() {},
|
||||||
|
setup(_: any, { emit }: any) {
|
||||||
|
emit('update:modelValue', ' one ')
|
||||||
|
emit('update:foo', ' two ')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const fn1 = vi.fn()
|
||||||
|
const fn2 = vi.fn()
|
||||||
|
render(
|
||||||
|
Foo,
|
||||||
|
{
|
||||||
|
get modelValue() {
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
get modelModifiers() {
|
||||||
|
return { trim: true }
|
||||||
|
},
|
||||||
|
get ['onUpdate:modelValue']() {
|
||||||
|
return fn1
|
||||||
|
},
|
||||||
|
get foo() {
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
get fooModifiers() {
|
||||||
|
return { trim: true }
|
||||||
|
},
|
||||||
|
get 'onUpdate:foo'() {
|
||||||
|
return fn2
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'#host',
|
||||||
|
)
|
||||||
|
expect(fn1).toHaveBeenCalledTimes(1)
|
||||||
|
expect(fn1).toHaveBeenCalledWith('one')
|
||||||
|
expect(fn2).toHaveBeenCalledTimes(1)
|
||||||
|
expect(fn2).toHaveBeenCalledWith('two')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('.trim and .number modifiers should work with v-model on component', () => {
|
||||||
|
const Foo = defineComponent({
|
||||||
|
render() {},
|
||||||
|
setup(_: any, { emit }: any) {
|
||||||
|
emit('update:modelValue', ' +01.2 ')
|
||||||
|
emit('update:foo', ' 1 ')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const fn1 = vi.fn()
|
||||||
|
const fn2 = vi.fn()
|
||||||
|
render(
|
||||||
|
Foo,
|
||||||
|
{
|
||||||
|
get modelValue() {
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
get modelModifiers() {
|
||||||
|
return { trim: true, number: true }
|
||||||
|
},
|
||||||
|
get ['onUpdate:modelValue']() {
|
||||||
|
return fn1
|
||||||
|
},
|
||||||
|
get foo() {
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
get fooModifiers() {
|
||||||
|
return { trim: true, number: true }
|
||||||
|
},
|
||||||
|
get ['onUpdate:foo']() {
|
||||||
|
return fn2
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'#host',
|
||||||
|
)
|
||||||
|
expect(fn1).toHaveBeenCalledTimes(1)
|
||||||
|
expect(fn1).toHaveBeenCalledWith(1.2)
|
||||||
|
expect(fn2).toHaveBeenCalledTimes(1)
|
||||||
|
expect(fn2).toHaveBeenCalledWith(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('only trim string parameter when work with v-model on component', () => {
|
||||||
|
const Foo = defineComponent({
|
||||||
|
render() {},
|
||||||
|
setup(_: any, { emit }: any) {
|
||||||
|
emit('update:modelValue', ' foo ', { bar: ' bar ' })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const fn = vi.fn()
|
||||||
|
render(
|
||||||
|
Foo,
|
||||||
|
{
|
||||||
|
get modelValue() {
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
get modelModifiers() {
|
||||||
|
return { trim: true }
|
||||||
|
},
|
||||||
|
get ['onUpdate:modelValue']() {
|
||||||
|
return fn
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'#host',
|
||||||
|
)
|
||||||
|
expect(fn).toHaveBeenCalledTimes(1)
|
||||||
|
expect(fn).toHaveBeenCalledWith('foo', { bar: ' bar ' })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('isEmitListener', () => {
|
||||||
|
const options = {
|
||||||
|
get click() {
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
get 'test-event'() {
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
get fooBar() {
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
get FooBaz() {
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
}
|
||||||
|
expect(isEmitListener(options, 'onClick')).toBe(true)
|
||||||
|
expect(isEmitListener(options, 'onclick')).toBe(false)
|
||||||
|
expect(isEmitListener(options, 'onBlick')).toBe(false)
|
||||||
|
// .once listeners
|
||||||
|
expect(isEmitListener(options, 'onClickOnce')).toBe(true)
|
||||||
|
expect(isEmitListener(options, 'onclickOnce')).toBe(false)
|
||||||
|
// kebab-case option
|
||||||
|
expect(isEmitListener(options, 'onTestEvent')).toBe(true)
|
||||||
|
// camelCase option
|
||||||
|
expect(isEmitListener(options, 'onFooBar')).toBe(true)
|
||||||
|
// PascalCase option
|
||||||
|
expect(isEmitListener(options, 'onFooBaz')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('does not emit after unmount', async () => {
|
||||||
|
const fn = vi.fn()
|
||||||
|
const Foo = defineComponent({
|
||||||
|
emits: ['closing'],
|
||||||
|
setup(_: any, { emit }: any) {
|
||||||
|
onBeforeUnmount(async () => {
|
||||||
|
await nextTick()
|
||||||
|
emit('closing', true)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
render() {},
|
||||||
|
})
|
||||||
|
const i = render(
|
||||||
|
Foo,
|
||||||
|
{
|
||||||
|
get onClosing() {
|
||||||
|
return fn
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'#host',
|
||||||
|
)
|
||||||
|
await nextTick()
|
||||||
|
unmountComponent(i)
|
||||||
|
await nextTick()
|
||||||
|
expect(fn).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
// NOTE: not supported mixins
|
||||||
|
// test.todo('merge string array emits', async () => {})
|
||||||
|
// test.todo('merge object emits', async () => {})
|
||||||
|
})
|
|
@ -8,6 +8,13 @@ import {
|
||||||
type NormalizedPropsOptions,
|
type NormalizedPropsOptions,
|
||||||
normalizePropsOptions,
|
normalizePropsOptions,
|
||||||
} from './componentProps'
|
} from './componentProps'
|
||||||
|
import {
|
||||||
|
type EmitFn,
|
||||||
|
type EmitsOptions,
|
||||||
|
type ObjectEmitsOptions,
|
||||||
|
emit,
|
||||||
|
normalizeEmitsOptions,
|
||||||
|
} from './componentEmits'
|
||||||
|
|
||||||
import type { Data } from '@vue/shared'
|
import type { Data } from '@vue/shared'
|
||||||
import { VaporLifecycleHooks } from './enums'
|
import { VaporLifecycleHooks } from './enums'
|
||||||
|
@ -17,10 +24,12 @@ export type Component = FunctionalComponent | ObjectComponent
|
||||||
export type SetupFn = (props: any, ctx: any) => Block | Data
|
export type SetupFn = (props: any, ctx: any) => Block | Data
|
||||||
export type FunctionalComponent = SetupFn & {
|
export type FunctionalComponent = SetupFn & {
|
||||||
props: ComponentPropsOptions
|
props: ComponentPropsOptions
|
||||||
|
emits: EmitsOptions
|
||||||
render(ctx: any): Block
|
render(ctx: any): Block
|
||||||
}
|
}
|
||||||
export interface ObjectComponent {
|
export interface ObjectComponent {
|
||||||
props: ComponentPropsOptions
|
props: ComponentPropsOptions
|
||||||
|
emits: EmitsOptions
|
||||||
setup?: SetupFn
|
setup?: SetupFn
|
||||||
render(ctx: any): Block
|
render(ctx: any): Block
|
||||||
}
|
}
|
||||||
|
@ -37,13 +46,21 @@ export interface ComponentInternalInstance {
|
||||||
block: Block | null
|
block: Block | null
|
||||||
scope: EffectScope
|
scope: EffectScope
|
||||||
component: FunctionalComponent | ObjectComponent
|
component: FunctionalComponent | ObjectComponent
|
||||||
|
|
||||||
|
// TODO: ExtraProps: key, ref, ...
|
||||||
|
rawProps: { [key: string]: any }
|
||||||
|
|
||||||
|
// normalized options
|
||||||
propsOptions: NormalizedPropsOptions
|
propsOptions: NormalizedPropsOptions
|
||||||
|
emitsOptions: ObjectEmitsOptions | null
|
||||||
|
|
||||||
parent: ComponentInternalInstance | null
|
parent: ComponentInternalInstance | null
|
||||||
|
|
||||||
// state
|
// state
|
||||||
props: Data
|
props: Data
|
||||||
setupState: Data
|
setupState: Data
|
||||||
|
emit: EmitFn
|
||||||
|
emitted: Record<string, boolean> | null
|
||||||
refs: Data
|
refs: Data
|
||||||
metadata: WeakMap<Node, ElementMetadata>
|
metadata: WeakMap<Node, ElementMetadata>
|
||||||
|
|
||||||
|
@ -139,6 +156,7 @@ export const unsetCurrentInstance = () => {
|
||||||
let uid = 0
|
let uid = 0
|
||||||
export const createComponentInstance = (
|
export const createComponentInstance = (
|
||||||
component: ObjectComponent | FunctionalComponent,
|
component: ObjectComponent | FunctionalComponent,
|
||||||
|
rawProps: Data,
|
||||||
): ComponentInternalInstance => {
|
): ComponentInternalInstance => {
|
||||||
const instance: ComponentInternalInstance = {
|
const instance: ComponentInternalInstance = {
|
||||||
uid: uid++,
|
uid: uid++,
|
||||||
|
@ -146,13 +164,18 @@ export const createComponentInstance = (
|
||||||
container: null!, // set on mountComponent
|
container: null!, // set on mountComponent
|
||||||
scope: new EffectScope(true /* detached */)!,
|
scope: new EffectScope(true /* detached */)!,
|
||||||
component,
|
component,
|
||||||
|
rawProps,
|
||||||
|
|
||||||
// TODO: registory of parent
|
// TODO: registory of parent
|
||||||
parent: null,
|
parent: null,
|
||||||
|
|
||||||
// resolved props and emits options
|
// resolved props and emits options
|
||||||
propsOptions: normalizePropsOptions(component),
|
propsOptions: normalizePropsOptions(component),
|
||||||
// emitsOptions: normalizeEmitsOptions(type, appContext), // TODO:
|
emitsOptions: normalizeEmitsOptions(component),
|
||||||
|
|
||||||
|
// emit
|
||||||
|
emit: null!, // to be set immediately
|
||||||
|
emitted: null,
|
||||||
|
|
||||||
// state
|
// state
|
||||||
props: EMPTY_OBJ,
|
props: EMPTY_OBJ,
|
||||||
|
@ -225,5 +248,8 @@ export const createComponentInstance = (
|
||||||
*/
|
*/
|
||||||
// [VaporLifecycleHooks.SERVER_PREFETCH]: null,
|
// [VaporLifecycleHooks.SERVER_PREFETCH]: null,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
instance.emit = emit.bind(null, instance)
|
||||||
|
|
||||||
return instance
|
return instance
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,142 @@
|
||||||
|
// NOTE: runtime-core/src/componentEmits.ts
|
||||||
|
|
||||||
|
import {
|
||||||
|
EMPTY_OBJ,
|
||||||
|
type UnionToIntersection,
|
||||||
|
camelize,
|
||||||
|
extend,
|
||||||
|
hasOwn,
|
||||||
|
hyphenate,
|
||||||
|
isArray,
|
||||||
|
isOn,
|
||||||
|
isString,
|
||||||
|
looseToNumber,
|
||||||
|
toHandlerKey,
|
||||||
|
} from '@vue/shared'
|
||||||
|
import type { Component, ComponentInternalInstance } from './component'
|
||||||
|
import { VaporErrorCodes, callWithAsyncErrorHandling } from './errorHandling'
|
||||||
|
|
||||||
|
export type ObjectEmitsOptions = Record<
|
||||||
|
string,
|
||||||
|
((...args: any[]) => any) | null // TODO: call validation?
|
||||||
|
>
|
||||||
|
|
||||||
|
export type EmitsOptions = ObjectEmitsOptions | string[]
|
||||||
|
|
||||||
|
export type EmitFn<
|
||||||
|
Options = ObjectEmitsOptions,
|
||||||
|
Event extends keyof Options = keyof Options,
|
||||||
|
> =
|
||||||
|
Options extends Array<infer V>
|
||||||
|
? (event: V, ...args: any[]) => void
|
||||||
|
: {} extends Options // if the emit is empty object (usually the default value for emit) should be converted to function
|
||||||
|
? (event: string, ...args: any[]) => void
|
||||||
|
: UnionToIntersection<
|
||||||
|
{
|
||||||
|
[key in Event]: Options[key] extends (...args: infer Args) => any
|
||||||
|
? (event: key, ...args: Args) => void
|
||||||
|
: (event: key, ...args: any[]) => void
|
||||||
|
}[Event]
|
||||||
|
>
|
||||||
|
|
||||||
|
export function emit(
|
||||||
|
instance: ComponentInternalInstance,
|
||||||
|
event: string,
|
||||||
|
...rawArgs: any[]
|
||||||
|
) {
|
||||||
|
if (instance.isUnmounted) return
|
||||||
|
const { rawProps } = instance
|
||||||
|
|
||||||
|
let args = rawArgs
|
||||||
|
const isModelListener = event.startsWith('update:')
|
||||||
|
|
||||||
|
// 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
|
||||||
|
if (trim) {
|
||||||
|
args = rawArgs.map(a => (isString(a) ? a.trim() : a))
|
||||||
|
}
|
||||||
|
if (number) {
|
||||||
|
args = rawArgs.map(looseToNumber)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
instance,
|
||||||
|
VaporErrorCodes.COMPONENT_EVENT_HANDLER,
|
||||||
|
args,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onceHandler = rawProps[`${handlerName}Once`]
|
||||||
|
if (onceHandler) {
|
||||||
|
if (!instance.emitted) {
|
||||||
|
instance.emitted = {}
|
||||||
|
} else if (instance.emitted[handlerName]) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
instance.emitted[handlerName] = true
|
||||||
|
callWithAsyncErrorHandling(
|
||||||
|
onceHandler,
|
||||||
|
instance,
|
||||||
|
VaporErrorCodes.COMPONENT_EVENT_HANDLER,
|
||||||
|
args,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeEmitsOptions(
|
||||||
|
comp: Component,
|
||||||
|
): ObjectEmitsOptions | null {
|
||||||
|
// TODO: caching?
|
||||||
|
|
||||||
|
const raw = comp.emits
|
||||||
|
let normalized: ObjectEmitsOptions = {}
|
||||||
|
|
||||||
|
if (isArray(raw)) {
|
||||||
|
raw.forEach(key => (normalized[key] = null))
|
||||||
|
} else {
|
||||||
|
extend(normalized, raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if an incoming prop key is a declared emit event listener.
|
||||||
|
// e.g. With `emits: { click: null }`, props named `onClick` and `onclick` are
|
||||||
|
// both considered matched listeners.
|
||||||
|
export function isEmitListener(
|
||||||
|
options: ObjectEmitsOptions | null,
|
||||||
|
key: string,
|
||||||
|
): boolean {
|
||||||
|
if (!options || !isOn(key)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
key = key.slice(2).replace(/Once$/, '')
|
||||||
|
return (
|
||||||
|
hasOwn(options, key[0].toLowerCase() + key.slice(1)) ||
|
||||||
|
hasOwn(options, hyphenate(key)) ||
|
||||||
|
hasOwn(options, key)
|
||||||
|
)
|
||||||
|
}
|
|
@ -27,7 +27,7 @@ export function render(
|
||||||
props: Data,
|
props: Data,
|
||||||
container: string | ParentNode,
|
container: string | ParentNode,
|
||||||
): ComponentInternalInstance {
|
): ComponentInternalInstance {
|
||||||
const instance = createComponentInstance(comp)
|
const instance = createComponentInstance(comp, props)
|
||||||
initProps(instance, props)
|
initProps(instance, props)
|
||||||
return mountComponent(instance, (container = normalizeContainer(container)))
|
return mountComponent(instance, (container = normalizeContainer(container)))
|
||||||
}
|
}
|
||||||
|
@ -46,8 +46,8 @@ export function mountComponent(
|
||||||
|
|
||||||
const reset = setCurrentInstance(instance)
|
const reset = setCurrentInstance(instance)
|
||||||
const block = instance.scope.run(() => {
|
const block = instance.scope.run(() => {
|
||||||
const { component, props } = instance
|
const { component, props, emit } = instance
|
||||||
const ctx = { expose: () => {} }
|
const ctx = { expose: () => {}, emit }
|
||||||
|
|
||||||
const setupFn =
|
const setupFn =
|
||||||
typeof component === 'function' ? component : component.setup
|
typeof component === 'function' ? component : component.setup
|
||||||
|
|
Loading…
Reference in New Issue