fix(custom-element): fix event listeners with capital letter event names on custom elements
close https://github.com/vuejs/docs/issues/1708 close https://github.com/vuejs/docs/pull/1890
This commit is contained in:
		
							parent
							
								
									9f8f07ed38
								
							
						
					
					
						commit
						0739f8909a
					
				@ -314,6 +314,37 @@ describe('compiler: element transform', () => {
 | 
				
			|||||||
    )
 | 
					    )
 | 
				
			||||||
    expect(root.helpers).toContain(MERGE_PROPS)
 | 
					    expect(root.helpers).toContain(MERGE_PROPS)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(node.props).toMatchObject({
 | 
				
			||||||
 | 
					      type: NodeTypes.JS_CALL_EXPRESSION,
 | 
				
			||||||
 | 
					      callee: MERGE_PROPS,
 | 
				
			||||||
 | 
					      arguments: [
 | 
				
			||||||
 | 
					        createObjectMatcher({
 | 
				
			||||||
 | 
					          id: 'foo'
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          type: NodeTypes.JS_CALL_EXPRESSION,
 | 
				
			||||||
 | 
					          callee: TO_HANDLERS,
 | 
				
			||||||
 | 
					          arguments: [
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					              type: NodeTypes.SIMPLE_EXPRESSION,
 | 
				
			||||||
 | 
					              content: `obj`
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            `true`
 | 
				
			||||||
 | 
					          ]
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        createObjectMatcher({
 | 
				
			||||||
 | 
					          class: 'bar'
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					      ]
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  test('v-on="obj" on component', () => {
 | 
				
			||||||
 | 
					    const { root, node } = parseWithElementTransform(
 | 
				
			||||||
 | 
					      `<Foo id="foo" v-on="obj" class="bar" />`
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    expect(root.helpers).toContain(MERGE_PROPS)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    expect(node.props).toMatchObject({
 | 
					    expect(node.props).toMatchObject({
 | 
				
			||||||
      type: NodeTypes.JS_CALL_EXPRESSION,
 | 
					      type: NodeTypes.JS_CALL_EXPRESSION,
 | 
				
			||||||
      callee: MERGE_PROPS,
 | 
					      callee: MERGE_PROPS,
 | 
				
			||||||
@ -358,7 +389,8 @@ describe('compiler: element transform', () => {
 | 
				
			|||||||
            {
 | 
					            {
 | 
				
			||||||
              type: NodeTypes.SIMPLE_EXPRESSION,
 | 
					              type: NodeTypes.SIMPLE_EXPRESSION,
 | 
				
			||||||
              content: `handlers`
 | 
					              content: `handlers`
 | 
				
			||||||
            }
 | 
					            },
 | 
				
			||||||
 | 
					            `true`
 | 
				
			||||||
          ]
 | 
					          ]
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
 | 
				
			|||||||
@ -647,7 +647,7 @@ export function buildProps(
 | 
				
			|||||||
              type: NodeTypes.JS_CALL_EXPRESSION,
 | 
					              type: NodeTypes.JS_CALL_EXPRESSION,
 | 
				
			||||||
              loc,
 | 
					              loc,
 | 
				
			||||||
              callee: context.helper(TO_HANDLERS),
 | 
					              callee: context.helper(TO_HANDLERS),
 | 
				
			||||||
              arguments: [exp]
 | 
					              arguments: isComponent ? [exp] : [exp, `true`]
 | 
				
			||||||
            })
 | 
					            })
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
 | 
				
			|||||||
@ -47,12 +47,17 @@ export const transformOn: DirectiveTransform = (
 | 
				
			|||||||
      if (rawName.startsWith('vue:')) {
 | 
					      if (rawName.startsWith('vue:')) {
 | 
				
			||||||
        rawName = `vnode-${rawName.slice(4)}`
 | 
					        rawName = `vnode-${rawName.slice(4)}`
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      // for all event listeners, auto convert it to camelCase. See issue #2249
 | 
					      const eventString =
 | 
				
			||||||
      eventName = createSimpleExpression(
 | 
					        node.tagType === ElementTypes.COMPONENT ||
 | 
				
			||||||
        toHandlerKey(camelize(rawName)),
 | 
					        rawName.startsWith('vnode') ||
 | 
				
			||||||
        true,
 | 
					        !/[A-Z]/.test(rawName)
 | 
				
			||||||
        arg.loc
 | 
					          ? // for component and vnode lifecycle event listeners, auto convert
 | 
				
			||||||
      )
 | 
					            // it to camelCase. See issue #2249
 | 
				
			||||||
 | 
					            toHandlerKey(camelize(rawName))
 | 
				
			||||||
 | 
					            // preserve case for plain element listeners that have uppercase
 | 
				
			||||||
 | 
					            // letters, as these may be custom elements' custom events
 | 
				
			||||||
 | 
					          : `on:${rawName}`
 | 
				
			||||||
 | 
					      eventName = createSimpleExpression(eventString, true, arg.loc)
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      // #2388
 | 
					      // #2388
 | 
				
			||||||
      eventName = createCompoundExpression([
 | 
					      eventName = createCompoundExpression([
 | 
				
			||||||
 | 
				
			|||||||
@ -5,14 +5,21 @@ import { warn } from '../warning'
 | 
				
			|||||||
 * For prefixing keys in v-on="obj" with "on"
 | 
					 * For prefixing keys in v-on="obj" with "on"
 | 
				
			||||||
 * @private
 | 
					 * @private
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export function toHandlers(obj: Record<string, any>): Record<string, any> {
 | 
					export function toHandlers(
 | 
				
			||||||
 | 
					  obj: Record<string, any>,
 | 
				
			||||||
 | 
					  preserveCaseIfNecessary?: boolean
 | 
				
			||||||
 | 
					): Record<string, any> {
 | 
				
			||||||
  const ret: Record<string, any> = {}
 | 
					  const ret: Record<string, any> = {}
 | 
				
			||||||
  if (__DEV__ && !isObject(obj)) {
 | 
					  if (__DEV__ && !isObject(obj)) {
 | 
				
			||||||
    warn(`v-on with no argument expects an object value.`)
 | 
					    warn(`v-on with no argument expects an object value.`)
 | 
				
			||||||
    return ret
 | 
					    return ret
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  for (const key in obj) {
 | 
					  for (const key in obj) {
 | 
				
			||||||
    ret[toHandlerKey(key)] = obj[key]
 | 
					    ret[
 | 
				
			||||||
 | 
					      preserveCaseIfNecessary && /[A-Z]/.test(key)
 | 
				
			||||||
 | 
					        ? `on:${key}`
 | 
				
			||||||
 | 
					        : toHandlerKey(key)
 | 
				
			||||||
 | 
					    ] = obj[key]
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  return ret
 | 
					  return ret
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -101,7 +101,8 @@ function parseName(name: string): [string, EventListenerOptions | undefined] {
 | 
				
			|||||||
      ;(options as any)[m[0].toLowerCase()] = true
 | 
					      ;(options as any)[m[0].toLowerCase()] = true
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  return [hyphenate(name.slice(2)), options]
 | 
					  const event = name[2] === ':' ? name.slice(3) : hyphenate(name.slice(2))
 | 
				
			||||||
 | 
					  return [event, options]
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function createInvoker(
 | 
					function createInvoker(
 | 
				
			||||||
 | 
				
			|||||||
@ -16,7 +16,7 @@ export function patchProp(
 | 
				
			|||||||
  })
 | 
					  })
 | 
				
			||||||
  el.props[key] = nextValue
 | 
					  el.props[key] = nextValue
 | 
				
			||||||
  if (isOn(key)) {
 | 
					  if (isOn(key)) {
 | 
				
			||||||
    const event = key.slice(2).toLowerCase()
 | 
					    const event = key[2] === ':' ? key.slice(3) : key.slice(2).toLowerCase()
 | 
				
			||||||
    ;(el.eventListeners || (el.eventListeners = {}))[event] = nextValue
 | 
					    ;(el.eventListeners || (el.eventListeners = {}))[event] = nextValue
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										42
									
								
								packages/vue/__tests__/customElementCasing.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								packages/vue/__tests__/customElementCasing.spec.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,42 @@
 | 
				
			|||||||
 | 
					import { createApp } from '../src'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// https://github.com/vuejs/docs/pull/1890
 | 
				
			||||||
 | 
					// https://github.com/vuejs/core/issues/5401
 | 
				
			||||||
 | 
					// https://github.com/vuejs/docs/issues/1708
 | 
				
			||||||
 | 
					test('custom element event casing', () => {
 | 
				
			||||||
 | 
					  customElements.define(
 | 
				
			||||||
 | 
					    'custom-event-casing',
 | 
				
			||||||
 | 
					    class Foo extends HTMLElement {
 | 
				
			||||||
 | 
					      connectedCallback() {
 | 
				
			||||||
 | 
					        this.dispatchEvent(new Event('camelCase'))
 | 
				
			||||||
 | 
					        this.dispatchEvent(new Event('CAPScase'))
 | 
				
			||||||
 | 
					        this.dispatchEvent(new Event('PascalCase'))
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const container = document.createElement('div')
 | 
				
			||||||
 | 
					  document.body.appendChild(container)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handler = jest.fn()
 | 
				
			||||||
 | 
					  const handler2 = jest.fn()
 | 
				
			||||||
 | 
					  createApp({
 | 
				
			||||||
 | 
					    template: `
 | 
				
			||||||
 | 
					    <custom-event-casing
 | 
				
			||||||
 | 
					      @camelCase="handler"
 | 
				
			||||||
 | 
					      @CAPScase="handler"
 | 
				
			||||||
 | 
					      @PascalCase="handler"
 | 
				
			||||||
 | 
					      v-on="{
 | 
				
			||||||
 | 
					        camelCase: handler2,
 | 
				
			||||||
 | 
					        CAPScase: handler2,
 | 
				
			||||||
 | 
					        PascalCase: handler2
 | 
				
			||||||
 | 
					      }" />`,
 | 
				
			||||||
 | 
					    methods: {
 | 
				
			||||||
 | 
					      handler,
 | 
				
			||||||
 | 
					      handler2
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }).mount(container)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  expect(handler).toHaveBeenCalledTimes(3)
 | 
				
			||||||
 | 
					  expect(handler2).toHaveBeenCalledTimes(3)
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user