directives.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738
  1. describe('UNIT: Directives', function () {
  2. describe('attr', function () {
  3. var dir = mockDirective('attr', 'input'),
  4. el = dir.el
  5. it('should set a truthy attribute value', function () {
  6. var value = 'Arrrrrr!'
  7. dir.arg = 'value'
  8. dir.update(value)
  9. assert.strictEqual(el.getAttribute('value'), value)
  10. })
  11. it('should set attribute value to `0`', function () {
  12. dir.arg = 'value'
  13. dir.update(0)
  14. assert.strictEqual(el.getAttribute('value'), '0')
  15. })
  16. it('should remove an attribute if value is `false`', function () {
  17. dir.arg = 'disabled'
  18. el.setAttribute('disabled', 'disabled')
  19. dir.update(false)
  20. assert.strictEqual(el.getAttribute('disabled'), null)
  21. })
  22. it('should remove an attribute if value is `null`', function () {
  23. dir.arg = 'disabled'
  24. el.setAttribute('disabled', 'disabled')
  25. dir.update(null)
  26. assert.strictEqual(el.getAttribute('disabled'), null)
  27. })
  28. it('should remove an attribute if value is `undefined`', function () {
  29. dir.arg = 'disabled'
  30. el.setAttribute('disabled', 'disabled')
  31. dir.update(undefined)
  32. assert.strictEqual(el.getAttribute('disabled'), null)
  33. })
  34. })
  35. describe('text', function () {
  36. var dir = mockDirective('text')
  37. it('should work with a string', function () {
  38. dir.update('hallo')
  39. assert.strictEqual(dir.el.textContent, 'hallo')
  40. })
  41. it('should work with a number', function () {
  42. dir.update(12345)
  43. assert.strictEqual(dir.el.textContent, '12345')
  44. })
  45. it('should work with booleans', function () {
  46. dir.update(true)
  47. assert.strictEqual(dir.el.textContent, 'true')
  48. })
  49. it('should be empty with other stuff', function () {
  50. dir.update(null)
  51. assert.strictEqual(dir.el.textContent, '')
  52. dir.update(undefined)
  53. assert.strictEqual(dir.el.textContent, '')
  54. dir.update({a:123})
  55. assert.strictEqual(dir.el.textContent, '')
  56. dir.update(function () {})
  57. assert.strictEqual(dir.el.textContent, '')
  58. })
  59. })
  60. describe('html', function () {
  61. var dir = mockDirective('html')
  62. it('should work with a string', function () {
  63. dir.update('hi!!!')
  64. assert.strictEqual(dir.el.innerHTML, 'hi!!!')
  65. dir.update('<span>haha</span><a>lol</a>')
  66. assert.strictEqual(dir.el.querySelector('span').textContent, 'haha')
  67. })
  68. it('should work with a number', function () {
  69. dir.update(12345)
  70. assert.strictEqual(dir.el.innerHTML, '12345')
  71. })
  72. it('should work with booleans', function () {
  73. dir.update(true)
  74. assert.strictEqual(dir.el.textContent, 'true')
  75. })
  76. it('should be empty with other stuff', function () {
  77. dir.update(null)
  78. assert.strictEqual(dir.el.innerHTML, '')
  79. dir.update(undefined)
  80. assert.strictEqual(dir.el.innerHTML, '')
  81. dir.update({a:123})
  82. assert.strictEqual(dir.el.innerHTML, '')
  83. dir.update(function () {})
  84. assert.strictEqual(dir.el.innerHTML, '')
  85. })
  86. it('should swap html if el is a comment placeholder', function () {
  87. var dir = mockDirective('html'),
  88. comment = document.createComment('hi'),
  89. parent = dir.el
  90. parent.innerHTML = 'what!'
  91. parent.appendChild(comment)
  92. dir.el = comment
  93. dir.bind()
  94. assert.ok(dir.holder)
  95. assert.ok(dir.nodes)
  96. var pre = 'what!',
  97. after = '<!--hi-->',
  98. h1 = '<span>hello</span><span>world</span>',
  99. h2 = '<a>whats</a><a>up</a>'
  100. dir.update(h1)
  101. assert.strictEqual(parent.innerHTML, pre + h1 + after)
  102. dir.update(h2)
  103. assert.strictEqual(parent.innerHTML, pre + h2 + after)
  104. })
  105. })
  106. describe('show', function () {
  107. var dir = mockDirective('show')
  108. it('should be default value when value is truthy', function () {
  109. dir.update(1)
  110. assert.strictEqual(dir.el.style.display, '')
  111. dir.update('hi!')
  112. assert.strictEqual(dir.el.style.display, '')
  113. dir.update(true)
  114. assert.strictEqual(dir.el.style.display, '')
  115. dir.update({})
  116. assert.strictEqual(dir.el.style.display, '')
  117. dir.update(function () {})
  118. assert.strictEqual(dir.el.style.display, '')
  119. })
  120. it('should be none when value is falsy', function () {
  121. dir.update(0)
  122. assert.strictEqual(dir.el.style.display, 'none')
  123. dir.update('')
  124. assert.strictEqual(dir.el.style.display, 'none')
  125. dir.update(false)
  126. assert.strictEqual(dir.el.style.display, 'none')
  127. dir.update(null)
  128. assert.strictEqual(dir.el.style.display, 'none')
  129. dir.update(undefined)
  130. assert.strictEqual(dir.el.style.display, 'none')
  131. })
  132. })
  133. describe('class', function () {
  134. it('should set class to the value if it has no arg', function () {
  135. var dir = mockDirective('class')
  136. dir.update('test')
  137. assert.ok(dir.el.classList.contains('test'))
  138. dir.update('hoho')
  139. assert.ok(!dir.el.classList.contains('test'))
  140. assert.ok(dir.el.classList.contains('hoho'))
  141. })
  142. it('should add/remove class based on truthy/falsy if it has an arg', function () {
  143. var dir = mockDirective('class')
  144. dir.arg = 'test'
  145. dir.update(true)
  146. assert.ok(dir.el.classList.contains('test'))
  147. dir.update(false)
  148. assert.ok(!dir.el.classList.contains('test'))
  149. })
  150. })
  151. describe('model', function () {
  152. describe('input[checkbox]', function () {
  153. var dir = mockDirective('model', 'input', 'checkbox')
  154. dir.bind()
  155. before(function () {
  156. document.body.appendChild(dir.el)
  157. })
  158. it('should set checked on update()', function () {
  159. dir.update(true)
  160. assert.ok(dir.el.checked)
  161. dir.update(false)
  162. assert.ok(!dir.el.checked)
  163. })
  164. it('should trigger vm.$set when clicked', function () {
  165. var triggered = false
  166. dir.key = 'foo'
  167. dir.vm = { $set: function (key, val) {
  168. assert.strictEqual(key, 'foo')
  169. assert.strictEqual(val, true)
  170. triggered = true
  171. }}
  172. dir.el.dispatchEvent(mockMouseEvent('click'))
  173. assert.ok(triggered)
  174. })
  175. it('should remove event listener with unbind()', function () {
  176. var removed = true
  177. dir.vm.$set = function () {
  178. removed = false
  179. }
  180. dir.unbind()
  181. dir.el.dispatchEvent(mockMouseEvent('click'))
  182. assert.ok(removed)
  183. })
  184. after(function () {
  185. document.body.removeChild(dir.el)
  186. })
  187. })
  188. describe('input[radio]', function () {
  189. var dir1 = mockDirective('model', 'input', 'radio'),
  190. dir2 = mockDirective('model', 'input', 'radio')
  191. dir1.el.name = 'input-radio'
  192. dir2.el.name = 'input-radio'
  193. dir1.el.value = '12345'
  194. dir2.el.value = '54321'
  195. dir1.bind()
  196. dir2.bind()
  197. before(function () {
  198. document.body.appendChild(dir1.el)
  199. document.body.appendChild(dir2.el)
  200. })
  201. it('should set el.checked on update()', function () {
  202. assert.notOk(dir1.el.checked)
  203. dir1.update(12345)
  204. assert.ok(dir1.el.checked)
  205. })
  206. it('should trigger vm.$set when clicked', function () {
  207. var triggered = false
  208. dir2.key = 'radio'
  209. dir2.vm = { $set: function (key, val) {
  210. triggered = true
  211. assert.strictEqual(key, 'radio')
  212. assert.strictEqual(val, dir2.el.value)
  213. }}
  214. dir2.el.dispatchEvent(mockMouseEvent('click'))
  215. assert.ok(triggered)
  216. assert.ok(dir2.el.checked)
  217. assert.notOk(dir1.el.checked)
  218. })
  219. it('should remove listeners on unbind()', function () {
  220. var removed = true
  221. dir1.vm = { $set: function () {
  222. removed = false
  223. }}
  224. dir1.unbind()
  225. dir1.el.dispatchEvent(mockMouseEvent('click'))
  226. assert.ok(removed)
  227. })
  228. after(function () {
  229. document.body.removeChild(dir1.el)
  230. document.body.removeChild(dir2.el)
  231. })
  232. })
  233. describe('select', function () {
  234. var dir = mockDirective('model', 'select'),
  235. o1 = document.createElement('option'),
  236. o2 = document.createElement('option')
  237. o1.value = 0
  238. o2.value = 1
  239. dir.el.appendChild(o1)
  240. dir.el.appendChild(o2)
  241. dir.bind()
  242. before(function () {
  243. document.body.appendChild(dir.el)
  244. })
  245. it('should set value on update()', function () {
  246. dir.update(0)
  247. assert.strictEqual(dir.el.value, '0')
  248. })
  249. it('should trigger vm.$set when value is changed', function () {
  250. var triggered = false
  251. dir.key = 'select'
  252. dir.vm = { $set: function (key, val) {
  253. triggered = true
  254. assert.strictEqual(key, 'select')
  255. assert.equal(val, 1)
  256. }}
  257. dir.el.options.selectedIndex = 1
  258. dir.el.dispatchEvent(mockHTMLEvent('change'))
  259. assert.ok(triggered)
  260. })
  261. it('should remove listener on unbind()', function () {
  262. var removed = true
  263. dir.vm = { $set: function () {
  264. removed = false
  265. }}
  266. dir.unbind()
  267. dir.el.dispatchEvent(mockHTMLEvent('change'))
  268. assert.ok(removed)
  269. })
  270. after(function () {
  271. document.body.removeChild(dir.el)
  272. })
  273. })
  274. describe('input[text] and others', function () {
  275. var dir = mockDirective('model', 'input', 'text')
  276. dir.bind()
  277. before(function () {
  278. document.body.appendChild(dir.el)
  279. })
  280. it('should set the value on update()', function () {
  281. dir.update('foobar')
  282. assert.strictEqual(dir.el.value, 'foobar')
  283. })
  284. // `lazy` option is tested in the API suite
  285. it('should trigger vm.$set when value is changed via input', function () {
  286. var triggered = false
  287. dir.key = 'foo'
  288. dir.vm = { $set: function (key, val) {
  289. assert.ok(dir.lock, 'the directive should be locked if it has no filters')
  290. assert.strictEqual(key, 'foo')
  291. assert.strictEqual(val, 'bar')
  292. triggered = true
  293. }}
  294. dir.el.value = 'bar'
  295. dir.el.dispatchEvent(mockHTMLEvent('input'))
  296. assert.ok(triggered)
  297. })
  298. it('should remove event listener with unbind()', function () {
  299. var removed = true
  300. dir.vm.$set = function () {
  301. removed = false
  302. }
  303. dir.unbind()
  304. dir.el.dispatchEvent(mockHTMLEvent('input'))
  305. assert.ok(removed)
  306. })
  307. it('should not lock during vm.$set if it has filters', function (done) {
  308. var triggered = false
  309. var dir = mockDirective('model', 'input', 'text')
  310. dir.filters = []
  311. dir.bind()
  312. dir.vm = {$set:function () {
  313. assert.notOk(dir.lock)
  314. triggered = true
  315. }}
  316. dir.el.value = 'foo'
  317. document.body.appendChild(dir.el)
  318. dir.el.dispatchEvent(mockHTMLEvent('input'))
  319. // timeout becuase the update is async
  320. setTimeout(function () {
  321. assert.ok(triggered)
  322. document.body.removeChild(dir.el)
  323. done()
  324. }, 1)
  325. })
  326. after(function () {
  327. document.body.removeChild(dir.el)
  328. })
  329. })
  330. })
  331. describe('if', function () {
  332. it('should remain detached if it was detached during bind() and never attached', function () {
  333. var dir = mockDirective('if')
  334. dir.bind()
  335. dir.update(true)
  336. assert.notOk(dir.el.parentNode)
  337. dir.update(false)
  338. assert.notOk(dir.el.parentNode)
  339. })
  340. it('should remove el and insert ref when value is falsy', function () {
  341. var dir = mockDirective('if'),
  342. parent = document.createElement('div')
  343. parent.appendChild(dir.el)
  344. dir.bind()
  345. dir.update(false)
  346. assert.notOk(dir.el.parentNode)
  347. assert.notOk(parent.contains(dir.el))
  348. // phantomJS weird bug:
  349. // Node.contains() returns false when argument is a comment node.
  350. assert.strictEqual(dir.ref.parentNode, parent)
  351. })
  352. it('should append el and remove ref when value is truthy', function () {
  353. var dir = mockDirective('if'),
  354. parent = document.createElement('div')
  355. parent.appendChild(dir.el)
  356. dir.bind()
  357. dir.update(false)
  358. dir.update(true)
  359. assert.strictEqual(dir.el.parentNode, parent)
  360. assert.ok(parent.contains(dir.el))
  361. assert.notOk(parent.contains(dir.ref))
  362. })
  363. it('should work even if it was detached during bind()', function () {
  364. var dir = mockDirective('if')
  365. dir.bind()
  366. var parent = document.createElement('div')
  367. parent.appendChild(dir.el)
  368. dir.update(false)
  369. assert.strictEqual(dir.parent, parent)
  370. assert.notOk(dir.el.parentNode)
  371. assert.notOk(parent.contains(dir.el))
  372. assert.strictEqual(dir.ref.parentNode, parent)
  373. dir.update(true)
  374. assert.strictEqual(dir.el.parentNode, parent)
  375. assert.ok(parent.contains(dir.el))
  376. assert.notOk(parent.contains(dir.ref))
  377. })
  378. })
  379. describe('on (non-delegated only)', function () {
  380. var dir = mockDirective('on')
  381. dir.arg = 'click'
  382. before(function () {
  383. document.body.appendChild(dir.el)
  384. })
  385. it('should set the handler to be triggered by arg through update()', function () {
  386. var triggered = false
  387. dir.update(function () {
  388. triggered = true
  389. })
  390. dir.el.dispatchEvent(mockMouseEvent('click'))
  391. assert.ok(triggered)
  392. })
  393. it('should wrap the handler to supply expected args', function () {
  394. var vm = dir.binding.compiler.vm, // owner VM
  395. e = mockMouseEvent('click'), // original event
  396. triggered = false
  397. dir.update(function (ev) {
  398. assert.strictEqual(this, vm, 'handler should be called on owner VM')
  399. assert.strictEqual(ev, e, 'event should be passed in')
  400. assert.strictEqual(ev.vm, dir.vm)
  401. triggered = true
  402. })
  403. dir.el.dispatchEvent(e)
  404. assert.ok(triggered)
  405. })
  406. it('should remove previous handler when update() a new handler', function () {
  407. var triggered1 = false,
  408. triggered2 = false
  409. dir.update(function () {
  410. triggered1 = true
  411. })
  412. dir.update(function () {
  413. triggered2 = true
  414. })
  415. dir.el.dispatchEvent(mockMouseEvent('click'))
  416. assert.notOk(triggered1)
  417. assert.ok(triggered2)
  418. })
  419. it('should remove the handler in unbind()', function () {
  420. var triggered = false
  421. dir.update(function () {
  422. triggered = true
  423. })
  424. dir.unbind()
  425. dir.el.dispatchEvent(mockMouseEvent('click'))
  426. assert.notOk(triggered)
  427. assert.strictEqual(dir.handler, null, 'should remove reference to handler')
  428. assert.strictEqual(dir.el.vue_viewmodel, null, 'should remove reference to VM on the element')
  429. })
  430. it('should not use delegation if the event is blur or focus', function () {
  431. var dir = mockDirective('on', 'input'),
  432. triggerCount = 0,
  433. handler = function () {
  434. triggerCount++
  435. }
  436. document.body.appendChild(dir.el)
  437. dir.arg = 'focus'
  438. dir.update(handler)
  439. dir.el.dispatchEvent(mockHTMLEvent('focus'))
  440. assert.strictEqual(triggerCount, 1)
  441. dir.arg = 'blur'
  442. dir.update(handler)
  443. dir.el.dispatchEvent(mockHTMLEvent('blur'))
  444. assert.strictEqual(triggerCount, 2)
  445. document.body.removeChild(dir.el)
  446. })
  447. after(function () {
  448. document.body.removeChild(dir.el)
  449. })
  450. })
  451. describe('pre', function () {
  452. it('should skip compilation', function () {
  453. var testId = 'pre-test'
  454. mock(testId, '<span v-pre><strong>{{lol}}</strong><a v-text="hi"></a></span>')
  455. var t = new Vue({
  456. el: '#' + testId,
  457. data: {
  458. lol: 'heyhey',
  459. hi: 'hohoho'
  460. }
  461. })
  462. assert.strictEqual(t.$el.querySelector('strong').textContent, '{{lol}}')
  463. assert.strictEqual(t.$el.querySelector('a').textContent, '')
  464. assert.ok(t.$el.querySelector('a').hasAttribute('v-text'))
  465. })
  466. })
  467. describe('component', function () {
  468. it('should create a child viewmodel with given constructor', function () {
  469. var testId = 'component-test'
  470. mock(testId, '<div v-component="' + testId + '"></div>')
  471. var t = new Vue({
  472. el: '#' + testId,
  473. data: {
  474. msg: '123'
  475. },
  476. components: {
  477. 'component-test': {
  478. template: '<span>{{msg}}</span>'
  479. }
  480. }
  481. })
  482. assert.strictEqual(t.$el.querySelector('span').textContent, '123')
  483. })
  484. })
  485. describe('with', function () {
  486. it('should create a child viewmodel with given data', function () {
  487. var testId = 'with-test'
  488. mock(testId, '<span v-with="test">{{msg}}</span>')
  489. var t = new Vue({
  490. el: '#' + testId,
  491. data: {
  492. test: {
  493. msg: testId
  494. }
  495. }
  496. })
  497. assert.strictEqual(t.$el.querySelector('span').textContent, testId)
  498. })
  499. })
  500. describe('component-id', function () {
  501. it('should register a VM isntance on its parent\'s $', function () {
  502. var called = false
  503. var Child = Vue.extend({
  504. methods: {
  505. test: function () {
  506. called = true
  507. }
  508. }
  509. })
  510. var t = new Vue({
  511. template: '<div v-component="child" v-component-id="hihi"></div>',
  512. components: {
  513. child: Child
  514. }
  515. })
  516. assert.ok(t.$.hihi instanceof Child)
  517. t.$.hihi.test()
  518. assert.ok(called)
  519. t.$.hihi.$destroy()
  520. assert.notOk('hihi' in t.$)
  521. })
  522. })
  523. // More detailed testing for v-repeat can be found in functional tests.
  524. // this is mainly for code coverage
  525. describe('repeat', function () {
  526. var nextTick = require('vue/src/utils').nextTick,
  527. VM = require('vue/src/viewmodel')
  528. it('should work', function (done) {
  529. var handlerCalled = false
  530. var v = new Vue({
  531. template: '<span v-repeat="items" v-on="click:check">{{title}}</span>',
  532. data: {
  533. items: [
  534. {title: 1},
  535. {title: 2}
  536. ]
  537. },
  538. methods: {
  539. check: function (e) {
  540. assert.ok(e.targetVM instanceof VM)
  541. assert.strictEqual(this, v)
  542. handlerCalled = true
  543. }
  544. }
  545. })
  546. nextTick(function () {
  547. assert.equal(v.$el.innerHTML, '<span>1</span><span>2</span><!--v-repeat-items-->')
  548. v.items.push({title:3})
  549. v.items.pop()
  550. v.items.unshift({title:0})
  551. v.items.shift()
  552. v.items.splice(0, 1, {title:-1})
  553. v.items.sort(function (a, b) {
  554. return a.title > b.title
  555. })
  556. v.items.reverse()
  557. nextTick(function () {
  558. assert.equal(v.$el.innerHTML, '<span>2</span><span>-1</span><!--v-repeat-items-->')
  559. testHandler()
  560. })
  561. })
  562. function testHandler () {
  563. document.getElementById('test').appendChild(v.$el)
  564. var span = v.$el.querySelector('span'),
  565. e = mockMouseEvent('click')
  566. span.dispatchEvent(e)
  567. nextTick(function () {
  568. assert.ok(handlerCalled)
  569. done()
  570. })
  571. }
  572. })
  573. })
  574. describe('style', function () {
  575. it('should apply a normal style', function () {
  576. var d = mockDirective('style')
  577. d.arg = 'text-align'
  578. d.bind()
  579. assert.strictEqual(d.prop, 'textAlign')
  580. d.update('center')
  581. assert.strictEqual(d.el.style.textAlign, 'center')
  582. })
  583. it('should apply prefixed style', function () {
  584. var d = mockDirective('style')
  585. d.arg = '-webkit-transform'
  586. d.bind()
  587. assert.strictEqual(d.prop, 'webkitTransform')
  588. d.update('scale(2)')
  589. assert.strictEqual(d.el.style.webkitTransform, 'scale(2)')
  590. })
  591. it('should auto prefix styles', function () {
  592. var d = mockDirective('style')
  593. d.arg = '$transform'
  594. d.bind()
  595. assert.ok(d.prefixed)
  596. assert.strictEqual(d.prop, 'transform')
  597. var val = 'scale(2)'
  598. d.update(val)
  599. assert.strictEqual(d.el.style.transform, val)
  600. assert.strictEqual(d.el.style.webkitTransform, val)
  601. assert.strictEqual(d.el.style.mozTransform, val)
  602. assert.strictEqual(d.el.style.msTransform, val)
  603. })
  604. })
  605. })
  606. function mockDirective (dirName, tag, type) {
  607. var dir = Vue.directive(dirName),
  608. ret = {
  609. binding: { compiler: { vm: {} } },
  610. compiler: { vm: {}, options: {}, execHook: function () {}},
  611. el: document.createElement(tag || 'div')
  612. }
  613. if (typeof dir === 'function') {
  614. ret.update = dir
  615. } else {
  616. for (var key in dir) {
  617. ret[key] = dir[key]
  618. }
  619. }
  620. if (tag === 'input') ret.el.type = type || 'text'
  621. return ret
  622. }