فهرست منبع

feat(runtime-vapor): support v-model w/ select tag

三咲智子 Kevin Deng 2 سال پیش
والد
کامیت
ed954bcd33
2فایلهای تغییر یافته به همراه127 افزوده شده و 8 حذف شده
  1. 2 1
      packages/runtime-vapor/src/directive.ts
  2. 125 7
      packages/runtime-vapor/src/directives/vModel.ts

+ 2 - 1
packages/runtime-vapor/src/directive.ts

@@ -36,6 +36,7 @@ export type DirectiveHookName =
 export type ObjectDirective<T = any, V = any, M extends string = string> = {
   [K in DirectiveHookName]?: DirectiveHook<T, V, M> | undefined
 } & {
+  /** Watch value deeply */
   deep?: boolean
 }
 
@@ -100,7 +101,7 @@ export function withDirectives<T extends Node>(
     // register source
     if (source) {
       // callback will be overridden by middleware
-      renderWatch(source, NOOP)
+      renderWatch(source, NOOP, { deep: dir.deep })
     }
   }
 

+ 125 - 7
packages/runtime-vapor/src/directives/vModel.ts

@@ -1,7 +1,16 @@
+import {
+  invokeArrayFns,
+  isArray,
+  isSet,
+  looseEqual,
+  looseIndexOf,
+  looseToNumber,
+} from '@vue/shared'
 import type { ComponentInternalInstance } from '../component'
 import type { ObjectDirective } from '../directive'
 import { on } from '../dom/on'
-import { invokeArrayFns, isArray, looseToNumber } from '@vue/shared'
+import { nextTick } from '../scheduler'
+import { warn } from '../warning'
 
 type AssignerFn = (value: any) => void
 
@@ -26,16 +35,19 @@ function onCompositionEnd(e: Event) {
   }
 }
 
-const assignKeyMap = new WeakMap<HTMLElement, AssignerFn>()
+const assignFnMap = new WeakMap<HTMLElement, AssignerFn>()
+const assigningMap = new WeakMap<HTMLElement, boolean>()
 
 // We are exporting the v-model runtime directly as vnode hooks so that it can
 // be tree-shaken in case v-model is never used.
 export const vModelText: ObjectDirective<
-  HTMLInputElement | HTMLTextAreaElement
+  HTMLInputElement | HTMLTextAreaElement,
+  any,
+  'lazy' | 'trim' | 'number'
 > = {
   beforeMount(el, { instance, modifiers: { lazy, trim, number } = {} }) {
     const assigner = getModelAssigner(el, instance)
-    assignKeyMap.set(el, assigner)
+    assignFnMap.set(el, assigner)
 
     const castToNumber = number // || (vnode.props && vnode.props.type === 'number')
     on(el, lazy ? 'change' : 'input', e => {
@@ -72,7 +84,7 @@ export const vModelText: ObjectDirective<
     el,
     { instance, value, modifiers: { lazy, trim, number } = {} },
   ) {
-    assignKeyMap.set(el, getModelAssigner(el, instance))
+    assignFnMap.set(el, getModelAssigner(el, instance))
 
     // avoid clearing unresolved text. #2302
     if ((el as any).composing) return
@@ -100,7 +112,113 @@ export const vModelText: ObjectDirective<
 }
 
 // TODO
-export const vModelDynamic = {}
 export const vModelRadio = {}
+
+export const vModelSelect: ObjectDirective<HTMLSelectElement, any, 'number'> = {
+  // <select multiple> value need to be deep traversed
+  deep: true,
+  beforeMount(
+    el,
+    { value, oldValue, instance, modifiers: { number = false } = {} },
+  ) {
+    const isSetModel = isSet(value)
+    on(el, 'change', () => {
+      const selectedVal = Array.prototype.filter
+        .call(el.options, (o: HTMLOptionElement) => o.selected)
+        .map((o: HTMLOptionElement) =>
+          number ? looseToNumber(getValue(o, instance)) : getValue(o, instance),
+        )
+      assignFnMap.get(el)!(
+        el.multiple
+          ? isSetModel
+            ? new Set(selectedVal)
+            : selectedVal
+          : selectedVal[0],
+      )
+      assigningMap.set(el, true)
+
+      nextTick(() => {
+        assigningMap.set(el, false)
+      })
+    })
+    assignFnMap.set(el, getModelAssigner(el, instance))
+    setSelected(el, instance, value, oldValue, number)
+  },
+  beforeUpdate(el, { instance }) {
+    assignFnMap.set(el, getModelAssigner(el, instance))
+  },
+  updated(
+    el,
+    { value, oldValue, instance, modifiers: { number = false } = {} },
+  ) {
+    if (!assigningMap.get(el)) {
+      setSelected(el, instance, value, oldValue, number)
+    }
+  },
+}
+
+function setSelected(
+  el: HTMLSelectElement,
+  instance: ComponentInternalInstance,
+  value: any,
+  oldValue: any,
+  number: boolean,
+) {
+  const isMultiple = el.multiple
+  const isArrayValue = isArray(value)
+  if (isMultiple && !isArrayValue && !isSet(value)) {
+    __DEV__ &&
+      warn(
+        `<select multiple v-model> expects an Array or Set value for its binding, ` +
+          `but got ${Object.prototype.toString.call(value).slice(8, -1)}.`,
+      )
+    return
+  }
+
+  // Disable fast path due to https://github.com/vuejs/core/issues/10267
+  // fast path for updates triggered by other changes
+  // if (isArrayValue && looseEqual(value, oldValue)) {
+  //   return
+  // }
+
+  for (let i = 0, l = el.options.length; i < l; i++) {
+    const option = el.options[i]
+    const optionValue = getValue(option, instance)
+    if (isMultiple) {
+      if (isArrayValue) {
+        const optionType = typeof optionValue
+        // fast path for string / number values
+        if (optionType === 'string' || optionType === 'number') {
+          option.selected = value.includes(
+            number ? looseToNumber(optionValue) : optionValue,
+          )
+        } else {
+          option.selected = looseIndexOf(value, optionValue) > -1
+        }
+      } else {
+        option.selected = value.has(optionValue)
+      }
+    } else {
+      if (looseEqual(getValue(option, instance), value)) {
+        if (el.selectedIndex !== i) el.selectedIndex = i
+        return
+      }
+    }
+  }
+  if (!isMultiple && el.selectedIndex !== -1) {
+    el.selectedIndex = -1
+  }
+}
+
+// retrieve raw value set via :value bindings
+function getValue(
+  el: HTMLOptionElement | HTMLInputElement,
+  instance: ComponentInternalInstance,
+) {
+  const metadata = instance.metadata.get(el)
+  return metadata ? metadata.props.value : el.value
+}
+
 export const vModelCheckbox = {}
-export const vModelSelect = {}
+
+export const vModelDynamic = {}