directives.js 21 KB

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