]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip: component v-model compat
authorEvan You <yyx990803@gmail.com>
Sat, 10 Apr 2021 03:10:29 +0000 (23:10 -0400)
committerEvan You <yyx990803@gmail.com>
Sat, 10 Apr 2021 03:10:29 +0000 (23:10 -0400)
packages/runtime-core/src/compat/deprecations.ts
packages/runtime-core/src/compat/vModel.ts [new file with mode: 0644]
packages/runtime-core/src/component.ts
packages/runtime-core/src/componentEmits.ts
packages/runtime-core/src/componentProps.ts
packages/runtime-core/src/vnode.ts

index b2d10f52d48d48788f32b744da9874c8ec5d0fde..8a00ab0e4b6882548c39408bede01bba49db1dcb 100644 (file)
@@ -1,5 +1,7 @@
+import { hasOwn, isArray } from '@vue/shared/src'
 import {
   ComponentInternalInstance,
+  ComponentOptions,
   formatComponentName,
   getComponentName,
   getCurrentInstance,
@@ -52,6 +54,7 @@ export const enum DeprecationTypes {
 
   COMPONENT_ASYNC = 'COMPONENT_ASYNC',
   COMPONENT_FUNCTIONAL = 'COMPONENT_FUNCTIONAL',
+  COMPONENT_V_MODEL = 'COMPONENT_V_MODEL',
 
   RENDER_FUNCTION = 'RENDER_FUNCTION'
 }
@@ -345,6 +348,32 @@ const deprecationData: Record<DeprecationTypes, DeprecationData> = {
     link: `https://v3.vuejs.org/guide/migration/functional-components.html`
   },
 
+  [DeprecationTypes.COMPONENT_V_MODEL]: {
+    message: (comp: ComponentOptions) => {
+      const configMsg =
+        `opt-in to ` +
+        `Vue 3 behavior on a per-component basis with \`compatConfig: { ${
+          DeprecationTypes.COMPONENT_V_MODEL
+        }: false }\`.`
+      if (
+        comp.props && isArray(comp.props)
+          ? comp.props.includes('modelValue')
+          : hasOwn(comp.props, 'modelValue')
+      ) {
+        return (
+          `Component delcares "modelValue" prop, which is Vue 3 usage, but ` +
+          `is running under Vue 2 compat v-model behavior. You can ${configMsg}`
+        )
+      }
+      return (
+        `v-model usage on component has changed in Vue 3. Component that expects ` +
+        `to work with v-model should now use the "modelValue" prop and emit the ` +
+        `"update:modelValue" event. You can update the usage and then ${configMsg}`
+      )
+    },
+    link: `https://v3.vuejs.org/guide/migration/v-model.html`
+  },
+
   [DeprecationTypes.RENDER_FUNCTION]: {
     message:
       `Vue 3's render function API has changed. ` +
diff --git a/packages/runtime-core/src/compat/vModel.ts b/packages/runtime-core/src/compat/vModel.ts
new file mode 100644 (file)
index 0000000..d0d8c6a
--- /dev/null
@@ -0,0 +1,71 @@
+import { ShapeFlags } from '@vue/shared'
+import { ComponentInternalInstance, ComponentOptions } from '../component'
+import { callWithErrorHandling, ErrorCodes } from '../errorHandling'
+import { VNode } from '../vnode'
+import { popWarningContext, pushWarningContext } from '../warning'
+import { isCompatEnabled } from './compatConfig'
+import { DeprecationTypes, warnDeprecation } from './deprecations'
+
+const defaultModelMapping = {
+  prop: 'value',
+  event: 'input'
+}
+
+export const compatModelEventPrefix = `onModelCompat:`
+
+const warnedTypes = new WeakSet()
+
+export function convertLegacyVModelProps(vnode: VNode) {
+  const { type, shapeFlag, props, dynamicProps } = vnode
+  if (shapeFlag & ShapeFlags.COMPONENT && props && 'modelValue' in props) {
+    if (
+      !isCompatEnabled(
+        DeprecationTypes.COMPONENT_V_MODEL,
+        // this is a special case where we want to use the vnode component's
+        // compat config instead of the current rendering instance (which is the
+        // parent of the component that exposes v-model)
+        { type } as any
+      )
+    ) {
+      return
+    }
+
+    if (__DEV__ && !warnedTypes.has(type as ComponentOptions)) {
+      pushWarningContext(vnode)
+      warnDeprecation(DeprecationTypes.COMPONENT_V_MODEL, { type } as any, type)
+      popWarningContext()
+      warnedTypes.add(type as ComponentOptions)
+    }
+
+    const { prop, event } = (type as any).model || defaultModelMapping
+    props[prop] = props.modelValue
+    delete props.modelValue
+    // important: update dynamic props
+    if (dynamicProps) {
+      dynamicProps[dynamicProps.indexOf('modelValue')] = prop
+    }
+
+    props[compatModelEventPrefix + event] = props['onUpdate:modelValue']
+    delete props['onUpdate:modelValue']
+  }
+}
+
+export function compatModelEmit(
+  instance: ComponentInternalInstance,
+  event: string,
+  args: any[]
+) {
+  if (!isCompatEnabled(DeprecationTypes.COMPONENT_V_MODEL, instance)) {
+    return
+  }
+  const props = instance.vnode.props
+  const modelHandler = props && props[compatModelEventPrefix + event]
+  if (modelHandler) {
+    callWithErrorHandling(
+      modelHandler,
+      instance,
+      ErrorCodes.COMPONENT_EVENT_HANDLER,
+      args
+    )
+  }
+}
index 1045db8c899ace62d5746d7b0d49992cce52fd7e..a2be7de7cad27cc5f5f92b053bcd57c1ecf27f22 100644 (file)
@@ -687,9 +687,9 @@ export function finishComponentSetup(
   if (
     __COMPAT__ &&
     Component.render &&
-    isCompatEnabled(DeprecationTypes.RENDER_FUNCTION)
+    isCompatEnabled(DeprecationTypes.RENDER_FUNCTION, instance)
   ) {
-    warnDeprecation(DeprecationTypes.RENDER_FUNCTION)
+    warnDeprecation(DeprecationTypes.RENDER_FUNCTION, instance)
     const originalRender = Component.render
     Component.render = function compatRender() {
       return originalRender.call(this, compatH)
index a4ffdf89b35a2cb2352c4f7c81318b144fee2a64..ca04092a0e3ed1f4f0fe41cfda956ab2e87ac58a 100644 (file)
@@ -21,7 +21,8 @@ import { warn } from './warning'
 import { UnionToIntersection } from './helpers/typeUtils'
 import { devtoolsComponentEmit } from './devtools'
 import { AppContext } from './apiCreateApp'
-import { emit as compatEmit } from './compat/instanceEventEmitter'
+import { emit as compatInstanceEmit } from './compat/instanceEventEmitter'
+import { compatModelEventPrefix, compatModelEmit } from './compat/vModel'
 
 export type ObjectEmitsOptions = Record<
   string,
@@ -57,7 +58,14 @@ export function emit(
       propsOptions: [propsOptions]
     } = instance
     if (emitsOptions) {
-      if (!(event in emitsOptions) && !event.startsWith('hook:')) {
+      if (
+        !(event in emitsOptions) &&
+        !(
+          __COMPAT__ &&
+          (event.startsWith('hook:') ||
+            event.startsWith(compatModelEventPrefix))
+        )
+      ) {
         if (!propsOptions || !(toHandlerKey(event) in propsOptions)) {
           warn(
             `Component emitted event "${event}" but it is neither declared in ` +
@@ -151,7 +159,8 @@ export function emit(
   }
 
   if (__COMPAT__) {
-    return compatEmit(instance, event, args)
+    compatModelEmit(instance, event, args)
+    return compatInstanceEmit(instance, event, args)
   }
 }
 
index 7725b18bb0eaf67ce81ad6beaf285b0f8d0eae8d..46a6b5ef4973c4017145d4761956d2a95c41ccd0 100644 (file)
@@ -348,7 +348,7 @@ function resolvePropValue(
           value = propsDefaults[key] = defaultValue.call(
             __COMPAT__ &&
             __DEV__ &&
-            isCompatEnabled(DeprecationTypes.PROPS_DEFAULT_THIS)
+            isCompatEnabled(DeprecationTypes.PROPS_DEFAULT_THIS, instance)
               ? createPropsDefaultThis(key)
               : null,
             props
index 35aa6678b2e1d7e935e16508c85a5d3b51ce6f07..9d1fd791ab2d5b660487d4917724480cbd45c7fc 100644 (file)
@@ -42,6 +42,7 @@ import { NULL_DYNAMIC_COMPONENT } from './helpers/resolveAssets'
 import { hmrDirtyComponents } from './hmr'
 import { setCompiledSlotRendering } from './helpers/renderSlot'
 import { convertLegacyComponent } from './compat/component'
+import { convertLegacyVModelProps } from './compat/vModel'
 
 export const Fragment = (Symbol(__DEV__ ? 'Fragment' : undefined) as any) as {
   __isFragment: true
@@ -469,6 +470,10 @@ function _createVNode(
     currentBlock.push(vnode)
   }
 
+  if (__COMPAT__) {
+    convertLegacyVModelProps(vnode)
+  }
+
   return vnode
 }