vModel.spec.ts 13 KB

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