} 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,
: ['["onUpdate:" + ', ...genExpression(prop.key, context), ']']
const handler = genModelHandler(prop.values[0], context)
- return [',', NEWLINE, ...name, ': ', ...handler]
+ return [',', NEWLINE, ...name, ': () => ', ...handler]
}
function genModelModifiers(
+++ /dev/null
-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'),
- ')',
- ]
-}
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'
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:
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',
// 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(',')} }`
),
]
}
+
+export function genModelHandler(
+ exp: SimpleExpressionNode,
+ context: CodegenContext,
+): CodeFragment[] {
+ return [
+ `${context.options.isTS ? `(_value: any)` : `_value`} => (`,
+ ...genExpression(exp, context, '_value'),
+ ')',
+ ]
+}
import type {
- BindingTypes,
CompoundExpressionNode,
DirectiveNode,
RootNode,
SET_DYNAMIC_EVENTS,
SET_HTML,
SET_TEMPLATE_REF,
- SET_MODEL_VALUE,
INSERT_NODE,
PREPEND_NODE,
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
| SetDynamicEventsIRNode
| SetHtmlIRNode
| SetTemplateRefIRNode
- | SetModelValueIRNode
| CreateTextNodeIRNode
| InsertNodeIRNode
| PrependNodeIRNode
)
}
- // 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,
'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,
vnode,
) {
el[assignKey] = getModelAssigner(vnode)
- vModelTextUpdate(el, value, oldValue, trim, number, lazy)
+ vModelTextUpdate(el, oldValue, value, trim, number, lazy)
},
}
*/
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
if (number) {
domValue = looseToNumber(domValue)
}
- set(domValue)
+ ;(set || (el as any)[assignKey])(domValue)
})
if (trim) {
addEventListener(el, 'change', () => {
*/
export const vModelTextUpdate = (
el: HTMLInputElement | HTMLTextAreaElement,
- value: any,
oldValue: any,
+ value: any,
trim: boolean | undefined,
number: boolean | undefined,
lazy: boolean | undefined,
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))
// <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)) {
}
}
-// 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
}
/**
* @internal
*/
-export { vModelTextInit, vModelTextUpdate } from './directives/vModel'
+export {
+ vModelTextInit,
+ vModelTextUpdate,
+ vModelCheckboxInit,
+ vModelCheckboxUpdate,
+ getValue as vModelGetValue,
+ vModelSelectInit,
+ vModelSetSelected,
+} from './directives/vModel'
-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)
+}