diff --git a/packages/runtime-core/src/compat/__tests__/misc.spec.ts b/packages/runtime-core/src/compat/__tests__/misc.spec.ts
new file mode 100644
index 000000000..21574ca3f
--- /dev/null
+++ b/packages/runtime-core/src/compat/__tests__/misc.spec.ts
@@ -0,0 +1,249 @@
+import Vue from '@vue/compat'
+import { nextTick } from '../../scheduler'
+import {
+ DeprecationTypes,
+ deprecationData,
+ toggleDeprecationWarning
+} from '../compatConfig'
+
+beforeEach(() => {
+ toggleDeprecationWarning(true)
+ Vue.configureCompat({
+ MODE: 2,
+ GLOBAL_MOUNT: 'suppress-warning'
+ })
+})
+
+afterEach(() => {
+ toggleDeprecationWarning(false)
+ Vue.configureCompat({ MODE: 3 })
+})
+
+function triggerEvent(
+ target: Element,
+ event: string,
+ process?: (e: any) => any
+) {
+ const e = document.createEvent('HTMLEvents')
+ e.initEvent(event, true, true)
+ if (process) process(e)
+ target.dispatchEvent(e)
+ return e
+}
+
+test('WATCH_ARRAY', async () => {
+ const spy = jest.fn()
+ const vm = new Vue({
+ data() {
+ return {
+ foo: []
+ }
+ },
+ watch: {
+ foo: spy
+ }
+ }) as any
+ expect(
+ deprecationData[DeprecationTypes.WATCH_ARRAY].message
+ ).toHaveBeenWarned()
+
+ expect(spy).not.toHaveBeenCalled()
+ vm.foo.push(1)
+ await nextTick()
+ expect(spy).toHaveBeenCalledTimes(1)
+})
+
+test('PROPS_DEFAULT_THIS', () => {
+ let thisCtx: any
+ const Child = {
+ customOption: 1,
+ inject: ['provided'],
+ props: {
+ foo: null,
+ bar: {
+ default(this: any) {
+ // copy values since injection must be sync
+ thisCtx = {
+ foo: this.foo,
+ $options: this.$options,
+ provided: this.provided
+ }
+ return this.foo + 1
+ }
+ }
+ },
+ template: `{{ bar }}`
+ }
+
+ const vm = new Vue({
+ components: { Child },
+ provide: {
+ provided: 2
+ },
+ template: ``
+ }).$mount()
+
+ expect(vm.$el.textContent).toBe('1')
+ // other props
+ expect(thisCtx.foo).toBe(0)
+ // $options
+ expect(thisCtx.$options.customOption).toBe(1)
+ // injections
+ expect(thisCtx.provided).toBe(2)
+
+ expect(
+ (deprecationData[DeprecationTypes.PROPS_DEFAULT_THIS].message as Function)(
+ 'bar'
+ )
+ ).toHaveBeenWarned()
+})
+
+test('V_FOR_REF', async () => {
+ const vm = new Vue({
+ data() {
+ return {
+ ok: true,
+ list: [1, 2, 3]
+ }
+ },
+ template: `
+
+ {{ i }}
+
+ `
+ }).$mount() as any
+
+ const mapRefs = () => vm.$refs.list.map((el: HTMLElement) => el.textContent)
+ expect(mapRefs()).toMatchObject(['1', '2', '3'])
+
+ expect(deprecationData[DeprecationTypes.V_FOR_REF].message).toHaveBeenWarned()
+
+ vm.list.push(4)
+ await nextTick()
+ expect(mapRefs()).toMatchObject(['1', '2', '3', '4'])
+
+ vm.list.shift()
+ await nextTick()
+ expect(mapRefs()).toMatchObject(['2', '3', '4'])
+
+ vm.ok = !vm.ok
+ await nextTick()
+ expect(mapRefs()).toMatchObject([])
+
+ vm.ok = !vm.ok
+ await nextTick()
+ expect(mapRefs()).toMatchObject(['2', '3', '4'])
+})
+
+test('V_ON_KEYCODE_MODIFIER', () => {
+ const spy = jest.fn()
+ const vm = new Vue({
+ template: ``,
+ methods: { spy }
+ }).$mount()
+ triggerEvent(vm.$el, 'keyup', e => {
+ e.key = '_'
+ e.keyCode = 1
+ })
+ expect(spy).toHaveBeenCalled()
+ expect(
+ deprecationData[DeprecationTypes.V_ON_KEYCODE_MODIFIER].message
+ ).toHaveBeenWarned()
+})
+
+test('CUSTOM_DIR', async () => {
+ const myDir = {
+ bind: jest.fn(),
+ inserted: jest.fn(),
+ update: jest.fn(),
+ componentUpdated: jest.fn(),
+ unbind: jest.fn()
+ } as any
+
+ const getCalls = () =>
+ Object.keys(myDir).map(key => myDir[key].mock.calls.length)
+
+ const vm = new Vue({
+ data() {
+ return {
+ ok: true,
+ foo: 1
+ }
+ },
+ template: `
`,
+ directives: {
+ myDir
+ }
+ }).$mount() as any
+
+ expect(getCalls()).toMatchObject([1, 1, 0, 0, 0])
+
+ expect(
+ (deprecationData[DeprecationTypes.CUSTOM_DIR].message as Function)(
+ 'bind',
+ 'beforeMount'
+ )
+ ).toHaveBeenWarned()
+ expect(
+ (deprecationData[DeprecationTypes.CUSTOM_DIR].message as Function)(
+ 'inserted',
+ 'mounted'
+ )
+ ).toHaveBeenWarned()
+
+ vm.foo++
+ await nextTick()
+ expect(getCalls()).toMatchObject([1, 1, 1, 1, 0])
+
+ expect(
+ (deprecationData[DeprecationTypes.CUSTOM_DIR].message as Function)(
+ 'update',
+ 'updated'
+ )
+ ).toHaveBeenWarned()
+ expect(
+ (deprecationData[DeprecationTypes.CUSTOM_DIR].message as Function)(
+ 'componentUpdated',
+ 'updated'
+ )
+ ).toHaveBeenWarned()
+})
+
+test('ATTR_FALSE_VALUE', () => {
+ const vm = new Vue({
+ template: ``
+ }).$mount()
+ expect(vm.$el.hasAttribute('id')).toBe(false)
+ expect(vm.$el.hasAttribute('foo')).toBe(false)
+ expect(
+ (deprecationData[DeprecationTypes.ATTR_FALSE_VALUE].message as Function)(
+ 'id'
+ )
+ ).toHaveBeenWarned()
+ expect(
+ (deprecationData[DeprecationTypes.ATTR_FALSE_VALUE].message as Function)(
+ 'foo'
+ )
+ ).toHaveBeenWarned()
+})
+
+test('ATTR_ENUMERATED_COERSION', () => {
+ const vm = new Vue({
+ template: ``
+ }).$mount()
+ expect(vm.$el.getAttribute('draggable')).toBe('false')
+ expect(vm.$el.getAttribute('spellcheck')).toBe('true')
+ expect(vm.$el.getAttribute('contenteditable')).toBe('true')
+ expect(
+ (deprecationData[DeprecationTypes.ATTR_ENUMERATED_COERSION]
+ .message as Function)('draggable', null, 'false')
+ ).toHaveBeenWarned()
+ expect(
+ (deprecationData[DeprecationTypes.ATTR_ENUMERATED_COERSION]
+ .message as Function)('spellcheck', 0, 'true')
+ ).toHaveBeenWarned()
+ expect(
+ (deprecationData[DeprecationTypes.ATTR_ENUMERATED_COERSION]
+ .message as Function)('contenteditable', 'foo', 'true')
+ ).toHaveBeenWarned()
+})
diff --git a/packages/runtime-core/src/compat/customDirective.ts b/packages/runtime-core/src/compat/customDirective.ts
index dc854dbca..da351eb08 100644
--- a/packages/runtime-core/src/compat/customDirective.ts
+++ b/packages/runtime-core/src/compat/customDirective.ts
@@ -32,13 +32,13 @@ export function mapCompatDirectiveHook(
if (mappedName) {
if (isArray(mappedName)) {
const hook: DirectiveHook[] = []
- mappedName.forEach(name => {
- const mappedHook = dir[name]
+ mappedName.forEach(mapped => {
+ const mappedHook = dir[mapped]
if (mappedHook) {
softAssertCompatEnabled(
DeprecationTypes.CUSTOM_DIR,
instance,
- mappedName,
+ mapped,
name
)
hook.push(mappedHook)
diff --git a/packages/runtime-dom/src/modules/attrs.ts b/packages/runtime-dom/src/modules/attrs.ts
index fc3ead526..4e1621e92 100644
--- a/packages/runtime-dom/src/modules/attrs.ts
+++ b/packages/runtime-dom/src/modules/attrs.ts
@@ -1,4 +1,9 @@
-import { isSpecialBooleanAttr } from '@vue/shared'
+import { isSpecialBooleanAttr, makeMap, NOOP } from '@vue/shared'
+import {
+ compatUtils,
+ ComponentInternalInstance,
+ DeprecationTypes
+} from '@vue/runtime-core'
export const xlinkNS = 'http://www.w3.org/1999/xlink'
@@ -6,7 +11,8 @@ export function patchAttr(
el: Element,
key: string,
value: any,
- isSVG: boolean
+ isSVG: boolean,
+ instance?: ComponentInternalInstance | null
) {
if (isSVG && key.startsWith('xlink:')) {
if (value == null) {
@@ -15,7 +21,7 @@ export function patchAttr(
el.setAttributeNS(xlinkNS, key, value)
}
} else {
- if (__COMPAT__ && compatCoerceAttr(el, key, value)) {
+ if (__COMPAT__ && compatCoerceAttr(el, key, value, instance)) {
return
}
@@ -31,9 +37,6 @@ export function patchAttr(
}
// 2.x compat
-import { makeMap, NOOP } from '@vue/shared'
-import { compatUtils, DeprecationTypes } from '@vue/runtime-core'
-
const isEnumeratedAttr = __COMPAT__
? /*#__PURE__*/ makeMap('contenteditable,draggable,spellcheck')
: NOOP
@@ -41,7 +44,8 @@ const isEnumeratedAttr = __COMPAT__
export function compatCoerceAttr(
el: Element,
key: string,
- value: unknown
+ value: unknown,
+ instance: ComponentInternalInstance | null = null
): boolean {
if (isEnumeratedAttr(key)) {
const v2CocercedValue =
@@ -54,7 +58,7 @@ export function compatCoerceAttr(
v2CocercedValue &&
compatUtils.softAssertCompatEnabled(
DeprecationTypes.ATTR_ENUMERATED_COERSION,
- null,
+ instance,
key,
value,
v2CocercedValue
@@ -68,7 +72,7 @@ export function compatCoerceAttr(
!isSpecialBooleanAttr(key) &&
compatUtils.softAssertCompatEnabled(
DeprecationTypes.ATTR_FALSE_VALUE,
- null,
+ instance,
key
)
) {
diff --git a/packages/runtime-dom/src/modules/props.ts b/packages/runtime-dom/src/modules/props.ts
index 068701642..01a16a58c 100644
--- a/packages/runtime-dom/src/modules/props.ts
+++ b/packages/runtime-dom/src/modules/props.ts
@@ -2,7 +2,7 @@
// Reason: potentially setting innerHTML.
// This can come from explicit usage of v-html or innerHTML as a prop in render
-import { warn } from '@vue/runtime-core'
+import { warn, DeprecationTypes, compatUtils } from '@vue/runtime-core'
// functions. The user is responsible for using them with only trusted content.
export function patchDOMProp(
@@ -55,6 +55,28 @@ export function patchDOMProp(
}
}
+ if (
+ __COMPAT__ &&
+ value === false &&
+ compatUtils.isCompatEnabled(
+ DeprecationTypes.ATTR_FALSE_VALUE,
+ parentComponent
+ )
+ ) {
+ const type = typeof el[key]
+ if (type === 'string' || type === 'number') {
+ __DEV__ &&
+ compatUtils.warnDeprecation(
+ DeprecationTypes.ATTR_FALSE_VALUE,
+ parentComponent,
+ key
+ )
+ el[key] = type === 'number' ? 0 : ''
+ el.removeAttribute(key)
+ return
+ }
+ }
+
// some properties perform value validation and throw
try {
el[key] = value
diff --git a/packages/runtime-dom/src/patchProp.ts b/packages/runtime-dom/src/patchProp.ts
index b7ae61e21..2754f7426 100644
--- a/packages/runtime-dom/src/patchProp.ts
+++ b/packages/runtime-dom/src/patchProp.ts
@@ -58,7 +58,7 @@ export const patchProp: DOMRendererOptions['patchProp'] = (
} else if (key === 'false-value') {
;(el as any)._falseValue = nextValue
}
- patchAttr(el, key, nextValue, isSVG)
+ patchAttr(el, key, nextValue, isSVG, parentComponent)
}
break
}