vModel.spec.ts 37 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499
  1. import {
  2. type VNode,
  3. defineComponent,
  4. h,
  5. nextTick,
  6. ref,
  7. render,
  8. vModelCheckbox,
  9. vModelDynamic,
  10. withDirectives,
  11. } from '@vue/runtime-dom'
  12. const triggerEvent = (type: string, el: Element) => {
  13. const event = new Event(type)
  14. el.dispatchEvent(event)
  15. }
  16. const withVModel = (node: VNode, arg: any, mods?: any) =>
  17. withDirectives(node, [[vModelDynamic, arg, '', mods]])
  18. const setValue = function (this: any, value: any) {
  19. this.value = value
  20. }
  21. let root: any
  22. beforeEach(() => {
  23. root = document.createElement('div') as any
  24. })
  25. describe('vModel', () => {
  26. it('should work with text input', async () => {
  27. const manualListener = vi.fn()
  28. const component = defineComponent({
  29. data() {
  30. return { value: null }
  31. },
  32. render() {
  33. return [
  34. withVModel(
  35. h('input', {
  36. 'onUpdate:modelValue': setValue.bind(this),
  37. onInput: () => {
  38. manualListener(data.value)
  39. },
  40. }),
  41. this.value,
  42. ),
  43. ]
  44. },
  45. })
  46. render(h(component), root)
  47. const input = root.querySelector('input')!
  48. const data = root._vnode.component.data
  49. expect(input.value).toEqual('')
  50. input.value = 'foo'
  51. triggerEvent('input', input)
  52. await nextTick()
  53. expect(data.value).toEqual('foo')
  54. // #1931
  55. expect(manualListener).toHaveBeenCalledWith('foo')
  56. data.value = 'bar'
  57. await nextTick()
  58. expect(input.value).toEqual('bar')
  59. data.value = undefined
  60. await nextTick()
  61. expect(input.value).toEqual('')
  62. })
  63. it('should work with number input', async () => {
  64. const component = defineComponent({
  65. data() {
  66. return { value: null }
  67. },
  68. render() {
  69. return [
  70. withVModel(
  71. h('input', {
  72. type: 'number',
  73. 'onUpdate:modelValue': setValue.bind(this),
  74. }),
  75. this.value,
  76. ),
  77. ]
  78. },
  79. })
  80. render(h(component), root)
  81. const input = root.querySelector('input')!
  82. const data = root._vnode.component.data
  83. expect(input.value).toEqual('')
  84. expect(input.type).toEqual('number')
  85. input.value = 1
  86. triggerEvent('input', input)
  87. await nextTick()
  88. expect(typeof data.value).toEqual('number')
  89. expect(data.value).toEqual(1)
  90. })
  91. // #7003
  92. it('should work with number input and be able to update rendering correctly', async () => {
  93. const setValue1 = function (this: any, value: any) {
  94. this.value1 = value
  95. }
  96. const setValue2 = function (this: any, value: any) {
  97. this.value2 = value
  98. }
  99. const component = defineComponent({
  100. data() {
  101. return { value1: 1.002, value2: 1.002 }
  102. },
  103. render() {
  104. return [
  105. withVModel(
  106. h('input', {
  107. id: 'input_num1',
  108. type: 'number',
  109. 'onUpdate:modelValue': setValue1.bind(this),
  110. }),
  111. this.value1,
  112. ),
  113. withVModel(
  114. h('input', {
  115. id: 'input_num2',
  116. type: 'number',
  117. 'onUpdate:modelValue': setValue2.bind(this),
  118. }),
  119. this.value2,
  120. ),
  121. ]
  122. },
  123. })
  124. render(h(component), root)
  125. const data = root._vnode.component.data
  126. const inputNum1 = root.querySelector('#input_num1')!
  127. expect(inputNum1.value).toBe('1.002')
  128. const inputNum2 = root.querySelector('#input_num2')!
  129. expect(inputNum2.value).toBe('1.002')
  130. inputNum1.value = '1.00'
  131. triggerEvent('input', inputNum1)
  132. await nextTick()
  133. expect(data.value1).toBe(1)
  134. inputNum2.value = '1.00'
  135. triggerEvent('input', inputNum2)
  136. await nextTick()
  137. expect(data.value2).toBe(1)
  138. expect(inputNum1.value).toBe('1.00')
  139. })
  140. it('should work with multiple listeners', async () => {
  141. const spy = vi.fn()
  142. const component = defineComponent({
  143. data() {
  144. return { value: null }
  145. },
  146. render() {
  147. return [
  148. withVModel(
  149. h('input', {
  150. 'onUpdate:modelValue': [setValue.bind(this), spy],
  151. }),
  152. this.value,
  153. ),
  154. ]
  155. },
  156. })
  157. render(h(component), root)
  158. const input = root.querySelector('input')!
  159. const data = root._vnode.component.data
  160. input.value = 'foo'
  161. triggerEvent('input', input)
  162. await nextTick()
  163. expect(data.value).toEqual('foo')
  164. expect(spy).toHaveBeenCalledWith('foo')
  165. })
  166. it('should work with updated listeners', async () => {
  167. const spy1 = vi.fn()
  168. const spy2 = vi.fn()
  169. const toggle = ref(true)
  170. const component = defineComponent({
  171. render() {
  172. return [
  173. withVModel(
  174. h('input', {
  175. 'onUpdate:modelValue': toggle.value ? spy1 : spy2,
  176. }),
  177. 'foo',
  178. ),
  179. ]
  180. },
  181. })
  182. render(h(component), root)
  183. const input = root.querySelector('input')!
  184. input.value = 'foo'
  185. triggerEvent('input', input)
  186. await nextTick()
  187. expect(spy1).toHaveBeenCalledWith('foo')
  188. // update listener
  189. toggle.value = false
  190. await nextTick()
  191. input.value = 'bar'
  192. triggerEvent('input', input)
  193. await nextTick()
  194. expect(spy1).not.toHaveBeenCalledWith('bar')
  195. expect(spy2).toHaveBeenCalledWith('bar')
  196. })
  197. it('should work with textarea', async () => {
  198. const component = defineComponent({
  199. data() {
  200. return { value: null }
  201. },
  202. render() {
  203. return [
  204. withVModel(
  205. h('textarea', {
  206. 'onUpdate:modelValue': setValue.bind(this),
  207. }),
  208. this.value,
  209. ),
  210. ]
  211. },
  212. })
  213. render(h(component), root)
  214. const input = root.querySelector('textarea')
  215. const data = root._vnode.component.data
  216. input.value = 'foo'
  217. triggerEvent('input', input)
  218. await nextTick()
  219. expect(data.value).toEqual('foo')
  220. data.value = 'bar'
  221. await nextTick()
  222. expect(input.value).toEqual('bar')
  223. })
  224. it('should support modifiers', async () => {
  225. const component = defineComponent({
  226. data() {
  227. return {
  228. number: null,
  229. trim: null,
  230. lazy: null,
  231. trimNumber: null,
  232. trimLazy: null,
  233. }
  234. },
  235. render() {
  236. return [
  237. withVModel(
  238. h('input', {
  239. class: 'number',
  240. 'onUpdate:modelValue': (val: any) => {
  241. this.number = val
  242. },
  243. }),
  244. this.number,
  245. {
  246. number: true,
  247. },
  248. ),
  249. withVModel(
  250. h('input', {
  251. class: 'trim',
  252. 'onUpdate:modelValue': (val: any) => {
  253. this.trim = val
  254. },
  255. }),
  256. this.trim,
  257. {
  258. trim: true,
  259. },
  260. ),
  261. withVModel(
  262. h('input', {
  263. class: 'trim-lazy',
  264. 'onUpdate:modelValue': (val: any) => {
  265. this.trimLazy = val
  266. },
  267. }),
  268. this.trim,
  269. {
  270. trim: true,
  271. lazy: true,
  272. },
  273. ),
  274. withVModel(
  275. h('input', {
  276. class: 'trim-number',
  277. 'onUpdate:modelValue': (val: any) => {
  278. this.trimNumber = val
  279. },
  280. }),
  281. this.trimNumber,
  282. {
  283. trim: true,
  284. number: true,
  285. },
  286. ),
  287. withVModel(
  288. h('input', {
  289. class: 'lazy',
  290. 'onUpdate:modelValue': (val: any) => {
  291. this.lazy = val
  292. },
  293. }),
  294. this.lazy,
  295. {
  296. lazy: true,
  297. },
  298. ),
  299. ]
  300. },
  301. })
  302. render(h(component), root)
  303. const number = root.querySelector('.number')
  304. const trim = root.querySelector('.trim')
  305. const trimNumber = root.querySelector('.trim-number')
  306. const trimLazy = root.querySelector('.trim-lazy')
  307. const lazy = root.querySelector('.lazy')
  308. const data = root._vnode.component.data
  309. number.value = '+01.2'
  310. triggerEvent('input', number)
  311. await nextTick()
  312. expect(data.number).toEqual(1.2)
  313. triggerEvent('change', number)
  314. await nextTick()
  315. expect(number.value).toEqual('1.2')
  316. trim.value = ' hello, world '
  317. triggerEvent('input', trim)
  318. await nextTick()
  319. expect(data.trim).toEqual('hello, world')
  320. trimNumber.value = ' 1 '
  321. triggerEvent('input', trimNumber)
  322. await nextTick()
  323. expect(data.trimNumber).toEqual(1)
  324. trimNumber.value = ' +01.2 '
  325. triggerEvent('input', trimNumber)
  326. await nextTick()
  327. expect(data.trimNumber).toEqual(1.2)
  328. trimLazy.value = ' ddd '
  329. triggerEvent('change', trimLazy)
  330. await nextTick()
  331. expect(data.trimLazy).toEqual('ddd')
  332. lazy.value = 'foo'
  333. triggerEvent('change', lazy)
  334. await nextTick()
  335. expect(data.lazy).toEqual('foo')
  336. })
  337. it('should work with range', async () => {
  338. const component = defineComponent({
  339. data() {
  340. return { value: 25 }
  341. },
  342. render() {
  343. return [
  344. withVModel(
  345. h('input', {
  346. type: 'range',
  347. min: 1,
  348. max: 100,
  349. class: 'foo',
  350. 'onUpdate:modelValue': setValue.bind(this),
  351. }),
  352. this.value,
  353. {
  354. number: true,
  355. },
  356. ),
  357. withVModel(
  358. h('input', {
  359. type: 'range',
  360. min: 1,
  361. max: 100,
  362. class: 'bar',
  363. 'onUpdate:modelValue': setValue.bind(this),
  364. }),
  365. this.value,
  366. {
  367. lazy: true,
  368. },
  369. ),
  370. ]
  371. },
  372. })
  373. render(h(component), root)
  374. const foo = root.querySelector('.foo')
  375. const bar = root.querySelector('.bar')
  376. const data = root._vnode.component.data
  377. foo.value = 20
  378. triggerEvent('input', foo)
  379. await nextTick()
  380. expect(data.value).toEqual(20)
  381. foo.value = 200
  382. triggerEvent('input', foo)
  383. await nextTick()
  384. expect(data.value).toEqual(100)
  385. foo.value = -1
  386. triggerEvent('input', foo)
  387. await nextTick()
  388. expect(data.value).toEqual(1)
  389. bar.value = 30
  390. triggerEvent('change', bar)
  391. await nextTick()
  392. expect(data.value).toEqual('30')
  393. bar.value = 200
  394. triggerEvent('change', bar)
  395. await nextTick()
  396. expect(data.value).toEqual('100')
  397. bar.value = -1
  398. triggerEvent('change', bar)
  399. await nextTick()
  400. expect(data.value).toEqual('1')
  401. data.value = 60
  402. await nextTick()
  403. expect(foo.value).toEqual('60')
  404. expect(bar.value).toEqual('60')
  405. data.value = -1
  406. await nextTick()
  407. expect(foo.value).toEqual('1')
  408. expect(bar.value).toEqual('1')
  409. data.value = 200
  410. await nextTick()
  411. expect(foo.value).toEqual('100')
  412. expect(bar.value).toEqual('100')
  413. })
  414. it('should work with checkbox', async () => {
  415. const component = defineComponent({
  416. data() {
  417. return { value: null }
  418. },
  419. render() {
  420. return [
  421. withVModel(
  422. h('input', {
  423. type: 'checkbox',
  424. 'onUpdate:modelValue': setValue.bind(this),
  425. }),
  426. this.value,
  427. ),
  428. ]
  429. },
  430. })
  431. render(h(component), root)
  432. const input = root.querySelector('input')
  433. const data = root._vnode.component.data
  434. input.checked = true
  435. triggerEvent('change', input)
  436. await nextTick()
  437. expect(data.value).toEqual(true)
  438. data.value = false
  439. await nextTick()
  440. expect(input.checked).toEqual(false)
  441. data.value = true
  442. await nextTick()
  443. expect(input.checked).toEqual(true)
  444. input.checked = false
  445. triggerEvent('change', input)
  446. await nextTick()
  447. expect(data.value).toEqual(false)
  448. })
  449. it('should work with checkbox and true-value/false-value', async () => {
  450. const component = defineComponent({
  451. data() {
  452. return { value: 'yes' }
  453. },
  454. render() {
  455. return [
  456. withVModel(
  457. h('input', {
  458. type: 'checkbox',
  459. 'true-value': 'yes',
  460. 'false-value': 'no',
  461. 'onUpdate:modelValue': setValue.bind(this),
  462. }),
  463. this.value,
  464. ),
  465. ]
  466. },
  467. })
  468. render(h(component), root)
  469. const input = root.querySelector('input')
  470. const data = root._vnode.component.data
  471. // DOM checked state should respect initial true-value/false-value
  472. expect(input.checked).toEqual(true)
  473. input.checked = false
  474. triggerEvent('change', input)
  475. await nextTick()
  476. expect(data.value).toEqual('no')
  477. data.value = 'yes'
  478. await nextTick()
  479. expect(input.checked).toEqual(true)
  480. data.value = 'no'
  481. await nextTick()
  482. expect(input.checked).toEqual(false)
  483. input.checked = true
  484. triggerEvent('change', input)
  485. await nextTick()
  486. expect(data.value).toEqual('yes')
  487. })
  488. it('should work with checkbox and true-value/false-value with object values', async () => {
  489. const component = defineComponent({
  490. data() {
  491. return { value: null }
  492. },
  493. render() {
  494. return [
  495. withVModel(
  496. h('input', {
  497. type: 'checkbox',
  498. 'true-value': { yes: 'yes' },
  499. 'false-value': { no: 'no' },
  500. 'onUpdate:modelValue': setValue.bind(this),
  501. }),
  502. this.value,
  503. ),
  504. ]
  505. },
  506. })
  507. render(h(component), root)
  508. const input = root.querySelector('input')
  509. const data = root._vnode.component.data
  510. input.checked = true
  511. triggerEvent('change', input)
  512. await nextTick()
  513. expect(data.value).toEqual({ yes: 'yes' })
  514. data.value = { no: 'no' }
  515. await nextTick()
  516. expect(input.checked).toEqual(false)
  517. data.value = { yes: 'yes' }
  518. await nextTick()
  519. expect(input.checked).toEqual(true)
  520. input.checked = false
  521. triggerEvent('change', input)
  522. await nextTick()
  523. expect(data.value).toEqual({ no: 'no' })
  524. })
  525. it(`should support array as a checkbox model`, async () => {
  526. const component = defineComponent({
  527. data() {
  528. return { value: [] }
  529. },
  530. render() {
  531. return [
  532. withVModel(
  533. h('input', {
  534. type: 'checkbox',
  535. class: 'foo',
  536. value: 'foo',
  537. 'onUpdate:modelValue': setValue.bind(this),
  538. }),
  539. this.value,
  540. ),
  541. withVModel(
  542. h('input', {
  543. type: 'checkbox',
  544. class: 'bar',
  545. value: 'bar',
  546. 'onUpdate:modelValue': setValue.bind(this),
  547. }),
  548. this.value,
  549. ),
  550. ]
  551. },
  552. })
  553. render(h(component), root)
  554. const foo = root.querySelector('.foo')
  555. const bar = root.querySelector('.bar')
  556. const data = root._vnode.component.data
  557. foo.checked = true
  558. triggerEvent('change', foo)
  559. await nextTick()
  560. expect(data.value).toMatchObject(['foo'])
  561. bar.checked = true
  562. triggerEvent('change', bar)
  563. await nextTick()
  564. expect(data.value).toMatchObject(['foo', 'bar'])
  565. bar.checked = false
  566. triggerEvent('change', bar)
  567. await nextTick()
  568. expect(data.value).toMatchObject(['foo'])
  569. foo.checked = false
  570. triggerEvent('change', foo)
  571. await nextTick()
  572. expect(data.value).toMatchObject([])
  573. data.value = ['foo']
  574. await nextTick()
  575. expect(bar.checked).toEqual(false)
  576. expect(foo.checked).toEqual(true)
  577. data.value = ['bar']
  578. await nextTick()
  579. expect(foo.checked).toEqual(false)
  580. expect(bar.checked).toEqual(true)
  581. data.value = []
  582. await nextTick()
  583. expect(foo.checked).toEqual(false)
  584. expect(bar.checked).toEqual(false)
  585. })
  586. it(`should support Set as a checkbox model`, async () => {
  587. const component = defineComponent({
  588. data() {
  589. return { value: new Set() }
  590. },
  591. render() {
  592. return [
  593. withVModel(
  594. h('input', {
  595. type: 'checkbox',
  596. class: 'foo',
  597. value: 'foo',
  598. 'onUpdate:modelValue': setValue.bind(this),
  599. }),
  600. this.value,
  601. ),
  602. withVModel(
  603. h('input', {
  604. type: 'checkbox',
  605. class: 'bar',
  606. value: 'bar',
  607. 'onUpdate:modelValue': setValue.bind(this),
  608. }),
  609. this.value,
  610. ),
  611. ]
  612. },
  613. })
  614. render(h(component), root)
  615. const foo = root.querySelector('.foo')
  616. const bar = root.querySelector('.bar')
  617. const data = root._vnode.component.data
  618. foo.checked = true
  619. triggerEvent('change', foo)
  620. await nextTick()
  621. expect(data.value).toMatchObject(new Set(['foo']))
  622. bar.checked = true
  623. triggerEvent('change', bar)
  624. await nextTick()
  625. expect(data.value).toMatchObject(new Set(['foo', 'bar']))
  626. bar.checked = false
  627. triggerEvent('change', bar)
  628. await nextTick()
  629. expect(data.value).toMatchObject(new Set(['foo']))
  630. foo.checked = false
  631. triggerEvent('change', foo)
  632. await nextTick()
  633. expect(data.value).toMatchObject(new Set())
  634. data.value = new Set(['foo'])
  635. await nextTick()
  636. expect(bar.checked).toEqual(false)
  637. expect(foo.checked).toEqual(true)
  638. data.value = new Set(['bar'])
  639. await nextTick()
  640. expect(foo.checked).toEqual(false)
  641. expect(bar.checked).toEqual(true)
  642. data.value = new Set()
  643. await nextTick()
  644. expect(foo.checked).toEqual(false)
  645. expect(bar.checked).toEqual(false)
  646. })
  647. it('should not update DOM unnecessarily', async () => {
  648. const component = defineComponent({
  649. data() {
  650. return { value: true }
  651. },
  652. render() {
  653. return [
  654. withVModel(
  655. h('input', {
  656. type: 'checkbox',
  657. 'onUpdate:modelValue': setValue.bind(this),
  658. }),
  659. this.value,
  660. ),
  661. ]
  662. },
  663. })
  664. render(h(component), root)
  665. const input = root.querySelector('input')
  666. const data = root._vnode.component.data
  667. const setCheckedSpy = vi.spyOn(input, 'checked', 'set')
  668. // Trigger a change event without actually changing the value
  669. triggerEvent('change', input)
  670. await nextTick()
  671. expect(data.value).toEqual(true)
  672. expect(setCheckedSpy).not.toHaveBeenCalled()
  673. // Change the value and trigger a change event
  674. input.checked = false
  675. triggerEvent('change', input)
  676. await nextTick()
  677. expect(data.value).toEqual(false)
  678. expect(setCheckedSpy).toHaveBeenCalledTimes(1)
  679. setCheckedSpy.mockClear()
  680. data.value = false
  681. await nextTick()
  682. expect(input.checked).toEqual(false)
  683. expect(setCheckedSpy).not.toHaveBeenCalled()
  684. data.value = true
  685. await nextTick()
  686. expect(input.checked).toEqual(true)
  687. expect(setCheckedSpy).toHaveBeenCalledTimes(1)
  688. })
  689. it('should handle array values correctly without unnecessary updates', async () => {
  690. const component = defineComponent({
  691. data() {
  692. return { value: ['foo'] }
  693. },
  694. render() {
  695. return [
  696. withVModel(
  697. h('input', {
  698. type: 'checkbox',
  699. value: 'foo',
  700. 'onUpdate:modelValue': setValue.bind(this),
  701. }),
  702. this.value,
  703. ),
  704. withVModel(
  705. h('input', {
  706. type: 'checkbox',
  707. value: 'bar',
  708. 'onUpdate:modelValue': setValue.bind(this),
  709. }),
  710. this.value,
  711. ),
  712. ]
  713. },
  714. })
  715. render(h(component), root)
  716. const [foo, bar] = root.querySelectorAll('input')
  717. const data = root._vnode.component.data
  718. const setCheckedSpyFoo = vi.spyOn(foo, 'checked', 'set')
  719. const setCheckedSpyBar = vi.spyOn(bar, 'checked', 'set')
  720. expect(foo.checked).toEqual(true)
  721. expect(bar.checked).toEqual(false)
  722. triggerEvent('change', foo)
  723. await nextTick()
  724. expect(data.value).toEqual(['foo'])
  725. expect(setCheckedSpyFoo).not.toHaveBeenCalled()
  726. bar.checked = true
  727. triggerEvent('change', bar)
  728. await nextTick()
  729. expect(data.value).toEqual(['foo', 'bar'])
  730. expect(setCheckedSpyBar).toHaveBeenCalledTimes(1)
  731. setCheckedSpyFoo.mockClear()
  732. setCheckedSpyBar.mockClear()
  733. data.value = ['foo', 'bar']
  734. await nextTick()
  735. expect(setCheckedSpyFoo).not.toHaveBeenCalled()
  736. expect(setCheckedSpyBar).not.toHaveBeenCalled()
  737. data.value = ['bar']
  738. await nextTick()
  739. expect(setCheckedSpyFoo).toHaveBeenCalledTimes(1)
  740. expect(setCheckedSpyBar).not.toHaveBeenCalled()
  741. expect(foo.checked).toEqual(false)
  742. expect(bar.checked).toEqual(true)
  743. })
  744. it('should work with radio', async () => {
  745. const component = defineComponent({
  746. data() {
  747. return { value: null }
  748. },
  749. render() {
  750. return [
  751. withVModel(
  752. h('input', {
  753. type: 'radio',
  754. class: 'foo',
  755. value: 'foo',
  756. 'onUpdate:modelValue': setValue.bind(this),
  757. }),
  758. this.value,
  759. ),
  760. withVModel(
  761. h('input', {
  762. type: 'radio',
  763. class: 'bar',
  764. value: 'bar',
  765. 'onUpdate:modelValue': setValue.bind(this),
  766. }),
  767. this.value,
  768. ),
  769. ]
  770. },
  771. })
  772. render(h(component), root)
  773. const foo = root.querySelector('.foo')
  774. const bar = root.querySelector('.bar')
  775. const data = root._vnode.component.data
  776. foo.checked = true
  777. triggerEvent('change', foo)
  778. await nextTick()
  779. expect(data.value).toEqual('foo')
  780. bar.checked = true
  781. triggerEvent('change', bar)
  782. await nextTick()
  783. expect(data.value).toEqual('bar')
  784. data.value = null
  785. await nextTick()
  786. expect(foo.checked).toEqual(false)
  787. expect(bar.checked).toEqual(false)
  788. data.value = 'foo'
  789. await nextTick()
  790. expect(foo.checked).toEqual(true)
  791. expect(bar.checked).toEqual(false)
  792. data.value = 'bar'
  793. await nextTick()
  794. expect(foo.checked).toEqual(false)
  795. expect(bar.checked).toEqual(true)
  796. })
  797. it('should work with single select', async () => {
  798. const component = defineComponent({
  799. data() {
  800. return { value: null }
  801. },
  802. render() {
  803. return [
  804. withVModel(
  805. h(
  806. 'select',
  807. {
  808. value: null,
  809. 'onUpdate:modelValue': setValue.bind(this),
  810. },
  811. [h('option', { value: 'foo' }), h('option', { value: 'bar' })],
  812. ),
  813. this.value,
  814. ),
  815. ]
  816. },
  817. })
  818. render(h(component), root)
  819. const input = root.querySelector('select')
  820. const foo = root.querySelector('option[value=foo]')
  821. const bar = root.querySelector('option[value=bar]')
  822. const data = root._vnode.component.data
  823. foo.selected = true
  824. triggerEvent('change', input)
  825. await nextTick()
  826. expect(data.value).toEqual('foo')
  827. foo.selected = false
  828. bar.selected = true
  829. triggerEvent('change', input)
  830. await nextTick()
  831. expect(data.value).toEqual('bar')
  832. foo.selected = false
  833. bar.selected = false
  834. data.value = 'foo'
  835. await nextTick()
  836. expect(input.value).toEqual('foo')
  837. expect(foo.selected).toEqual(true)
  838. expect(bar.selected).toEqual(false)
  839. foo.selected = true
  840. bar.selected = false
  841. data.value = 'bar'
  842. await nextTick()
  843. expect(input.value).toEqual('bar')
  844. expect(foo.selected).toEqual(false)
  845. expect(bar.selected).toEqual(true)
  846. })
  847. it('multiple select (model is Array)', async () => {
  848. const component = defineComponent({
  849. data() {
  850. return { value: [] }
  851. },
  852. render() {
  853. return [
  854. withVModel(
  855. h(
  856. 'select',
  857. {
  858. value: null,
  859. multiple: true,
  860. 'onUpdate:modelValue': setValue.bind(this),
  861. },
  862. [h('option', { value: 'foo' }), h('option', { value: 'bar' })],
  863. ),
  864. this.value,
  865. ),
  866. ]
  867. },
  868. })
  869. render(h(component), root)
  870. const input = root.querySelector('select')
  871. const foo = root.querySelector('option[value=foo]')
  872. const bar = root.querySelector('option[value=bar]')
  873. const data = root._vnode.component.data
  874. foo.selected = true
  875. triggerEvent('change', input)
  876. await nextTick()
  877. expect(data.value).toMatchObject(['foo'])
  878. foo.selected = false
  879. bar.selected = true
  880. triggerEvent('change', input)
  881. await nextTick()
  882. expect(data.value).toMatchObject(['bar'])
  883. foo.selected = true
  884. bar.selected = true
  885. triggerEvent('change', input)
  886. await nextTick()
  887. expect(data.value).toMatchObject(['foo', 'bar'])
  888. foo.selected = false
  889. bar.selected = false
  890. data.value = ['foo']
  891. await nextTick()
  892. expect(input.value).toEqual('foo')
  893. expect(foo.selected).toEqual(true)
  894. expect(bar.selected).toEqual(false)
  895. foo.selected = false
  896. bar.selected = false
  897. data.value = ['foo', 'bar']
  898. await nextTick()
  899. expect(foo.selected).toEqual(true)
  900. expect(bar.selected).toEqual(true)
  901. })
  902. it('v-model.number should work with select tag', async () => {
  903. const component = defineComponent({
  904. data() {
  905. return { value: null }
  906. },
  907. render() {
  908. return [
  909. withVModel(
  910. h(
  911. 'select',
  912. {
  913. value: null,
  914. 'onUpdate:modelValue': setValue.bind(this),
  915. },
  916. [h('option', { value: '1' }), h('option', { value: '2' })],
  917. ),
  918. this.value,
  919. {
  920. number: true,
  921. },
  922. ),
  923. ]
  924. },
  925. })
  926. render(h(component), root)
  927. const input = root.querySelector('select')
  928. const one = root.querySelector('option[value="1"]')
  929. const data = root._vnode.component.data
  930. one.selected = true
  931. triggerEvent('change', input)
  932. await nextTick()
  933. expect(typeof data.value).toEqual('number')
  934. expect(data.value).toEqual(1)
  935. })
  936. it('v-model.number should work with multiple select', async () => {
  937. const component = defineComponent({
  938. data() {
  939. return { value: [] }
  940. },
  941. render() {
  942. return [
  943. withVModel(
  944. h(
  945. 'select',
  946. {
  947. value: null,
  948. multiple: true,
  949. 'onUpdate:modelValue': setValue.bind(this),
  950. },
  951. [h('option', { value: '1' }), h('option', { value: '2' })],
  952. ),
  953. this.value,
  954. {
  955. number: true,
  956. },
  957. ),
  958. ]
  959. },
  960. })
  961. render(h(component), root)
  962. const input = root.querySelector('select')
  963. const one = root.querySelector('option[value="1"]')
  964. const two = root.querySelector('option[value="2"]')
  965. const data = root._vnode.component.data
  966. one.selected = true
  967. two.selected = false
  968. triggerEvent('change', input)
  969. await nextTick()
  970. expect(data.value).toMatchObject([1])
  971. one.selected = false
  972. two.selected = true
  973. triggerEvent('change', input)
  974. await nextTick()
  975. expect(data.value).toMatchObject([2])
  976. one.selected = true
  977. two.selected = true
  978. triggerEvent('change', input)
  979. await nextTick()
  980. expect(data.value).toMatchObject([1, 2])
  981. one.selected = false
  982. two.selected = false
  983. data.value = [1]
  984. await nextTick()
  985. expect(one.selected).toEqual(true)
  986. expect(two.selected).toEqual(false)
  987. one.selected = false
  988. two.selected = false
  989. data.value = [1, 2]
  990. await nextTick()
  991. expect(one.selected).toEqual(true)
  992. expect(two.selected).toEqual(true)
  993. })
  994. it('multiple select (model is Array, option value is object)', async () => {
  995. const fooValue = { foo: 1 }
  996. const barValue = { bar: 1 }
  997. const component = defineComponent({
  998. data() {
  999. return { value: [] }
  1000. },
  1001. render() {
  1002. return [
  1003. withVModel(
  1004. h(
  1005. 'select',
  1006. {
  1007. value: null,
  1008. multiple: true,
  1009. 'onUpdate:modelValue': setValue.bind(this),
  1010. },
  1011. [
  1012. h('option', { value: fooValue }),
  1013. h('option', { value: barValue }),
  1014. ],
  1015. ),
  1016. this.value,
  1017. ),
  1018. ]
  1019. },
  1020. })
  1021. render(h(component), root)
  1022. await nextTick()
  1023. const input = root.querySelector('select')
  1024. const [foo, bar] = root.querySelectorAll('option')
  1025. const data = root._vnode.component.data
  1026. foo.selected = true
  1027. triggerEvent('change', input)
  1028. await nextTick()
  1029. expect(data.value).toMatchObject([fooValue])
  1030. foo.selected = false
  1031. bar.selected = true
  1032. triggerEvent('change', input)
  1033. await nextTick()
  1034. expect(data.value).toMatchObject([barValue])
  1035. foo.selected = true
  1036. bar.selected = true
  1037. triggerEvent('change', input)
  1038. await nextTick()
  1039. expect(data.value).toMatchObject([fooValue, barValue])
  1040. // reset
  1041. foo.selected = false
  1042. bar.selected = false
  1043. triggerEvent('change', input)
  1044. await nextTick()
  1045. expect(data.value).toMatchObject([])
  1046. data.value = [fooValue, barValue]
  1047. await nextTick()
  1048. expect(foo.selected).toEqual(true)
  1049. expect(bar.selected).toEqual(true)
  1050. // reset
  1051. foo.selected = false
  1052. bar.selected = false
  1053. triggerEvent('change', input)
  1054. await nextTick()
  1055. expect(data.value).toMatchObject([])
  1056. data.value = [{ foo: 1 }, { bar: 1 }]
  1057. await nextTick()
  1058. // looseEqual
  1059. expect(foo.selected).toEqual(true)
  1060. expect(bar.selected).toEqual(true)
  1061. })
  1062. it('multiple select (model is Set)', async () => {
  1063. const component = defineComponent({
  1064. data() {
  1065. return { value: new Set() }
  1066. },
  1067. render() {
  1068. return [
  1069. withVModel(
  1070. h(
  1071. 'select',
  1072. {
  1073. value: null,
  1074. multiple: true,
  1075. 'onUpdate:modelValue': setValue.bind(this),
  1076. },
  1077. [h('option', { value: 'foo' }), h('option', { value: 'bar' })],
  1078. ),
  1079. this.value,
  1080. ),
  1081. ]
  1082. },
  1083. })
  1084. render(h(component), root)
  1085. const input = root.querySelector('select')
  1086. const foo = root.querySelector('option[value=foo]')
  1087. const bar = root.querySelector('option[value=bar]')
  1088. const data = root._vnode.component.data
  1089. foo.selected = true
  1090. triggerEvent('change', input)
  1091. await nextTick()
  1092. expect(data.value).toBeInstanceOf(Set)
  1093. expect(data.value).toMatchObject(new Set(['foo']))
  1094. foo.selected = false
  1095. bar.selected = true
  1096. triggerEvent('change', input)
  1097. await nextTick()
  1098. expect(data.value).toBeInstanceOf(Set)
  1099. expect(data.value).toMatchObject(new Set(['bar']))
  1100. foo.selected = true
  1101. bar.selected = true
  1102. triggerEvent('change', input)
  1103. await nextTick()
  1104. expect(data.value).toBeInstanceOf(Set)
  1105. expect(data.value).toMatchObject(new Set(['foo', 'bar']))
  1106. foo.selected = false
  1107. bar.selected = false
  1108. data.value = new Set(['foo'])
  1109. await nextTick()
  1110. expect(input.value).toEqual('foo')
  1111. expect(foo.selected).toEqual(true)
  1112. expect(bar.selected).toEqual(false)
  1113. foo.selected = false
  1114. bar.selected = false
  1115. data.value = new Set(['foo', 'bar'])
  1116. await nextTick()
  1117. expect(foo.selected).toEqual(true)
  1118. expect(bar.selected).toEqual(true)
  1119. })
  1120. it('multiple select (model is Set, option value is object)', async () => {
  1121. const fooValue = { foo: 1 }
  1122. const barValue = { bar: 1 }
  1123. const component = defineComponent({
  1124. data() {
  1125. return { value: new Set() }
  1126. },
  1127. render() {
  1128. return [
  1129. withVModel(
  1130. h(
  1131. 'select',
  1132. {
  1133. value: null,
  1134. multiple: true,
  1135. 'onUpdate:modelValue': setValue.bind(this),
  1136. },
  1137. [
  1138. h('option', { value: fooValue }),
  1139. h('option', { value: barValue }),
  1140. ],
  1141. ),
  1142. this.value,
  1143. ),
  1144. ]
  1145. },
  1146. })
  1147. render(h(component), root)
  1148. await nextTick()
  1149. const input = root.querySelector('select')
  1150. const [foo, bar] = root.querySelectorAll('option')
  1151. const data = root._vnode.component.data
  1152. foo.selected = true
  1153. triggerEvent('change', input)
  1154. await nextTick()
  1155. expect(data.value).toMatchObject(new Set([fooValue]))
  1156. foo.selected = false
  1157. bar.selected = true
  1158. triggerEvent('change', input)
  1159. await nextTick()
  1160. expect(data.value).toMatchObject(new Set([barValue]))
  1161. foo.selected = true
  1162. bar.selected = true
  1163. triggerEvent('change', input)
  1164. await nextTick()
  1165. expect(data.value).toMatchObject(new Set([fooValue, barValue]))
  1166. foo.selected = false
  1167. bar.selected = false
  1168. data.value = new Set([fooValue, barValue])
  1169. await nextTick()
  1170. expect(foo.selected).toEqual(true)
  1171. expect(bar.selected).toEqual(true)
  1172. foo.selected = false
  1173. bar.selected = false
  1174. data.value = new Set([{ foo: 1 }, { bar: 1 }])
  1175. await nextTick()
  1176. // without looseEqual, here is different from Array
  1177. expect(foo.selected).toEqual(false)
  1178. expect(bar.selected).toEqual(false)
  1179. })
  1180. it('should work with composition session', async () => {
  1181. const component = defineComponent({
  1182. data() {
  1183. return { value: '' }
  1184. },
  1185. render() {
  1186. return [
  1187. withVModel(
  1188. h('input', {
  1189. 'onUpdate:modelValue': setValue.bind(this),
  1190. }),
  1191. this.value,
  1192. ),
  1193. ]
  1194. },
  1195. })
  1196. render(h(component), root)
  1197. const input = root.querySelector('input')!
  1198. const data = root._vnode.component.data
  1199. expect(input.value).toEqual('')
  1200. //developer.mozilla.org/en-US/docs/Web/API/Element/compositionstart_event
  1201. //compositionstart event could be fired after a user starts entering a Chinese character using a Pinyin IME
  1202. input.value = '使用拼音'
  1203. triggerEvent('compositionstart', input)
  1204. await nextTick()
  1205. expect(data.value).toEqual('')
  1206. // input event has no effect during composition session
  1207. input.value = '使用拼音输入'
  1208. triggerEvent('input', input)
  1209. await nextTick()
  1210. expect(data.value).toEqual('')
  1211. // After compositionend event being fired, an input event will be automatically trigger
  1212. triggerEvent('compositionend', input)
  1213. await nextTick()
  1214. expect(data.value).toEqual('使用拼音输入')
  1215. })
  1216. it('multiple select (model is number, option value is string)', async () => {
  1217. const component = defineComponent({
  1218. data() {
  1219. return {
  1220. value: [1, 2],
  1221. }
  1222. },
  1223. render() {
  1224. return [
  1225. withVModel(
  1226. h(
  1227. 'select',
  1228. {
  1229. multiple: true,
  1230. 'onUpdate:modelValue': setValue.bind(this),
  1231. },
  1232. [h('option', { value: '1' }), h('option', { value: '2' })],
  1233. ),
  1234. this.value,
  1235. ),
  1236. ]
  1237. },
  1238. })
  1239. render(h(component), root)
  1240. await nextTick()
  1241. const [foo, bar] = root.querySelectorAll('option')
  1242. expect(foo.selected).toEqual(true)
  1243. expect(bar.selected).toEqual(true)
  1244. })
  1245. // #10503
  1246. test('equal value with a leading 0 should trigger update.', async () => {
  1247. const setNum = function (this: any, value: any) {
  1248. this.num = value
  1249. }
  1250. const component = defineComponent({
  1251. data() {
  1252. return { num: 0 }
  1253. },
  1254. render() {
  1255. return [
  1256. withVModel(
  1257. h('input', {
  1258. id: 'input_num1',
  1259. type: 'number',
  1260. 'onUpdate:modelValue': setNum.bind(this),
  1261. }),
  1262. this.num,
  1263. ),
  1264. ]
  1265. },
  1266. })
  1267. render(h(component), root)
  1268. const data = root._vnode.component.data
  1269. const inputNum1 = root.querySelector('#input_num1')!
  1270. expect(inputNum1.value).toBe('0')
  1271. inputNum1.value = '01'
  1272. triggerEvent('input', inputNum1)
  1273. await nextTick()
  1274. expect(data.num).toBe(1)
  1275. expect(inputNum1.value).toBe('1')
  1276. })
  1277. it(`should support mutating an array or set value for a checkbox`, async () => {
  1278. const component = defineComponent({
  1279. data() {
  1280. return { value: [] }
  1281. },
  1282. render() {
  1283. return [
  1284. withDirectives(
  1285. h('input', {
  1286. type: 'checkbox',
  1287. class: 'foo',
  1288. value: 'foo',
  1289. 'onUpdate:modelValue': setValue.bind(this),
  1290. }),
  1291. [[vModelCheckbox, this.value]],
  1292. ),
  1293. ]
  1294. },
  1295. })
  1296. render(h(component), root)
  1297. const foo = root.querySelector('.foo')
  1298. const data = root._vnode.component.data
  1299. expect(foo.checked).toEqual(false)
  1300. data.value.push('foo')
  1301. await nextTick()
  1302. expect(foo.checked).toEqual(true)
  1303. data.value[0] = 'bar'
  1304. await nextTick()
  1305. expect(foo.checked).toEqual(false)
  1306. data.value = new Set()
  1307. await nextTick()
  1308. expect(foo.checked).toEqual(false)
  1309. data.value.add('foo')
  1310. await nextTick()
  1311. expect(foo.checked).toEqual(true)
  1312. data.value.delete('foo')
  1313. await nextTick()
  1314. expect(foo.checked).toEqual(false)
  1315. })
  1316. })