]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip(vapor): v-model checkbox, radio, select, dynamic
authorEvan You <evan@vuejs.org>
Sat, 1 Feb 2025 14:21:11 +0000 (22:21 +0800)
committerEvan You <evan@vuejs.org>
Sat, 1 Feb 2025 14:37:34 +0000 (22:37 +0800)
packages/compiler-vapor/src/generators/component.ts
packages/compiler-vapor/src/generators/modelValue.ts [deleted file]
packages/compiler-vapor/src/generators/operation.ts
packages/compiler-vapor/src/generators/vModel.ts
packages/compiler-vapor/src/ir/index.ts
packages/compiler-vapor/src/transforms/vModel.ts
packages/runtime-dom/src/directives/vModel.ts
packages/runtime-dom/src/index.ts
packages/runtime-vapor/src/directives/vModel.ts

index cb92aa5968c4e51115858bdc35cc6fe25c730de8..a010b788f8ecb95374048fbb7905b2c156fdb3eb 100644 (file)
@@ -37,8 +37,8 @@ import {
 } from '@vue/compiler-core'
 import { genEventHandler } from './event'
 import { genDirectiveModifiers, genDirectivesForElement } from './directive'
-import { genModelHandler } from './modelValue'
 import { genBlock } from './block'
+import { genModelHandler } from './vModel'
 
 export function genCreateComponent(
   operation: CreateComponentIRNode,
@@ -223,7 +223,7 @@ function genModelEvent(prop: IRProp, context: CodegenContext): CodeFragment[] {
     : ['["onUpdate:" + ', ...genExpression(prop.key, context), ']']
   const handler = genModelHandler(prop.values[0], context)
 
-  return [',', NEWLINE, ...name, ': ', ...handler]
+  return [',', NEWLINE, ...name, ': () => ', ...handler]
 }
 
 function genModelModifiers(
diff --git a/packages/compiler-vapor/src/generators/modelValue.ts b/packages/compiler-vapor/src/generators/modelValue.ts
deleted file mode 100644 (file)
index a12bab0..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-import { camelize } from '@vue/shared'
-import { genExpression } from './expression'
-import type { SetModelValueIRNode } from '../ir'
-import type { CodegenContext } from '../generate'
-import { type CodeFragment, NEWLINE, genCall } from './utils'
-import type { SimpleExpressionNode } from '@vue/compiler-dom'
-
-export function genSetModelValue(
-  oper: SetModelValueIRNode,
-  context: CodegenContext,
-): CodeFragment[] {
-  const { helper } = context
-  const name = oper.key.isStatic
-    ? [JSON.stringify(`update:${camelize(oper.key.content)}`)]
-    : ['`update:${', ...genExpression(oper.key, context), '}`']
-
-  const handler = genModelHandler(oper.value, context)
-
-  return [
-    NEWLINE,
-    ...genCall(helper('delegate'), `n${oper.element}`, name, handler),
-  ]
-}
-
-export function genModelHandler(
-  value: SimpleExpressionNode,
-  context: CodegenContext,
-): CodeFragment[] {
-  const {
-    options: { isTS },
-  } = context
-
-  return [
-    `() => ${isTS ? `($event: any)` : `$event`} => (`,
-    ...genExpression(value, context, '$event'),
-    ')',
-  ]
-}
index 5159348fd160ff59308ec153ef8886235f039f2e..997b2655ec10c9bef0ddaaf8b7f59de7bc3e6db5 100644 (file)
@@ -5,7 +5,6 @@ import { genSetDynamicEvents, genSetEvent } from './event'
 import { genFor } from './for'
 import { genSetHtml } from './html'
 import { genIf } from './if'
-import { genSetModelValue } from './modelValue'
 import { genDynamicProps, genSetProp } from './prop'
 import { genDeclareOldRef, genSetTemplateRef } from './templateRef'
 import { genCreateTextNode, genSetText } from './text'
@@ -51,8 +50,6 @@ export function genOperation(
       return genSetHtml(oper, context)
     case IRNodeTypes.SET_TEMPLATE_REF:
       return genSetTemplateRef(oper, context)
-    case IRNodeTypes.SET_MODEL_VALUE:
-      return genSetModelValue(oper, context)
     case IRNodeTypes.CREATE_TEXT_NODE:
       return genCreateTextNode(oper, context)
     case IRNodeTypes.INSERT_NODE:
index 8ebd365762ba5dc19228da3f07b871f8a14d9358..a7082d91464e8674e2e98668a8aef03312d83e30 100644 (file)
@@ -2,6 +2,7 @@ import type { CodegenContext } from '../generate'
 import type { DirectiveIRNode } from '../ir'
 import { type CodeFragment, NEWLINE, genCall } from './utils'
 import { genExpression } from './expression'
+import type { SimpleExpressionNode } from '@vue/compiler-dom'
 
 const helperMap = {
   text: 'applyTextModel',
@@ -30,11 +31,7 @@ export function genVModel(
       // getter
       [`() => (`, ...genExpression(exp!, context), `)`],
       // setter
-      [
-        `${context.options.isTS ? `($event: any)` : `$event`} => (`,
-        ...genExpression(exp!, context, '$event'),
-        ')',
-      ],
+      genModelHandler(exp!, context),
       // modifiers
       modifiers.length
         ? `{ ${modifiers.map(e => e.content + ': true').join(',')} }`
@@ -42,3 +39,14 @@ export function genVModel(
     ),
   ]
 }
+
+export function genModelHandler(
+  exp: SimpleExpressionNode,
+  context: CodegenContext,
+): CodeFragment[] {
+  return [
+    `${context.options.isTS ? `(_value: any)` : `_value`} => (`,
+    ...genExpression(exp, context, '_value'),
+    ')',
+  ]
+}
index f51591f374d9c93717068e114d7d74f8dab352cf..cf45a60139fb11019a0b67f22320d6b423b3ba3b 100644 (file)
@@ -1,5 +1,4 @@
 import type {
-  BindingTypes,
   CompoundExpressionNode,
   DirectiveNode,
   RootNode,
@@ -23,7 +22,6 @@ export enum IRNodeTypes {
   SET_DYNAMIC_EVENTS,
   SET_HTML,
   SET_TEMPLATE_REF,
-  SET_MODEL_VALUE,
 
   INSERT_NODE,
   PREPEND_NODE,
@@ -154,15 +152,6 @@ export interface SetTemplateRefIRNode extends BaseIRNode {
   effect: boolean
 }
 
-export interface SetModelValueIRNode extends BaseIRNode {
-  type: IRNodeTypes.SET_MODEL_VALUE
-  element: number
-  key: SimpleExpressionNode
-  value: SimpleExpressionNode
-  bindingType?: BindingTypes
-  isComponent: boolean
-}
-
 export interface CreateTextNodeIRNode extends BaseIRNode {
   type: IRNodeTypes.CREATE_TEXT_NODE
   id: number
@@ -227,7 +216,6 @@ export type OperationNode =
   | SetDynamicEventsIRNode
   | SetHtmlIRNode
   | SetTemplateRefIRNode
-  | SetModelValueIRNode
   | CreateTextNodeIRNode
   | InsertNodeIRNode
   | PrependNodeIRNode
index e616954ebe5b6dd7f9f6aefe67602a499b372e83..e92f1bcebff1fdd5ca866061c63f553aa78d3751 100644 (file)
@@ -139,15 +139,6 @@ export const transformVModel: DirectiveTransform = (dir, node, context) => {
     )
   }
 
-  // TODO this should no longer be needed
-  context.registerOperation({
-    type: IRNodeTypes.SET_MODEL_VALUE,
-    element: context.reference(),
-    key: arg || createSimpleExpression('modelValue', true),
-    value: exp,
-    isComponent,
-  })
-
   if (modelType)
     context.registerOperation({
       type: IRNodeTypes.DIRECTIVE,
index 41e33a68afcc21324b38a32856fce25b0a07d655..256b9155923045c2275c8a8a6abd913b8c4cba42 100644 (file)
@@ -52,9 +52,9 @@ export const vModelText: ModelDirective<
   'trim' | 'number' | 'lazy'
 > = {
   created(el, { modifiers: { lazy, trim, number } }, vnode) {
+    el[assignKey] = getModelAssigner(vnode)
     vModelTextInit(
       el,
-      (el[assignKey] = getModelAssigner(vnode)),
       trim,
       number || !!(vnode.props && vnode.props.type === 'number'),
       lazy,
@@ -70,7 +70,7 @@ export const vModelText: ModelDirective<
     vnode,
   ) {
     el[assignKey] = getModelAssigner(vnode)
-    vModelTextUpdate(el, value, oldValue, trim, number, lazy)
+    vModelTextUpdate(el, oldValue, value, trim, number, lazy)
   },
 }
 
@@ -79,10 +79,10 @@ export const vModelText: ModelDirective<
  */
 export const vModelTextInit = (
   el: HTMLInputElement | HTMLTextAreaElement,
-  set: (v: any) => void,
   trim: boolean | undefined,
   number: boolean | undefined,
   lazy: boolean | undefined,
+  set?: (v: any) => void,
 ): void => {
   addEventListener(el, lazy ? 'change' : 'input', e => {
     if ((e.target as any).composing) return
@@ -93,7 +93,7 @@ export const vModelTextInit = (
     if (number) {
       domValue = looseToNumber(domValue)
     }
-    set(domValue)
+    ;(set || (el as any)[assignKey])(domValue)
   })
   if (trim) {
     addEventListener(el, 'change', () => {
@@ -116,8 +116,8 @@ export const vModelTextInit = (
  */
 export const vModelTextUpdate = (
   el: HTMLInputElement | HTMLTextAreaElement,
-  value: any,
   oldValue: any,
+  value: any,
   trim: boolean | undefined,
   number: boolean | undefined,
   lazy: boolean | undefined,
@@ -152,56 +152,82 @@ export const vModelCheckbox: ModelDirective<HTMLInputElement> = {
   deep: true,
   created(el, _, vnode) {
     el[assignKey] = getModelAssigner(vnode)
-    addEventListener(el, 'change', () => {
-      const modelValue = (el as any)._modelValue
-      const elementValue = getValue(el)
-      const checked = el.checked
-      const assign = el[assignKey]
-      if (isArray(modelValue)) {
-        const index = looseIndexOf(modelValue, elementValue)
-        const found = index !== -1
-        if (checked && !found) {
-          assign(modelValue.concat(elementValue))
-        } else if (!checked && found) {
-          const filtered = [...modelValue]
-          filtered.splice(index, 1)
-          assign(filtered)
-        }
-      } else if (isSet(modelValue)) {
-        const cloned = new Set(modelValue)
-        if (checked) {
-          cloned.add(elementValue)
-        } else {
-          cloned.delete(elementValue)
-        }
-        assign(cloned)
-      } else {
-        assign(getCheckboxValue(el, checked))
-      }
-    })
+    vModelCheckboxInit(el)
   },
   // set initial checked on mount to wait for true-value/false-value
-  mounted: setChecked,
+  mounted(el, binding, vnode) {
+    vModelCheckboxUpdate(
+      el,
+      binding.oldValue,
+      binding.value,
+      vnode.props!.value,
+    )
+  },
   beforeUpdate(el, binding, vnode) {
     el[assignKey] = getModelAssigner(vnode)
-    setChecked(el, binding, vnode)
+    vModelCheckboxUpdate(
+      el,
+      binding.oldValue,
+      binding.value,
+      vnode.props!.value,
+    )
   },
 }
 
-function setChecked(
+/**
+ * @internal
+ */
+export const vModelCheckboxInit = (
   el: HTMLInputElement,
-  { value, oldValue }: DirectiveBinding,
-  vnode: VNode,
-) {
+  set?: (v: any) => void,
+): void => {
+  addEventListener(el, 'change', () => {
+    const assign = set || (el as any)[assignKey]
+    const modelValue = (el as any)._modelValue
+    const elementValue = getValue(el)
+    const checked = el.checked
+    if (isArray(modelValue)) {
+      const index = looseIndexOf(modelValue, elementValue)
+      const found = index !== -1
+      if (checked && !found) {
+        assign(modelValue.concat(elementValue))
+      } else if (!checked && found) {
+        const filtered = [...modelValue]
+        filtered.splice(index, 1)
+        assign(filtered)
+      }
+    } else if (isSet(modelValue)) {
+      const cloned = new Set(modelValue)
+      if (checked) {
+        cloned.add(elementValue)
+      } else {
+        cloned.delete(elementValue)
+      }
+      assign(cloned)
+    } else {
+      assign(getCheckboxValue(el, checked))
+    }
+  })
+}
+
+/**
+ * @internal
+ */
+export const vModelCheckboxUpdate = (
+  el: HTMLInputElement,
+  oldValue: any,
+  value: any,
+  rawValue: any = getValue(el),
+): void => {
   // store the v-model value on the element so it can be accessed by the
   // change listener.
   ;(el as any)._modelValue = value
   let checked: boolean
 
   if (isArray(value)) {
-    checked = looseIndexOf(value, vnode.props!.value) > -1
+    checked = looseIndexOf(value, rawValue) > -1
   } else if (isSet(value)) {
-    checked = value.has(vnode.props!.value)
+    checked = value.has(rawValue)
   } else {
     if (value === oldValue) return
     checked = looseEqual(value, getCheckboxValue(el, true))
@@ -233,43 +259,57 @@ export const vModelSelect: ModelDirective<HTMLSelectElement, 'number'> = {
   // <select multiple> value need to be deep traversed
   deep: true,
   created(el, { value, modifiers: { number } }, vnode) {
-    const isSetModel = isSet(value)
-    addEventListener(el, 'change', () => {
-      const selectedVal = Array.prototype.filter
-        .call(el.options, (o: HTMLOptionElement) => o.selected)
-        .map((o: HTMLOptionElement) =>
-          number ? looseToNumber(getValue(o)) : getValue(o),
-        )
-      el[assignKey](
-        el.multiple
-          ? isSetModel
-            ? new Set(selectedVal)
-            : selectedVal
-          : selectedVal[0],
-      )
-      el._assigning = true
-      nextTick(() => {
-        el._assigning = false
-      })
-    })
+    vModelSelectInit(el, value, number)
     el[assignKey] = getModelAssigner(vnode)
   },
   // set value in mounted & updated because <select> relies on its children
   // <option>s.
   mounted(el, { value }) {
-    setSelected(el, value)
+    vModelSetSelected(el, value)
   },
   beforeUpdate(el, _binding, vnode) {
     el[assignKey] = getModelAssigner(vnode)
   },
   updated(el, { value }) {
-    if (!el._assigning) {
-      setSelected(el, value)
-    }
+    vModelSetSelected(el, value)
   },
 }
 
-function setSelected(el: HTMLSelectElement, value: any) {
+/**
+ * @internal
+ */
+export const vModelSelectInit = (
+  el: HTMLSelectElement & { [assignKey]?: AssignerFn; _assigning?: boolean },
+  value: any,
+  number: boolean | undefined,
+  set?: (v: any) => void,
+): void => {
+  const isSetModel = isSet(value)
+  addEventListener(el, 'change', () => {
+    const selectedVal = Array.prototype.filter
+      .call(el.options, (o: HTMLOptionElement) => o.selected)
+      .map((o: HTMLOptionElement) =>
+        number ? looseToNumber(getValue(o)) : getValue(o),
+      )
+    ;(set || el[assignKey]!)(
+      el.multiple
+        ? isSetModel
+          ? new Set(selectedVal)
+          : selectedVal
+        : selectedVal[0],
+    )
+    el._assigning = true
+    nextTick(() => {
+      el._assigning = false
+    })
+  })
+}
+
+/**
+ * @internal
+ */
+export const vModelSetSelected = (el: HTMLSelectElement, value: any): void => {
+  if ((el as any)._assigning) return
   const isMultiple = el.multiple
   const isArrayValue = isArray(value)
   if (isMultiple && !isArrayValue && !isSet(value)) {
@@ -306,8 +346,10 @@ function setSelected(el: HTMLSelectElement, value: any) {
   }
 }
 
-// retrieve raw value set via :value bindings
-function getValue(el: HTMLOptionElement | HTMLInputElement) {
+/**
+ * @internal retrieve raw value set via :value bindings
+ */
+export function getValue(el: HTMLOptionElement | HTMLInputElement): any {
   return '_value' in el ? (el as any)._value : el.value
 }
 
index 2ba88006f97f9dddc3241fc67f00458bb8f98c09..71e8fcb2e6310ffcce7f74000ce3ae97f927c831 100644 (file)
@@ -333,4 +333,12 @@ export {
 /**
  * @internal
  */
-export { vModelTextInit, vModelTextUpdate } from './directives/vModel'
+export {
+  vModelTextInit,
+  vModelTextUpdate,
+  vModelCheckboxInit,
+  vModelCheckboxUpdate,
+  getValue as vModelGetValue,
+  vModelSelectInit,
+  vModelSetSelected,
+} from './directives/vModel'
index 48880f6d0be834a10461299b441b349951132f48..12dfb81ff1fdcb390d4507e022600184c1038308 100644 (file)
-import { onMounted, vModelTextInit, vModelTextUpdate } from '@vue/runtime-dom'
+import {
+  onMounted,
+  vModelCheckboxInit,
+  vModelCheckboxUpdate,
+  vModelGetValue,
+  vModelSelectInit,
+  vModelSetSelected,
+  vModelTextInit,
+  vModelTextUpdate,
+} from '@vue/runtime-dom'
 import { renderEffect } from '../renderEffect'
+import { looseEqual } from '@vue/shared'
+import { addEventListener } from '../dom/event'
+import { traverse } from '@vue/reactivity'
 
-type VaporModelDirective<T = Element> = (
+type VaporModelDirective<
+  T extends HTMLElement =
+    | HTMLInputElement
+    | HTMLTextAreaElement
+    | HTMLSelectElement,
+  Modifiers extends string = string,
+> = (
   el: T,
   get: () => any,
   set: (v: any) => void,
-  modifiers?: { number?: true; trim?: true; lazy?: true },
+  modifiers?: { [key in Modifiers]?: true },
 ) => void
 
 export const applyTextModel: VaporModelDirective<
-  HTMLInputElement | HTMLTextAreaElement
+  HTMLInputElement | HTMLTextAreaElement,
+  'trim' | 'number' | 'lazy'
 > = (el, get, set, { trim, number, lazy } = {}) => {
-  vModelTextInit(el, set, trim, number, lazy)
+  vModelTextInit(el, trim, number, lazy, set)
   onMounted(() => {
-    let oldValue: any
+    let value: any
     renderEffect(() => {
-      const value = get()
-      vModelTextUpdate(el, value, oldValue, trim, number, lazy)
-      oldValue = value
+      vModelTextUpdate(el, value, (value = get()), trim, number, lazy)
     })
   })
 }
 
-export const applyRadioModel: VaporModelDirective = (el, get, set) => {}
-export const applyCheckboxModel: VaporModelDirective = (el, get, set) => {}
-export const applySelectModel: VaporModelDirective = (el, get, set) => {}
-export const applyDynamicModel: VaporModelDirective = (el, get, set) => {}
+export const applyCheckboxModel: VaporModelDirective<HTMLInputElement> = (
+  el,
+  get,
+  set,
+) => {
+  vModelCheckboxInit(el, set)
+  onMounted(() => {
+    let value: any
+    renderEffect(() => {
+      vModelCheckboxUpdate(
+        el,
+        value,
+        // #4096 array checkboxes need to be deep traversed
+        traverse((value = get())),
+      )
+    })
+  })
+}
+
+export const applyRadioModel: VaporModelDirective<HTMLInputElement> = (
+  el,
+  get,
+  set,
+) => {
+  addEventListener(el, 'change', () => set(vModelGetValue(el)))
+  onMounted(() => {
+    let value: any
+    renderEffect(() => {
+      if (value !== (value = get())) {
+        el.checked = looseEqual(value, vModelGetValue(el))
+      }
+    })
+  })
+}
+
+export const applySelectModel: VaporModelDirective<
+  HTMLSelectElement,
+  'number'
+> = (el, get, set, modifiers) => {
+  vModelSelectInit(el, get(), modifiers && modifiers.number, set)
+  onMounted(() => {
+    renderEffect(() => vModelSetSelected(el, traverse(get())))
+  })
+}
+
+export const applyDynamicModel: VaporModelDirective = (
+  el,
+  get,
+  set,
+  modifiers,
+) => {
+  let apply: VaporModelDirective<any> = applyTextModel
+  if (el.tagName === 'SELECT') {
+    apply = applySelectModel
+  } else if (el.tagName === 'TEXTAREA') {
+    apply = applyTextModel
+  } else if ((el as HTMLInputElement).type === 'checkbox') {
+    apply = applyCheckboxModel
+  } else if ((el as HTMLInputElement).type === 'radio') {
+    apply = applyRadioModel
+  }
+  apply(el, get, set, modifiers)
+}