vModel.ts 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. import {
  2. ObjectDirective,
  3. VNode,
  4. DirectiveHook,
  5. DirectiveBinding,
  6. warn
  7. } from '@vue/runtime-core'
  8. import { addEventListener } from '../modules/events'
  9. import {
  10. isArray,
  11. looseEqual,
  12. looseIndexOf,
  13. invokeArrayFns,
  14. toNumber
  15. } from '@vue/shared'
  16. type AssignerFn = (value: any) => void
  17. const getModelAssigner = (vnode: VNode): AssignerFn => {
  18. const fn = vnode.props!['onUpdate:modelValue']
  19. return isArray(fn) ? value => invokeArrayFns(fn, value) : fn
  20. }
  21. function onCompositionStart(e: Event) {
  22. ;(e.target as any).composing = true
  23. }
  24. function onCompositionEnd(e: Event) {
  25. const target = e.target as any
  26. if (target.composing) {
  27. target.composing = false
  28. trigger(target, 'input')
  29. }
  30. }
  31. function trigger(el: HTMLElement, type: string) {
  32. const e = document.createEvent('HTMLEvents')
  33. e.initEvent(type, true, true)
  34. el.dispatchEvent(e)
  35. }
  36. type ModelDirective<T> = ObjectDirective<T & { _assign: AssignerFn }>
  37. // We are exporting the v-model runtime directly as vnode hooks so that it can
  38. // be tree-shaken in case v-model is never used. These are used by compilers
  39. // only and userland code should avoid relying on them.
  40. /**
  41. * @internal
  42. */
  43. export const vModelText: ModelDirective<
  44. HTMLInputElement | HTMLTextAreaElement
  45. > = {
  46. beforeMount(el, { value, modifiers: { lazy, trim, number } }, vnode) {
  47. el.value = value
  48. el._assign = getModelAssigner(vnode)
  49. const castToNumber = number || el.type === 'number'
  50. addEventListener(el, lazy ? 'change' : 'input', e => {
  51. if ((e.target as any).composing) return
  52. let domValue: string | number = el.value
  53. if (trim) {
  54. domValue = domValue.trim()
  55. } else if (castToNumber) {
  56. domValue = toNumber(domValue)
  57. }
  58. el._assign(domValue)
  59. })
  60. if (trim) {
  61. addEventListener(el, 'change', () => {
  62. el.value = el.value.trim()
  63. })
  64. }
  65. if (!lazy) {
  66. addEventListener(el, 'compositionstart', onCompositionStart)
  67. addEventListener(el, 'compositionend', onCompositionEnd)
  68. // Safari < 10.2 & UIWebView doesn't fire compositionend when
  69. // switching focus before confirming composition choice
  70. // this also fixes the issue where some browsers e.g. iOS Chrome
  71. // fires "change" instead of "input" on autocomplete.
  72. addEventListener(el, 'change', onCompositionEnd)
  73. }
  74. },
  75. beforeUpdate(el, { value, oldValue, modifiers: { trim, number } }, vnode) {
  76. el._assign = getModelAssigner(vnode)
  77. if (value === oldValue) {
  78. return
  79. }
  80. if (document.activeElement === el) {
  81. if (trim && el.value.trim() === value) {
  82. return
  83. }
  84. if ((number || el.type === 'number') && toNumber(el.value) === value) {
  85. return
  86. }
  87. }
  88. el.value = value
  89. }
  90. }
  91. /**
  92. * @internal
  93. */
  94. export const vModelCheckbox: ModelDirective<HTMLInputElement> = {
  95. beforeMount(el, binding, vnode) {
  96. setChecked(el, binding, vnode)
  97. el._assign = getModelAssigner(vnode)
  98. addEventListener(el, 'change', () => {
  99. const modelValue = (el as any)._modelValue
  100. const elementValue = getValue(el)
  101. const checked = el.checked
  102. const assign = el._assign
  103. if (isArray(modelValue)) {
  104. const index = looseIndexOf(modelValue, elementValue)
  105. const found = index !== -1
  106. if (checked && !found) {
  107. assign(modelValue.concat(elementValue))
  108. } else if (!checked && found) {
  109. const filtered = [...modelValue]
  110. filtered.splice(index, 1)
  111. assign(filtered)
  112. }
  113. } else {
  114. assign(getCheckboxValue(el, checked))
  115. }
  116. })
  117. },
  118. beforeUpdate(el, binding, vnode) {
  119. el._assign = getModelAssigner(vnode)
  120. setChecked(el, binding, vnode)
  121. }
  122. }
  123. function setChecked(
  124. el: HTMLInputElement,
  125. { value, oldValue }: DirectiveBinding,
  126. vnode: VNode
  127. ) {
  128. // store the v-model value on the element so it can be accessed by the
  129. // change listener.
  130. ;(el as any)._modelValue = value
  131. if (isArray(value)) {
  132. el.checked = looseIndexOf(value, vnode.props!.value) > -1
  133. } else if (value !== oldValue) {
  134. el.checked = looseEqual(value, getCheckboxValue(el, true))
  135. }
  136. }
  137. /**
  138. * @internal
  139. */
  140. export const vModelRadio: ModelDirective<HTMLInputElement> = {
  141. beforeMount(el, { value }, vnode) {
  142. el.checked = looseEqual(value, vnode.props!.value)
  143. el._assign = getModelAssigner(vnode)
  144. addEventListener(el, 'change', () => {
  145. el._assign(getValue(el))
  146. })
  147. },
  148. beforeUpdate(el, { value, oldValue }, vnode) {
  149. el._assign = getModelAssigner(vnode)
  150. if (value !== oldValue) {
  151. el.checked = looseEqual(value, vnode.props!.value)
  152. }
  153. }
  154. }
  155. /**
  156. * @internal
  157. */
  158. export const vModelSelect: ModelDirective<HTMLSelectElement> = {
  159. // use mounted & updated because <select> relies on its children <option>s.
  160. mounted(el, { value }, vnode) {
  161. setSelected(el, value)
  162. el._assign = getModelAssigner(vnode)
  163. addEventListener(el, 'change', () => {
  164. const selectedVal = Array.prototype.filter
  165. .call(el.options, (o: HTMLOptionElement) => o.selected)
  166. .map(getValue)
  167. el._assign(el.multiple ? selectedVal : selectedVal[0])
  168. })
  169. },
  170. beforeUpdate(el, _binding, vnode) {
  171. el._assign = getModelAssigner(vnode)
  172. },
  173. updated(el, { value }) {
  174. setSelected(el, value)
  175. }
  176. }
  177. function setSelected(el: HTMLSelectElement, value: any) {
  178. const isMultiple = el.multiple
  179. if (isMultiple && !isArray(value)) {
  180. __DEV__ &&
  181. warn(
  182. `<select multiple v-model> expects an Array value for its binding, ` +
  183. `but got ${Object.prototype.toString.call(value).slice(8, -1)}.`
  184. )
  185. return
  186. }
  187. for (let i = 0, l = el.options.length; i < l; i++) {
  188. const option = el.options[i]
  189. const optionValue = getValue(option)
  190. if (isMultiple) {
  191. option.selected = looseIndexOf(value, optionValue) > -1
  192. } else {
  193. if (looseEqual(getValue(option), value)) {
  194. el.selectedIndex = i
  195. return
  196. }
  197. }
  198. }
  199. if (!isMultiple) {
  200. el.selectedIndex = -1
  201. }
  202. }
  203. // retrieve raw value set via :value bindings
  204. function getValue(el: HTMLOptionElement | HTMLInputElement) {
  205. return '_value' in el ? (el as any)._value : el.value
  206. }
  207. // retrieve raw value for true-value and false-value set via :true-value or :false-value bindings
  208. function getCheckboxValue(
  209. el: HTMLInputElement & { _trueValue?: any; _falseValue?: any },
  210. checked: boolean
  211. ) {
  212. const key = checked ? '_trueValue' : '_falseValue'
  213. return key in el ? el[key] : checked
  214. }
  215. /**
  216. * @internal
  217. */
  218. export const vModelDynamic: ObjectDirective<
  219. HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
  220. > = {
  221. beforeMount(el, binding, vnode) {
  222. callModelHook(el, binding, vnode, null, 'beforeMount')
  223. },
  224. mounted(el, binding, vnode) {
  225. callModelHook(el, binding, vnode, null, 'mounted')
  226. },
  227. beforeUpdate(el, binding, vnode, prevVNode) {
  228. callModelHook(el, binding, vnode, prevVNode, 'beforeUpdate')
  229. },
  230. updated(el, binding, vnode, prevVNode) {
  231. callModelHook(el, binding, vnode, prevVNode, 'updated')
  232. }
  233. }
  234. function callModelHook(
  235. el: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement,
  236. binding: DirectiveBinding,
  237. vnode: VNode,
  238. prevVNode: VNode | null,
  239. hook: 'beforeMount' | 'mounted' | 'beforeUpdate' | 'updated'
  240. ) {
  241. let modelToUse: ObjectDirective
  242. switch (el.tagName) {
  243. case 'SELECT':
  244. modelToUse = vModelSelect
  245. break
  246. case 'TEXTAREA':
  247. modelToUse = vModelText
  248. break
  249. default:
  250. switch (el.type) {
  251. case 'checkbox':
  252. modelToUse = vModelCheckbox
  253. break
  254. case 'radio':
  255. modelToUse = vModelRadio
  256. break
  257. default:
  258. modelToUse = vModelText
  259. }
  260. }
  261. const fn = modelToUse[hook] as DirectiveHook
  262. fn && fn(el, binding, vnode, prevVNode)
  263. }
  264. // SSR vnode transforms
  265. if (__NODE_JS__) {
  266. vModelText.getSSRProps = ({ value }) => ({ value })
  267. vModelRadio.getSSRProps = ({ value }, vnode) => {
  268. if (vnode.props && looseEqual(vnode.props.value, value)) {
  269. return { checked: true }
  270. }
  271. }
  272. vModelCheckbox.getSSRProps = ({ value }, vnode) => {
  273. if (isArray(value)) {
  274. if (vnode.props && looseIndexOf(value, vnode.props.value) > -1) {
  275. return { checked: true }
  276. }
  277. } else if (value) {
  278. return { checked: true }
  279. }
  280. }
  281. }