vModel.spec.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602
  1. import {
  2. h,
  3. render,
  4. nextTick,
  5. defineComponent,
  6. vModelDynamic,
  7. withDirectives,
  8. VNode,
  9. ref
  10. } from '@vue/runtime-dom'
  11. const triggerEvent = (type: string, el: Element) => {
  12. const event = new Event(type)
  13. el.dispatchEvent(event)
  14. }
  15. const withVModel = (node: VNode, arg: any, mods?: any) =>
  16. withDirectives(node, [[vModelDynamic, arg, '', mods]])
  17. const setValue = function(this: any, value: any) {
  18. this.value = value
  19. }
  20. let root: any
  21. beforeEach(() => {
  22. root = document.createElement('div') as any
  23. })
  24. describe('vModel', () => {
  25. it('should work with text input', async () => {
  26. const component = defineComponent({
  27. data() {
  28. return { value: null }
  29. },
  30. render() {
  31. return [
  32. withVModel(
  33. h('input', {
  34. 'onUpdate:modelValue': setValue.bind(this)
  35. }),
  36. this.value
  37. )
  38. ]
  39. }
  40. })
  41. render(h(component), root)
  42. const input = root.querySelector('input')!
  43. const data = root._vnode.component.data
  44. input.value = 'foo'
  45. triggerEvent('input', input)
  46. await nextTick()
  47. expect(data.value).toEqual('foo')
  48. data.value = 'bar'
  49. await nextTick()
  50. expect(input.value).toEqual('bar')
  51. })
  52. it('should work with multiple listeners', async () => {
  53. const spy = jest.fn()
  54. const component = defineComponent({
  55. data() {
  56. return { value: null }
  57. },
  58. render() {
  59. return [
  60. withVModel(
  61. h('input', {
  62. 'onUpdate:modelValue': [setValue.bind(this), spy]
  63. }),
  64. this.value
  65. )
  66. ]
  67. }
  68. })
  69. render(h(component), root)
  70. const input = root.querySelector('input')!
  71. const data = root._vnode.component.data
  72. input.value = 'foo'
  73. triggerEvent('input', input)
  74. await nextTick()
  75. expect(data.value).toEqual('foo')
  76. expect(spy).toHaveBeenCalledWith('foo')
  77. })
  78. it('should work with updated listeners', async () => {
  79. const spy1 = jest.fn()
  80. const spy2 = jest.fn()
  81. const toggle = ref(true)
  82. const component = defineComponent({
  83. render() {
  84. return [
  85. withVModel(
  86. h('input', {
  87. 'onUpdate:modelValue': toggle.value ? spy1 : spy2
  88. }),
  89. 'foo'
  90. )
  91. ]
  92. }
  93. })
  94. render(h(component), root)
  95. const input = root.querySelector('input')!
  96. input.value = 'foo'
  97. triggerEvent('input', input)
  98. await nextTick()
  99. expect(spy1).toHaveBeenCalledWith('foo')
  100. // update listener
  101. toggle.value = false
  102. await nextTick()
  103. input.value = 'bar'
  104. triggerEvent('input', input)
  105. await nextTick()
  106. expect(spy1).not.toHaveBeenCalledWith('bar')
  107. expect(spy2).toHaveBeenCalledWith('bar')
  108. })
  109. it('should work with textarea', async () => {
  110. const component = defineComponent({
  111. data() {
  112. return { value: null }
  113. },
  114. render() {
  115. return [
  116. withVModel(
  117. h('textarea', {
  118. 'onUpdate:modelValue': setValue.bind(this)
  119. }),
  120. this.value
  121. )
  122. ]
  123. }
  124. })
  125. render(h(component), root)
  126. const input = root.querySelector('textarea')
  127. const data = root._vnode.component.data
  128. input.value = 'foo'
  129. triggerEvent('input', input)
  130. await nextTick()
  131. expect(data.value).toEqual('foo')
  132. data.value = 'bar'
  133. await nextTick()
  134. expect(input.value).toEqual('bar')
  135. })
  136. it('should support modifiers', async () => {
  137. const component = defineComponent({
  138. data() {
  139. return { number: null, trim: null, lazy: null }
  140. },
  141. render() {
  142. return [
  143. withVModel(
  144. h('input', {
  145. class: 'number',
  146. 'onUpdate:modelValue': (val: any) => {
  147. this.number = val
  148. }
  149. }),
  150. this.number,
  151. {
  152. number: true
  153. }
  154. ),
  155. withVModel(
  156. h('input', {
  157. class: 'trim',
  158. 'onUpdate:modelValue': (val: any) => {
  159. this.trim = val
  160. }
  161. }),
  162. this.trim,
  163. {
  164. trim: true
  165. }
  166. ),
  167. withVModel(
  168. h('input', {
  169. class: 'lazy',
  170. 'onUpdate:modelValue': (val: any) => {
  171. this.lazy = val
  172. }
  173. }),
  174. this.lazy,
  175. {
  176. lazy: true
  177. }
  178. )
  179. ]
  180. }
  181. })
  182. render(h(component), root)
  183. const number = root.querySelector('.number')
  184. const trim = root.querySelector('.trim')
  185. const lazy = root.querySelector('.lazy')
  186. const data = root._vnode.component.data
  187. number.value = '+01.2'
  188. triggerEvent('input', number)
  189. await nextTick()
  190. expect(data.number).toEqual(1.2)
  191. trim.value = ' hello, world '
  192. triggerEvent('input', trim)
  193. await nextTick()
  194. expect(data.trim).toEqual('hello, world')
  195. lazy.value = 'foo'
  196. triggerEvent('change', lazy)
  197. await nextTick()
  198. expect(data.lazy).toEqual('foo')
  199. })
  200. it('should work with checkbox', async () => {
  201. const component = defineComponent({
  202. data() {
  203. return { value: null }
  204. },
  205. render() {
  206. return [
  207. withVModel(
  208. h('input', {
  209. type: 'checkbox',
  210. 'onUpdate:modelValue': setValue.bind(this)
  211. }),
  212. this.value
  213. )
  214. ]
  215. }
  216. })
  217. render(h(component), root)
  218. const input = root.querySelector('input')
  219. const data = root._vnode.component.data
  220. input.checked = true
  221. triggerEvent('change', input)
  222. await nextTick()
  223. expect(data.value).toEqual(true)
  224. data.value = false
  225. await nextTick()
  226. expect(input.checked).toEqual(false)
  227. data.value = true
  228. await nextTick()
  229. expect(input.checked).toEqual(true)
  230. input.checked = false
  231. triggerEvent('change', input)
  232. await nextTick()
  233. expect(data.value).toEqual(false)
  234. })
  235. it('should work with checkbox and true-value/false-value', async () => {
  236. const component = defineComponent({
  237. data() {
  238. return { value: null }
  239. },
  240. render() {
  241. return [
  242. withVModel(
  243. h('input', {
  244. type: 'checkbox',
  245. 'true-value': 'yes',
  246. 'false-value': 'no',
  247. 'onUpdate:modelValue': setValue.bind(this)
  248. }),
  249. this.value
  250. )
  251. ]
  252. }
  253. })
  254. render(h(component), root)
  255. const input = root.querySelector('input')
  256. const data = root._vnode.component.data
  257. input.checked = true
  258. triggerEvent('change', input)
  259. await nextTick()
  260. expect(data.value).toEqual('yes')
  261. data.value = 'no'
  262. await nextTick()
  263. expect(input.checked).toEqual(false)
  264. data.value = 'yes'
  265. await nextTick()
  266. expect(input.checked).toEqual(true)
  267. input.checked = false
  268. triggerEvent('change', input)
  269. await nextTick()
  270. expect(data.value).toEqual('no')
  271. })
  272. it('should work with checkbox and true-value/false-value with object values', async () => {
  273. const component = defineComponent({
  274. data() {
  275. return { value: null }
  276. },
  277. render() {
  278. return [
  279. withVModel(
  280. h('input', {
  281. type: 'checkbox',
  282. 'true-value': { yes: 'yes' },
  283. 'false-value': { no: 'no' },
  284. 'onUpdate:modelValue': setValue.bind(this)
  285. }),
  286. this.value
  287. )
  288. ]
  289. }
  290. })
  291. render(h(component), root)
  292. const input = root.querySelector('input')
  293. const data = root._vnode.component.data
  294. input.checked = true
  295. triggerEvent('change', input)
  296. await nextTick()
  297. expect(data.value).toEqual({ yes: 'yes' })
  298. data.value = { no: 'no' }
  299. await nextTick()
  300. expect(input.checked).toEqual(false)
  301. data.value = { yes: 'yes' }
  302. await nextTick()
  303. expect(input.checked).toEqual(true)
  304. input.checked = false
  305. triggerEvent('change', input)
  306. await nextTick()
  307. expect(data.value).toEqual({ no: 'no' })
  308. })
  309. it(`should support array as a checkbox model`, async () => {
  310. const component = defineComponent({
  311. data() {
  312. return { value: [] }
  313. },
  314. render() {
  315. return [
  316. withVModel(
  317. h('input', {
  318. type: 'checkbox',
  319. class: 'foo',
  320. value: 'foo',
  321. 'onUpdate:modelValue': setValue.bind(this)
  322. }),
  323. this.value
  324. ),
  325. withVModel(
  326. h('input', {
  327. type: 'checkbox',
  328. class: 'bar',
  329. value: 'bar',
  330. 'onUpdate:modelValue': setValue.bind(this)
  331. }),
  332. this.value
  333. )
  334. ]
  335. }
  336. })
  337. render(h(component), root)
  338. const foo = root.querySelector('.foo')
  339. const bar = root.querySelector('.bar')
  340. const data = root._vnode.component.data
  341. foo.checked = true
  342. triggerEvent('change', foo)
  343. await nextTick()
  344. expect(data.value).toMatchObject(['foo'])
  345. bar.checked = true
  346. triggerEvent('change', bar)
  347. await nextTick()
  348. expect(data.value).toMatchObject(['foo', 'bar'])
  349. bar.checked = false
  350. triggerEvent('change', bar)
  351. await nextTick()
  352. expect(data.value).toMatchObject(['foo'])
  353. foo.checked = false
  354. triggerEvent('change', foo)
  355. await nextTick()
  356. expect(data.value).toMatchObject([])
  357. data.value = ['foo']
  358. await nextTick()
  359. expect(bar.checked).toEqual(false)
  360. expect(foo.checked).toEqual(true)
  361. data.value = ['bar']
  362. await nextTick()
  363. expect(foo.checked).toEqual(false)
  364. expect(bar.checked).toEqual(true)
  365. data.value = []
  366. await nextTick()
  367. expect(foo.checked).toEqual(false)
  368. expect(bar.checked).toEqual(false)
  369. })
  370. it('should work with radio', async () => {
  371. const component = defineComponent({
  372. data() {
  373. return { value: null }
  374. },
  375. render() {
  376. return [
  377. withVModel(
  378. h('input', {
  379. type: 'radio',
  380. class: 'foo',
  381. value: 'foo',
  382. 'onUpdate:modelValue': setValue.bind(this)
  383. }),
  384. this.value
  385. ),
  386. withVModel(
  387. h('input', {
  388. type: 'radio',
  389. class: 'bar',
  390. value: 'bar',
  391. 'onUpdate:modelValue': setValue.bind(this)
  392. }),
  393. this.value
  394. )
  395. ]
  396. }
  397. })
  398. render(h(component), root)
  399. const foo = root.querySelector('.foo')
  400. const bar = root.querySelector('.bar')
  401. const data = root._vnode.component.data
  402. foo.checked = true
  403. triggerEvent('change', foo)
  404. await nextTick()
  405. expect(data.value).toEqual('foo')
  406. bar.checked = true
  407. triggerEvent('change', bar)
  408. await nextTick()
  409. expect(data.value).toEqual('bar')
  410. data.value = null
  411. await nextTick()
  412. expect(foo.checked).toEqual(false)
  413. expect(bar.checked).toEqual(false)
  414. data.value = 'foo'
  415. await nextTick()
  416. expect(foo.checked).toEqual(true)
  417. expect(bar.checked).toEqual(false)
  418. data.value = 'bar'
  419. await nextTick()
  420. expect(foo.checked).toEqual(false)
  421. expect(bar.checked).toEqual(true)
  422. })
  423. it('should work with single select', async () => {
  424. const component = defineComponent({
  425. data() {
  426. return { value: null }
  427. },
  428. render() {
  429. return [
  430. withVModel(
  431. h(
  432. 'select',
  433. {
  434. value: null,
  435. 'onUpdate:modelValue': setValue.bind(this)
  436. },
  437. [h('option', { value: 'foo' }), h('option', { value: 'bar' })]
  438. ),
  439. this.value
  440. )
  441. ]
  442. }
  443. })
  444. render(h(component), root)
  445. const input = root.querySelector('select')
  446. const foo = root.querySelector('option[value=foo]')
  447. const bar = root.querySelector('option[value=bar]')
  448. const data = root._vnode.component.data
  449. foo.selected = true
  450. triggerEvent('change', input)
  451. await nextTick()
  452. expect(data.value).toEqual('foo')
  453. foo.selected = false
  454. bar.selected = true
  455. triggerEvent('change', input)
  456. await nextTick()
  457. expect(data.value).toEqual('bar')
  458. foo.selected = false
  459. bar.selected = false
  460. data.value = 'foo'
  461. await nextTick()
  462. expect(input.value).toEqual('foo')
  463. expect(foo.selected).toEqual(true)
  464. expect(bar.selected).toEqual(false)
  465. foo.selected = true
  466. bar.selected = false
  467. data.value = 'bar'
  468. await nextTick()
  469. expect(input.value).toEqual('bar')
  470. expect(foo.selected).toEqual(false)
  471. expect(bar.selected).toEqual(true)
  472. })
  473. it('should work with multiple select', async () => {
  474. const component = defineComponent({
  475. data() {
  476. return { value: [] }
  477. },
  478. render() {
  479. return [
  480. withVModel(
  481. h(
  482. 'select',
  483. {
  484. value: null,
  485. multiple: true,
  486. 'onUpdate:modelValue': setValue.bind(this)
  487. },
  488. [h('option', { value: 'foo' }), h('option', { value: 'bar' })]
  489. ),
  490. this.value
  491. )
  492. ]
  493. }
  494. })
  495. render(h(component), root)
  496. const input = root.querySelector('select')
  497. const foo = root.querySelector('option[value=foo]')
  498. const bar = root.querySelector('option[value=bar]')
  499. const data = root._vnode.component.data
  500. foo.selected = true
  501. triggerEvent('change', input)
  502. await nextTick()
  503. expect(data.value).toMatchObject(['foo'])
  504. foo.selected = false
  505. bar.selected = true
  506. triggerEvent('change', input)
  507. await nextTick()
  508. expect(data.value).toMatchObject(['bar'])
  509. foo.selected = true
  510. bar.selected = true
  511. triggerEvent('change', input)
  512. await nextTick()
  513. expect(data.value).toMatchObject(['foo', 'bar'])
  514. foo.selected = false
  515. bar.selected = false
  516. data.value = ['foo']
  517. await nextTick()
  518. expect(input.value).toEqual('foo')
  519. expect(foo.selected).toEqual(true)
  520. expect(bar.selected).toEqual(false)
  521. foo.selected = false
  522. bar.selected = false
  523. data.value = ['foo', 'bar']
  524. await nextTick()
  525. expect(foo.selected).toEqual(true)
  526. expect(bar.selected).toEqual(true)
  527. })
  528. })