Browse Source

also warn when listening to camelCase events in in-DOM templates

dev
Evan You 8 years ago
parent
commit
0bb529a275
  1. 14
      src/core/instance/events.js
  2. 12
      src/core/util/debug.js
  3. 18
      src/core/vdom/create-component.js
  4. 55
      test/helpers/to-have-been-warned.js
  5. 23
      test/unit/features/component/component.spec.js
  6. 1
      test/unit/modules/vdom/patch/hydration.spec.js

14
src/core/instance/events.js

@ -1,7 +1,7 @@
/* @flow */
import { toArray } from '../util/index'
import { updateListeners } from '../vdom/helpers/index'
import { toArray, tip, hyphenate, formatComponentName } from '../util/index'
export function initEvents (vm: Component) {
vm._events = Object.create(null)
@ -104,6 +104,18 @@ export function eventsMixin (Vue: Class<Component>) {
Vue.prototype.$emit = function (event: string): Component {
const vm: Component = this
if (process.env.NODE_ENV !== 'production') {
const lowerCaseEvent = event.toLowerCase()
if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
tip(
`Event "${lowerCaseEvent}" is emitted in component ` +
`${formatComponentName(vm)} but the handler is registered for "${event}". ` +
`Note that HTML attributes are case-insensitive and you cannot use ` +
`v-on to listen to camelCase events when using in-DOM templates. ` +
`You should probably use "${hyphenate(event)}" instead of "${event}".`
)
}
}
let cbs = vm._events[event]
if (cbs) {
cbs = cbs.length > 1 ? toArray(cbs) : cbs

12
src/core/util/debug.js

@ -32,11 +32,13 @@ if (process.env.NODE_ENV !== 'production') {
if (vm.$root === vm) {
return '<Root>'
}
let name = typeof vm === 'function' && vm.options
? vm.options.name
: vm._isVue
? vm.$options.name || vm.$options._componentTag
: vm.name
let name = typeof vm === 'string'
? vm
: typeof vm === 'function' && vm.options
? vm.options.name
: vm._isVue
? vm.$options.name || vm.$options._componentTag
: vm.name
const file = vm._isVue && vm.$options.__file
if (!name && file) {

18
src/core/vdom/create-component.js

@ -6,6 +6,7 @@ import { resolveConstructorOptions } from '../instance/init'
import { resolveSlots } from '../instance/render-helpers/resolve-slots'
import {
tip,
warn,
isObject,
hasOwn,
@ -133,7 +134,7 @@ export function createComponent (
}
// extract props
const propsData = extractProps(data, Ctor)
const propsData = extractProps(data, Ctor, tag)
// functional component
if (Ctor.options.functional) {
@ -274,7 +275,7 @@ function resolveAsyncComponent (
}
}
function extractProps (data: VNodeData, Ctor: Class<Component>): ?Object {
function extractProps (data: VNodeData, Ctor: Class<Component>, tag?: string): ?Object {
// we are only extracting raw values here.
// validation and default values are handled in the child
// component itself.
@ -293,12 +294,13 @@ function extractProps (data: VNodeData, Ctor: Class<Component>): ?Object {
key !== keyInLowerCase &&
attrs && attrs.hasOwnProperty(keyInLowerCase)
) {
warn(
`Prop "${keyInLowerCase}" is not declared in component ` +
`${formatComponentName(Ctor)}. Note that HTML attributes are ` +
`case-insensitive and camelCased props need to use their kebab-case ` +
`equivalents when using in-DOM templates. You should probably use ` +
`"${altKey}" instead of "${key}".`
tip(
`Prop "${keyInLowerCase}" is passed to component ` +
`${formatComponentName(tag || Ctor)}, but the delared prop name is` +
` "${key}". ` +
`Note that HTML attributes are case-insensitive and camelCased ` +
`props need to use their kebab-case equivalents when using in-DOM ` +
`templates. You should probably use "${altKey}" instead of "${key}".`
)
}
}

55
test/helpers/to-have-been-warned.js

@ -11,42 +11,47 @@ if (typeof console === 'undefined') {
console.info = noop
let asserted
function hasWarned (msg) {
var count = console.error.calls.count()
var args
while (count--) {
args = console.error.calls.argsFor(count)
if (args.some(containsMsg)) {
return true
function createCompareFn (spy) {
const hasWarned = msg => {
var count = spy.calls.count()
var args
while (count--) {
args = spy.calls.argsFor(count)
if (args.some(containsMsg)) {
return true
}
}
function containsMsg (arg) {
return arg.toString().indexOf(msg) > -1
}
}
function containsMsg (arg) {
return arg.toString().indexOf(msg) > -1
return {
compare: msg => {
asserted = asserted.concat(msg)
var warned = Array.isArray(msg)
? msg.some(hasWarned)
: hasWarned(msg)
return {
pass: warned,
message: warned
? 'Expected message "' + msg + '" not to have been warned'
: 'Expected message "' + msg + '" to have been warned'
}
}
}
}
// define custom matcher for warnings
beforeEach(() => {
asserted = []
spyOn(console, 'warn')
spyOn(console, 'error')
jasmine.addMatchers({
toHaveBeenWarned: () => {
return {
compare: msg => {
asserted = asserted.concat(msg)
var warned = Array.isArray(msg)
? msg.some(hasWarned)
: hasWarned(msg)
return {
pass: warned,
message: warned
? 'Expected message "' + msg + '" not to have been warned'
: 'Expected message "' + msg + '" to have been warned'
}
}
}
}
toHaveBeenWarned: () => createCompareFn(console.error),
toHaveBeenTipped: () => createCompareFn(console.warn)
})
})

23
test/unit/features/component/component.spec.js

@ -258,12 +258,12 @@ describe('Component', () => {
expect(vm.$el.outerHTML).toBe('<ul><li>1</li><li>2</li></ul>')
})
it('should warn when not passing props in kebab-case', () => {
it('should warn when using camelCased props in in-DOM template', () => {
new Vue({
data: {
list: [{ a: 1 }, { a: 2 }]
},
template: '<test :somecollection="list"></test>',
template: '<test :somecollection="list"></test>', // <-- simulate lowercased template
components: {
test: {
template: '<ul><li v-for="item in someCollection">{{item.a}}</li></ul>',
@ -273,7 +273,24 @@ describe('Component', () => {
}).$mount()
expect(
'You should probably use "some-collection" instead of "someCollection".'
).toHaveBeenWarned()
).toHaveBeenTipped()
})
it('should warn when using camelCased events in in-DOM template', () => {
new Vue({
template: '<test @foobar="a++"></test>', // <-- simulate lowercased template
components: {
test: {
template: '<div></div>',
created () {
this.$emit('fooBar')
}
}
}
}).$mount()
expect(
'You should probably use "foo-bar" instead of "fooBar".'
).toHaveBeenTipped()
})
it('not found component should not throw', () => {

1
test/unit/modules/vdom/patch/hydration.spec.js

@ -5,7 +5,6 @@ import VNode from 'core/vdom/vnode'
describe('vdom patch: hydration', () => {
let vnode0
beforeEach(() => {
spyOn(console, 'warn')
vnode0 = new VNode('p', { attrs: { id: '1' }}, [createTextVNode('hello world')])
patch(null, vnode0)
})

Loading…
Cancel
Save