mirror of https://github.com/vuejs/core.git
feat: v-model for input & textarea
This commit is contained in:
parent
782d60475d
commit
31e8fa35c0
|
@ -24,6 +24,7 @@ import { transformVShow } from './transforms/vShow'
|
||||||
import { transformRef } from './transforms/transformRef'
|
import { transformRef } from './transforms/transformRef'
|
||||||
import { transformInterpolation } from './transforms/transformInterpolation'
|
import { transformInterpolation } from './transforms/transformInterpolation'
|
||||||
import type { HackOptions } from './ir'
|
import type { HackOptions } from './ir'
|
||||||
|
import { transformVModel } from './transforms/vModel'
|
||||||
|
|
||||||
export type CompilerOptions = HackOptions<BaseCompilerOptions>
|
export type CompilerOptions = HackOptions<BaseCompilerOptions>
|
||||||
|
|
||||||
|
@ -103,6 +104,7 @@ export function getBaseTransformPreset(
|
||||||
html: transformVHtml,
|
html: transformVHtml,
|
||||||
text: transformVText,
|
text: transformVText,
|
||||||
show: transformVShow,
|
show: transformVShow,
|
||||||
|
model: transformVModel,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ import {
|
||||||
type RootIRNode,
|
type RootIRNode,
|
||||||
type SetEventIRNode,
|
type SetEventIRNode,
|
||||||
type SetHtmlIRNode,
|
type SetHtmlIRNode,
|
||||||
|
type SetModelValueIRNode,
|
||||||
type SetPropIRNode,
|
type SetPropIRNode,
|
||||||
type SetRefIRNode,
|
type SetRefIRNode,
|
||||||
type SetTextIRNode,
|
type SetTextIRNode,
|
||||||
|
@ -389,6 +390,8 @@ function genOperation(oper: OperationNode, context: CodegenContext) {
|
||||||
return genSetHtml(oper, context)
|
return genSetHtml(oper, context)
|
||||||
case IRNodeTypes.SET_REF:
|
case IRNodeTypes.SET_REF:
|
||||||
return genSetRef(oper, context)
|
return genSetRef(oper, context)
|
||||||
|
case IRNodeTypes.SET_MODEL_VALUE:
|
||||||
|
return genSetModelValue(oper, context)
|
||||||
case IRNodeTypes.CREATE_TEXT_NODE:
|
case IRNodeTypes.CREATE_TEXT_NODE:
|
||||||
return genCreateTextNode(oper, context)
|
return genCreateTextNode(oper, context)
|
||||||
case IRNodeTypes.INSERT_NODE:
|
case IRNodeTypes.INSERT_NODE:
|
||||||
|
@ -453,6 +456,34 @@ function genSetRef(oper: SetRefIRNode, context: CodegenContext) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function genSetModelValue(oper: SetModelValueIRNode, context: CodegenContext) {
|
||||||
|
const { vaporHelper, push, newline, pushFnCall } = context
|
||||||
|
|
||||||
|
newline()
|
||||||
|
pushFnCall(
|
||||||
|
vaporHelper('on'),
|
||||||
|
// 1st arg: event name
|
||||||
|
() => push(`n${oper.element}`),
|
||||||
|
// 2nd arg: event name
|
||||||
|
() => {
|
||||||
|
if (isString(oper.key)) {
|
||||||
|
push(JSON.stringify(`update:${camelize(oper.key)}`))
|
||||||
|
} else {
|
||||||
|
push('`update:${')
|
||||||
|
genExpression(oper.key, context)
|
||||||
|
push('}`')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 3rd arg: event handler
|
||||||
|
() => {
|
||||||
|
push((context.isTS ? `($event: any)` : `$event`) + ' => ((')
|
||||||
|
// TODO handle not a ref
|
||||||
|
genExpression(oper.value, context)
|
||||||
|
push(') = $event)')
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function genCreateTextNode(
|
function genCreateTextNode(
|
||||||
oper: CreateTextNodeIRNode,
|
oper: CreateTextNodeIRNode,
|
||||||
context: CodegenContext,
|
context: CodegenContext,
|
||||||
|
@ -576,7 +607,7 @@ function genSetEvent(oper: SetEventIRNode, context: CodegenContext) {
|
||||||
function genWithDirective(oper: WithDirectiveIRNode, context: CodegenContext) {
|
function genWithDirective(oper: WithDirectiveIRNode, context: CodegenContext) {
|
||||||
const { push, newline, pushFnCall, pushMulti, vaporHelper, bindingMetadata } =
|
const { push, newline, pushFnCall, pushMulti, vaporHelper, bindingMetadata } =
|
||||||
context
|
context
|
||||||
const { dir } = oper
|
const { dir, builtin } = oper
|
||||||
|
|
||||||
// TODO merge directive for the same node
|
// TODO merge directive for the same node
|
||||||
newline()
|
newline()
|
||||||
|
@ -591,6 +622,8 @@ function genWithDirective(oper: WithDirectiveIRNode, context: CodegenContext) {
|
||||||
pushMulti(['[', ']', ', '], () => {
|
pushMulti(['[', ']', ', '], () => {
|
||||||
if (dir.name === 'show') {
|
if (dir.name === 'show') {
|
||||||
push(vaporHelper('vShow'))
|
push(vaporHelper('vShow'))
|
||||||
|
} else if (builtin) {
|
||||||
|
push(vaporHelper(builtin))
|
||||||
} else {
|
} else {
|
||||||
const directiveReference = camelize(`v-${dir.name}`)
|
const directiveReference = camelize(`v-${dir.name}`)
|
||||||
// TODO resolve directive
|
// TODO resolve directive
|
||||||
|
|
|
@ -18,6 +18,7 @@ export enum IRNodeTypes {
|
||||||
SET_EVENT,
|
SET_EVENT,
|
||||||
SET_HTML,
|
SET_HTML,
|
||||||
SET_REF,
|
SET_REF,
|
||||||
|
SET_MODEL_VALUE,
|
||||||
|
|
||||||
INSERT_NODE,
|
INSERT_NODE,
|
||||||
PREPEND_NODE,
|
PREPEND_NODE,
|
||||||
|
@ -100,6 +101,14 @@ export interface SetRefIRNode extends BaseIRNode {
|
||||||
value: IRExpression
|
value: IRExpression
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SetModelValueIRNode extends BaseIRNode {
|
||||||
|
type: IRNodeTypes.SET_MODEL_VALUE
|
||||||
|
element: number
|
||||||
|
key: IRExpression
|
||||||
|
value: IRExpression
|
||||||
|
isComponent: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export interface CreateTextNodeIRNode extends BaseIRNode {
|
export interface CreateTextNodeIRNode extends BaseIRNode {
|
||||||
type: IRNodeTypes.CREATE_TEXT_NODE
|
type: IRNodeTypes.CREATE_TEXT_NODE
|
||||||
id: number
|
id: number
|
||||||
|
@ -129,6 +138,7 @@ export interface WithDirectiveIRNode extends BaseIRNode {
|
||||||
type: IRNodeTypes.WITH_DIRECTIVE
|
type: IRNodeTypes.WITH_DIRECTIVE
|
||||||
element: number
|
element: number
|
||||||
dir: VaporDirectiveNode
|
dir: VaporDirectiveNode
|
||||||
|
builtin?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type IRNode =
|
export type IRNode =
|
||||||
|
@ -142,6 +152,7 @@ export type OperationNode =
|
||||||
| SetEventIRNode
|
| SetEventIRNode
|
||||||
| SetHtmlIRNode
|
| SetHtmlIRNode
|
||||||
| SetRefIRNode
|
| SetRefIRNode
|
||||||
|
| SetModelValueIRNode
|
||||||
| CreateTextNodeIRNode
|
| CreateTextNodeIRNode
|
||||||
| InsertNodeIRNode
|
| InsertNodeIRNode
|
||||||
| PrependNodeIRNode
|
| PrependNodeIRNode
|
||||||
|
|
|
@ -3,7 +3,7 @@ import {
|
||||||
type SimpleExpressionNode,
|
type SimpleExpressionNode,
|
||||||
createCompilerError,
|
createCompilerError,
|
||||||
createSimpleExpression,
|
createSimpleExpression,
|
||||||
} from '@vue/compiler-core'
|
} from '@vue/compiler-dom'
|
||||||
import { camelize, isReservedProp } from '@vue/shared'
|
import { camelize, isReservedProp } from '@vue/shared'
|
||||||
import { IRNodeTypes } from '../ir'
|
import { IRNodeTypes } from '../ir'
|
||||||
import type { DirectiveTransform } from '../transform'
|
import type { DirectiveTransform } from '../transform'
|
||||||
|
|
|
@ -0,0 +1,167 @@
|
||||||
|
import {
|
||||||
|
BindingTypes,
|
||||||
|
DOMErrorCodes,
|
||||||
|
ElementTypes,
|
||||||
|
ErrorCodes,
|
||||||
|
NodeTypes,
|
||||||
|
createCompilerError,
|
||||||
|
createDOMCompilerError,
|
||||||
|
findDir,
|
||||||
|
findProp,
|
||||||
|
hasDynamicKeyVBind,
|
||||||
|
isMemberExpression,
|
||||||
|
isStaticArgOf,
|
||||||
|
} from '@vue/compiler-dom'
|
||||||
|
import type { DirectiveTransform } from '../transform'
|
||||||
|
import { IRNodeTypes } from '..'
|
||||||
|
|
||||||
|
export const transformVModel: DirectiveTransform = (dir, node, context) => {
|
||||||
|
const { exp, arg, loc } = dir
|
||||||
|
if (!exp) {
|
||||||
|
context.options.onError(
|
||||||
|
createCompilerError(ErrorCodes.X_V_MODEL_NO_EXPRESSION, dir.loc),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// we assume v-model directives are always parsed
|
||||||
|
// (not artificially created by a transform)
|
||||||
|
const rawExp = exp.loc.source
|
||||||
|
const expString = exp.content
|
||||||
|
|
||||||
|
// in SFC <script setup> inline mode, the exp may have been transformed into
|
||||||
|
// _unref(exp)
|
||||||
|
const bindingType = context.options.bindingMetadata[rawExp]
|
||||||
|
|
||||||
|
// check props
|
||||||
|
if (
|
||||||
|
bindingType === BindingTypes.PROPS ||
|
||||||
|
bindingType === BindingTypes.PROPS_ALIASED
|
||||||
|
) {
|
||||||
|
context.options.onError(
|
||||||
|
createCompilerError(ErrorCodes.X_V_MODEL_ON_PROPS, exp.loc),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const maybeRef =
|
||||||
|
!__BROWSER__ &&
|
||||||
|
context.options.inline &&
|
||||||
|
(bindingType === BindingTypes.SETUP_LET ||
|
||||||
|
bindingType === BindingTypes.SETUP_REF ||
|
||||||
|
bindingType === BindingTypes.SETUP_MAYBE_REF)
|
||||||
|
|
||||||
|
if (
|
||||||
|
!expString.trim() ||
|
||||||
|
(!isMemberExpression(expString, context.options) && !maybeRef)
|
||||||
|
) {
|
||||||
|
context.options.onError(
|
||||||
|
createCompilerError(ErrorCodes.X_V_MODEL_MALFORMED_EXPRESSION, exp.loc),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const isComponent = node.tagType === ElementTypes.COMPONENT
|
||||||
|
let runtimeDirective: string | undefined
|
||||||
|
|
||||||
|
if (isComponent) {
|
||||||
|
if (dir.arg)
|
||||||
|
context.options.onError(
|
||||||
|
createDOMCompilerError(
|
||||||
|
DOMErrorCodes.X_V_MODEL_ARG_ON_ELEMENT,
|
||||||
|
dir.arg.loc,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
const { tag } = node
|
||||||
|
const isCustomElement = context.options.isCustomElement(tag)
|
||||||
|
runtimeDirective = 'vModelText'
|
||||||
|
if (
|
||||||
|
tag === 'input' ||
|
||||||
|
tag === 'textarea' ||
|
||||||
|
tag === 'select' ||
|
||||||
|
isCustomElement
|
||||||
|
) {
|
||||||
|
if (tag === 'input' || isCustomElement) {
|
||||||
|
const type = findProp(node, 'type')
|
||||||
|
if (type) {
|
||||||
|
if (type.type === NodeTypes.DIRECTIVE) {
|
||||||
|
// :type="foo"
|
||||||
|
runtimeDirective = 'vModelDynamic'
|
||||||
|
} else if (type.value) {
|
||||||
|
switch (type.value.content) {
|
||||||
|
case 'radio':
|
||||||
|
runtimeDirective = 'vModelRadio'
|
||||||
|
break
|
||||||
|
case 'checkbox':
|
||||||
|
runtimeDirective = 'vModelCheckbox'
|
||||||
|
break
|
||||||
|
case 'file':
|
||||||
|
runtimeDirective = undefined
|
||||||
|
context.options.onError(
|
||||||
|
createDOMCompilerError(
|
||||||
|
DOMErrorCodes.X_V_MODEL_ON_FILE_INPUT_ELEMENT,
|
||||||
|
dir.loc,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
// text type
|
||||||
|
__DEV__ && checkDuplicatedValue()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (hasDynamicKeyVBind(node)) {
|
||||||
|
// element has bindings with dynamic keys, which can possibly contain
|
||||||
|
// "type".
|
||||||
|
runtimeDirective = 'vModelDynamic'
|
||||||
|
} else {
|
||||||
|
// text type
|
||||||
|
__DEV__ && checkDuplicatedValue()
|
||||||
|
}
|
||||||
|
} else if (tag === 'select') {
|
||||||
|
runtimeDirective = 'vModelSelect'
|
||||||
|
} else {
|
||||||
|
// textarea
|
||||||
|
__DEV__ && checkDuplicatedValue()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
context.options.onError(
|
||||||
|
createDOMCompilerError(
|
||||||
|
DOMErrorCodes.X_V_MODEL_ON_INVALID_ELEMENT,
|
||||||
|
dir.loc,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context.registerOperation({
|
||||||
|
type: IRNodeTypes.SET_MODEL_VALUE,
|
||||||
|
element: context.reference(),
|
||||||
|
key: (arg && arg.isStatic ? arg.content : arg) || 'modelValue',
|
||||||
|
value: exp,
|
||||||
|
isComponent,
|
||||||
|
loc: loc,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (runtimeDirective)
|
||||||
|
context.registerOperation({
|
||||||
|
type: IRNodeTypes.WITH_DIRECTIVE,
|
||||||
|
element: context.reference(),
|
||||||
|
dir,
|
||||||
|
loc,
|
||||||
|
builtin: runtimeDirective,
|
||||||
|
})
|
||||||
|
|
||||||
|
function checkDuplicatedValue() {
|
||||||
|
const value = findDir(node, 'bind')
|
||||||
|
if (value && isStaticArgOf(value.arg, 'value')) {
|
||||||
|
context.options.onError(
|
||||||
|
createDOMCompilerError(
|
||||||
|
DOMErrorCodes.X_V_MODEL_UNNECESSARY_VALUE,
|
||||||
|
value.loc,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,7 +2,7 @@ import {
|
||||||
ElementTypes,
|
ElementTypes,
|
||||||
ErrorCodes,
|
ErrorCodes,
|
||||||
createCompilerError,
|
createCompilerError,
|
||||||
} from '@vue/compiler-core'
|
} from '@vue/compiler-dom'
|
||||||
import type { DirectiveTransform } from '../transform'
|
import type { DirectiveTransform } from '../transform'
|
||||||
import { IRNodeTypes, type KeyOverride, type SetEventIRNode } from '../ir'
|
import { IRNodeTypes, type KeyOverride, type SetEventIRNode } from '../ir'
|
||||||
import { resolveModifiers } from '@vue/compiler-dom'
|
import { resolveModifiers } from '@vue/compiler-dom'
|
||||||
|
|
|
@ -27,6 +27,10 @@ export interface ObjectComponent {
|
||||||
|
|
||||||
type LifecycleHook<TFn = Function> = TFn[] | null
|
type LifecycleHook<TFn = Function> = TFn[] | null
|
||||||
|
|
||||||
|
export interface ElementMetadata {
|
||||||
|
props: Data
|
||||||
|
}
|
||||||
|
|
||||||
export interface ComponentInternalInstance {
|
export interface ComponentInternalInstance {
|
||||||
uid: number
|
uid: number
|
||||||
container: ParentNode
|
container: ParentNode
|
||||||
|
@ -43,6 +47,7 @@ export interface ComponentInternalInstance {
|
||||||
|
|
||||||
/** directives */
|
/** directives */
|
||||||
dirs: Map<Node, DirectiveBinding[]>
|
dirs: Map<Node, DirectiveBinding[]>
|
||||||
|
metadata: WeakMap<Node, ElementMetadata>
|
||||||
|
|
||||||
// lifecycle
|
// lifecycle
|
||||||
isMounted: boolean
|
isMounted: boolean
|
||||||
|
@ -151,6 +156,7 @@ export const createComponentInstance = (
|
||||||
setupState: EMPTY_OBJ,
|
setupState: EMPTY_OBJ,
|
||||||
|
|
||||||
dirs: new Map(),
|
dirs: new Map(),
|
||||||
|
metadata: new WeakMap(),
|
||||||
|
|
||||||
// lifecycle
|
// lifecycle
|
||||||
isMounted: false,
|
isMounted: false,
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { renderWatch } from './renderWatch'
|
||||||
export type DirectiveModifiers<M extends string = string> = Record<M, boolean>
|
export type DirectiveModifiers<M extends string = string> = Record<M, boolean>
|
||||||
|
|
||||||
export interface DirectiveBinding<V = any, M extends string = string> {
|
export interface DirectiveBinding<V = any, M extends string = string> {
|
||||||
instance: ComponentInternalInstance | null
|
instance: ComponentInternalInstance
|
||||||
source?: () => V
|
source?: () => V
|
||||||
value: V
|
value: V
|
||||||
oldValue: V | null
|
oldValue: V | null
|
||||||
|
|
|
@ -0,0 +1,100 @@
|
||||||
|
import type { ComponentInternalInstance } from '../component'
|
||||||
|
import type { ObjectDirective } from '../directive'
|
||||||
|
import { on } from '../dom/on'
|
||||||
|
import { invokeArrayFns, isArray, looseToNumber } from '@vue/shared'
|
||||||
|
|
||||||
|
type AssignerFn = (value: any) => void
|
||||||
|
|
||||||
|
function getModelAssigner(
|
||||||
|
el: Element,
|
||||||
|
instance: ComponentInternalInstance,
|
||||||
|
): AssignerFn {
|
||||||
|
const metadata = instance.metadata.get(el)!
|
||||||
|
const fn: any = metadata.props['onUpdate:modelValue']
|
||||||
|
return isArray(fn) ? (value) => invokeArrayFns(fn, value) : fn
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCompositionStart(e: Event) {
|
||||||
|
;(e.target as any).composing = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCompositionEnd(e: Event) {
|
||||||
|
const target = e.target as any
|
||||||
|
if (target.composing) {
|
||||||
|
target.composing = false
|
||||||
|
target.dispatchEvent(new Event('input'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const assignKeyMap = new WeakMap<HTMLElement, AssignerFn>()
|
||||||
|
|
||||||
|
// 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
|
||||||
|
> = {
|
||||||
|
beforeMount(el, { instance, modifiers: { lazy, trim, number } = {} }) {
|
||||||
|
const assigner = getModelAssigner(el, instance)
|
||||||
|
assignKeyMap.set(el, assigner)
|
||||||
|
|
||||||
|
const castToNumber = number // || (vnode.props && vnode.props.type === 'number')
|
||||||
|
on(el, lazy ? 'change' : 'input', (e) => {
|
||||||
|
if ((e.target as any).composing) return
|
||||||
|
let domValue: string | number = el.value
|
||||||
|
if (trim) {
|
||||||
|
domValue = domValue.trim()
|
||||||
|
}
|
||||||
|
if (castToNumber) {
|
||||||
|
domValue = looseToNumber(domValue)
|
||||||
|
}
|
||||||
|
assigner(domValue)
|
||||||
|
})
|
||||||
|
if (trim) {
|
||||||
|
on(el, 'change', () => {
|
||||||
|
el.value = el.value.trim()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (!lazy) {
|
||||||
|
on(el, 'compositionstart', onCompositionStart)
|
||||||
|
on(el, 'compositionend', onCompositionEnd)
|
||||||
|
// Safari < 10.2 & UIWebView doesn't fire compositionend when
|
||||||
|
// switching focus before confirming composition choice
|
||||||
|
// this also fixes the issue where some browsers e.g. iOS Chrome
|
||||||
|
// fires "change" instead of "input" on autocomplete.
|
||||||
|
on(el, 'change', onCompositionEnd)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// set value on mounted so it's after min/max for type="range"
|
||||||
|
mounted(el, { value }) {
|
||||||
|
el.value = value == null ? '' : value
|
||||||
|
},
|
||||||
|
beforeUpdate(
|
||||||
|
el,
|
||||||
|
{ instance, value, modifiers: { lazy, trim, number } = {} },
|
||||||
|
) {
|
||||||
|
assignKeyMap.set(el, getModelAssigner(el, instance))
|
||||||
|
|
||||||
|
// avoid clearing unresolved text. #2302
|
||||||
|
if ((el as any).composing) return
|
||||||
|
|
||||||
|
const elValue =
|
||||||
|
number || el.type === 'number' ? looseToNumber(el.value) : el.value
|
||||||
|
const newValue = value == null ? '' : value
|
||||||
|
|
||||||
|
if (elValue === newValue) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals
|
||||||
|
if (document.activeElement === el && el.type !== 'range') {
|
||||||
|
if (lazy) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (trim && el.value.trim() === newValue) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
el.value = newValue
|
||||||
|
},
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ import type { Block, ParentBlock } from './render'
|
||||||
|
|
||||||
export * from './dom/patchProp'
|
export * from './dom/patchProp'
|
||||||
export * from './dom/templateRef'
|
export * from './dom/templateRef'
|
||||||
|
export * from './dom/on'
|
||||||
|
|
||||||
export function insert(block: Block, parent: Node, anchor: Node | null = null) {
|
export function insert(block: Block, parent: Node, anchor: Node | null = null) {
|
||||||
// if (!isHydrating) {
|
// if (!isHydrating) {
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
import { getCurrentEffect, onEffectCleanup } from '@vue/reactivity'
|
import { getCurrentEffect, onEffectCleanup } from '@vue/reactivity'
|
||||||
|
import { recordPropMetadata } from './patchProp'
|
||||||
|
import { toHandlerKey } from '@vue/shared'
|
||||||
|
|
||||||
export function on(
|
export function on(
|
||||||
el: HTMLElement,
|
el: HTMLElement,
|
||||||
event: string,
|
event: string,
|
||||||
handler: () => any,
|
handler: (...args: any) => any,
|
||||||
options?: AddEventListenerOptions,
|
options?: AddEventListenerOptions,
|
||||||
) {
|
) {
|
||||||
|
recordPropMetadata(el, toHandlerKey(event), handler)
|
||||||
el.addEventListener(event, handler, options)
|
el.addEventListener(event, handler, options)
|
||||||
if (getCurrentEffect()) {
|
if (getCurrentEffect()) {
|
||||||
onEffectCleanup(() => el.removeEventListener(event, handler, options))
|
onEffectCleanup(() => el.removeEventListener(event, handler, options))
|
|
@ -4,15 +4,18 @@ import {
|
||||||
normalizeClass,
|
normalizeClass,
|
||||||
normalizeStyle,
|
normalizeStyle,
|
||||||
} from '@vue/shared'
|
} from '@vue/shared'
|
||||||
|
import { currentInstance } from '../component'
|
||||||
|
|
||||||
export function setClass(el: Element, oldVal: any, newVal: any) {
|
export function setClass(el: Element, oldVal: any, newVal: any) {
|
||||||
if ((newVal = normalizeClass(newVal)) !== oldVal && (newVal || oldVal)) {
|
if ((newVal = normalizeClass(newVal)) !== oldVal && (newVal || oldVal)) {
|
||||||
|
recordPropMetadata(el, 'class', newVal)
|
||||||
el.className = newVal
|
el.className = newVal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setStyle(el: HTMLElement, oldVal: any, newVal: any) {
|
export function setStyle(el: HTMLElement, oldVal: any, newVal: any) {
|
||||||
if ((newVal = normalizeStyle(newVal)) !== oldVal && (newVal || oldVal)) {
|
if ((newVal = normalizeStyle(newVal)) !== oldVal && (newVal || oldVal)) {
|
||||||
|
recordPropMetadata(el, 'style', newVal)
|
||||||
if (typeof newVal === 'string') {
|
if (typeof newVal === 'string') {
|
||||||
el.style.cssText = newVal
|
el.style.cssText = newVal
|
||||||
} else {
|
} else {
|
||||||
|
@ -23,6 +26,7 @@ export function setStyle(el: HTMLElement, oldVal: any, newVal: any) {
|
||||||
|
|
||||||
export function setAttr(el: Element, key: string, oldVal: any, newVal: any) {
|
export function setAttr(el: Element, key: string, oldVal: any, newVal: any) {
|
||||||
if (newVal !== oldVal) {
|
if (newVal !== oldVal) {
|
||||||
|
recordPropMetadata(el, key, newVal)
|
||||||
if (newVal != null) {
|
if (newVal != null) {
|
||||||
el.setAttribute(key, newVal)
|
el.setAttribute(key, newVal)
|
||||||
} else {
|
} else {
|
||||||
|
@ -34,6 +38,7 @@ export function setAttr(el: Element, key: string, oldVal: any, newVal: any) {
|
||||||
export function setDOMProp(el: any, key: string, oldVal: any, newVal: any) {
|
export function setDOMProp(el: any, key: string, oldVal: any, newVal: any) {
|
||||||
// TODO special checks
|
// TODO special checks
|
||||||
if (newVal !== oldVal) {
|
if (newVal !== oldVal) {
|
||||||
|
recordPropMetadata(el, key, newVal)
|
||||||
el[key] = newVal
|
el[key] = newVal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -64,6 +69,16 @@ export function setDynamicProp(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function recordPropMetadata(el: Node, key: string, value: any) {
|
||||||
|
if (currentInstance) {
|
||||||
|
let metadata = currentInstance.metadata.get(el)
|
||||||
|
if (!metadata) {
|
||||||
|
currentInstance.metadata.set(el, (metadata = { props: {} }))
|
||||||
|
}
|
||||||
|
metadata.props[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO copied from runtime-dom
|
// TODO copied from runtime-dom
|
||||||
const isNativeOn = (key: string) =>
|
const isNativeOn = (key: string) =>
|
||||||
key.charCodeAt(0) === 111 /* o */ &&
|
key.charCodeAt(0) === 111 /* o */ &&
|
||||||
|
|
|
@ -41,13 +41,14 @@ export { withModifiers, withKeys } from '@vue/runtime-dom'
|
||||||
|
|
||||||
export { nextTick } from './scheduler'
|
export { nextTick } from './scheduler'
|
||||||
export { getCurrentInstance, type ComponentInternalInstance } from './component'
|
export { getCurrentInstance, type ComponentInternalInstance } from './component'
|
||||||
export * from './on'
|
|
||||||
export * from './render'
|
export * from './render'
|
||||||
export * from './renderWatch'
|
export * from './renderWatch'
|
||||||
export * from './template'
|
export * from './template'
|
||||||
export * from './apiWatch'
|
export * from './apiWatch'
|
||||||
export * from './directive'
|
export * from './directive'
|
||||||
export * from './dom'
|
export * from './dom'
|
||||||
export * from './directives/vShow'
|
|
||||||
export * from './apiLifecycle'
|
export * from './apiLifecycle'
|
||||||
export * from './if'
|
export * from './if'
|
||||||
|
|
||||||
|
export * from './directives/vShow'
|
||||||
|
export * from './directives/vModel'
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref } from 'vue/vapor'
|
import { ref } from 'vue/vapor'
|
||||||
|
|
||||||
interface Task {
|
interface Task {
|
||||||
title: string
|
title: string
|
||||||
|
@ -7,7 +7,6 @@ interface Task {
|
||||||
}
|
}
|
||||||
const tasks = ref<Task[]>([])
|
const tasks = ref<Task[]>([])
|
||||||
const value = ref('hello')
|
const value = ref('hello')
|
||||||
const inputRef = ref<HTMLInputElement>()
|
|
||||||
|
|
||||||
function handleAdd() {
|
function handleAdd() {
|
||||||
tasks.value.push({
|
tasks.value.push({
|
||||||
|
@ -17,11 +16,6 @@ function handleAdd() {
|
||||||
// TODO: clear input
|
// TODO: clear input
|
||||||
value.value = ''
|
value.value = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
console.log('onMounted')
|
|
||||||
console.log(inputRef.value)
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -45,12 +39,7 @@ onMounted(() => {
|
||||||
{{ tasks[3]?.title }}
|
{{ tasks[3]?.title }}
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<input
|
<input type="text" v-model="value" />
|
||||||
type="text"
|
|
||||||
:ref="el => (inputRef = el)"
|
|
||||||
:value="value"
|
|
||||||
@input="evt => (value = evt.target.value)"
|
|
||||||
/>
|
|
||||||
<button @click="handleAdd">Add</button>
|
<button @click="handleAdd">Add</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
Loading…
Reference in New Issue