vModel.spec.ts 13 KB

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