functional.spec.js 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. import Vue from 'vue'
  2. import { createEmptyVNode } from 'core/vdom/vnode'
  3. describe('Options functional', () => {
  4. it('should work', done => {
  5. const vm = new Vue({
  6. data: { test: 'foo' },
  7. template: '<div><wrap :msg="test">bar</wrap></div>',
  8. components: {
  9. wrap: {
  10. functional: true,
  11. props: ['msg'],
  12. render (h, { props, children }) {
  13. return h('div', null, [props.msg, ' '].concat(children))
  14. }
  15. }
  16. }
  17. }).$mount()
  18. expect(vm.$el.innerHTML).toBe('<div>foo bar</div>')
  19. vm.test = 'qux'
  20. waitForUpdate(() => {
  21. expect(vm.$el.innerHTML).toBe('<div>qux bar</div>')
  22. }).then(done)
  23. })
  24. it('should expose all props when not declared', done => {
  25. const fn = {
  26. functional: true,
  27. render (h, { props }) {
  28. return h('div', `${props.msg} ${props.kebabMsg}`)
  29. }
  30. }
  31. const vm = new Vue({
  32. data: { test: 'foo' },
  33. render (h) {
  34. return h('div', [
  35. h(fn, {
  36. props: { msg: this.test },
  37. attrs: { 'kebab-msg': 'bar' }
  38. })
  39. ])
  40. }
  41. }).$mount()
  42. expect(vm.$el.innerHTML).toBe('<div>foo bar</div>')
  43. vm.test = 'qux'
  44. waitForUpdate(() => {
  45. expect(vm.$el.innerHTML).toBe('<div>qux bar</div>')
  46. }).then(done)
  47. })
  48. it('should expose data.on as listeners', () => {
  49. const foo = jasmine.createSpy('foo')
  50. const bar = jasmine.createSpy('bar')
  51. const vm = new Vue({
  52. template: '<div><wrap @click="foo" @test="bar"/></div>',
  53. methods: { foo, bar },
  54. components: {
  55. wrap: {
  56. functional: true,
  57. render (h, { listeners }) {
  58. return h('div', {
  59. on: {
  60. click: [listeners.click, () => listeners.test('bar')]
  61. }
  62. })
  63. }
  64. }
  65. }
  66. }).$mount()
  67. document.body.appendChild(vm.$el)
  68. triggerEvent(vm.$el.children[0], 'click')
  69. expect(foo).toHaveBeenCalled()
  70. expect(foo.calls.argsFor(0)[0].type).toBe('click') // should have click event
  71. triggerEvent(vm.$el.children[0], 'mousedown')
  72. expect(bar).toHaveBeenCalledWith('bar')
  73. document.body.removeChild(vm.$el)
  74. })
  75. it('should support returning more than one root node', () => {
  76. const vm = new Vue({
  77. template: `<div><test></test></div>`,
  78. components: {
  79. test: {
  80. functional: true,
  81. render (h) {
  82. return [h('span', 'foo'), h('span', 'bar')]
  83. }
  84. }
  85. }
  86. }).$mount()
  87. expect(vm.$el.innerHTML).toBe('<span>foo</span><span>bar</span>')
  88. })
  89. it('should support slots', () => {
  90. const vm = new Vue({
  91. data: { test: 'foo' },
  92. template: '<div><wrap><div slot="a">foo</div><div slot="b">bar</div></wrap></div>',
  93. components: {
  94. wrap: {
  95. functional: true,
  96. props: ['msg'],
  97. render (h, { slots }) {
  98. slots = slots()
  99. return h('div', null, [slots.b, slots.a])
  100. }
  101. }
  102. }
  103. }).$mount()
  104. expect(vm.$el.innerHTML).toBe('<div><div>bar</div><div>foo</div></div>')
  105. })
  106. it('should let vnode raw data pass through', done => {
  107. const onValid = jasmine.createSpy('valid')
  108. const vm = new Vue({
  109. data: { msg: 'hello' },
  110. template: `<div>
  111. <validate field="field1" @valid="onValid">
  112. <input type="text" v-model="msg">
  113. </validate>
  114. </div>`,
  115. components: {
  116. validate: {
  117. functional: true,
  118. props: ['field'],
  119. render (h, { props, children, data: { on }}) {
  120. props.child = children[0]
  121. return h('validate-control', { props, on })
  122. }
  123. },
  124. 'validate-control': {
  125. props: ['field', 'child'],
  126. render () {
  127. return this.child
  128. },
  129. mounted () {
  130. this.$el.addEventListener('input', this.onInput)
  131. },
  132. destroyed () {
  133. this.$el.removeEventListener('input', this.onInput)
  134. },
  135. methods: {
  136. onInput (e) {
  137. const value = e.target.value
  138. if (this.validate(value)) {
  139. this.$emit('valid', this)
  140. }
  141. },
  142. // something validation logic here
  143. validate (val) {
  144. return val.length > 0
  145. }
  146. }
  147. }
  148. },
  149. methods: { onValid }
  150. }).$mount()
  151. document.body.appendChild(vm.$el)
  152. const input = vm.$el.querySelector('input')
  153. expect(onValid).not.toHaveBeenCalled()
  154. waitForUpdate(() => {
  155. input.value = 'foo'
  156. triggerEvent(input, 'input')
  157. }).then(() => {
  158. expect(onValid).toHaveBeenCalled()
  159. }).then(() => {
  160. document.body.removeChild(vm.$el)
  161. vm.$destroy()
  162. }).then(done)
  163. })
  164. it('create empty vnode when render return null', () => {
  165. const child = {
  166. functional: true,
  167. render () {
  168. return null
  169. }
  170. }
  171. const vm = new Vue({
  172. components: {
  173. child
  174. }
  175. })
  176. const h = vm.$createElement
  177. const vnode = h('child')
  178. expect(vnode).toEqual(createEmptyVNode())
  179. })
  180. // #7282
  181. it('should normalize top-level arrays', () => {
  182. const Foo = {
  183. functional: true,
  184. render (h) {
  185. return [h('span', 'hi'), null]
  186. }
  187. }
  188. const vm = new Vue({
  189. template: `<div><foo/></div>`,
  190. components: { Foo }
  191. }).$mount()
  192. expect(vm.$el.innerHTML).toBe('<span>hi</span>')
  193. })
  194. it('should work when used as named slot and returning array', () => {
  195. const Foo = {
  196. template: `<div><slot name="test"/></div>`
  197. }
  198. const Bar = {
  199. functional: true,
  200. render: h => ([
  201. h('div', 'one'),
  202. h('div', 'two'),
  203. h(Baz)
  204. ])
  205. }
  206. const Baz = {
  207. functional: true,
  208. render: h => h('div', 'three')
  209. }
  210. const vm = new Vue({
  211. template: `<foo><bar slot="test"/></foo>`,
  212. components: { Foo, Bar }
  213. }).$mount()
  214. expect(vm.$el.innerHTML).toBe('<div>one</div><div>two</div><div>three</div>')
  215. })
  216. it('should apply namespace when returning arrays', () => {
  217. const Child = {
  218. functional: true,
  219. render: h => ([h('foo'), h('bar')])
  220. }
  221. const vm = new Vue({
  222. template: `<svg><child/></svg>`,
  223. components: { Child }
  224. }).$mount()
  225. expect(vm.$el.childNodes[0].namespaceURI).toContain('svg')
  226. expect(vm.$el.childNodes[1].namespaceURI).toContain('svg')
  227. })
  228. it('should work with render fns compiled from template', done => {
  229. // code generated via vue-template-es2015-compiler
  230. const render = function (_h, _vm) {
  231. const _c = _vm._c
  232. return _c(
  233. 'div',
  234. [
  235. _c('h2', { staticClass: 'red' }, [_vm._v(_vm._s(_vm.props.msg))]),
  236. _vm._t('default'),
  237. _vm._t('slot2'),
  238. _vm._t('scoped', null, { msg: _vm.props.msg }),
  239. _vm._m(0),
  240. _c('div', { staticClass: 'clickable', on: { click: _vm.parent.fn }}, [
  241. _vm._v('click me')
  242. ])
  243. ],
  244. 2
  245. )
  246. }
  247. const staticRenderFns = [
  248. function (_h, _vm) {
  249. const _c = _vm._c
  250. return _c('div', [_vm._v('Some '), _c('span', [_vm._v('text')])])
  251. }
  252. ]
  253. const child = {
  254. functional: true,
  255. _compiled: true,
  256. render,
  257. staticRenderFns
  258. }
  259. const parent = new Vue({
  260. components: {
  261. child
  262. },
  263. data: {
  264. msg: 'hello'
  265. },
  266. template: `
  267. <div>
  268. <child :msg="msg">
  269. <span>{{ msg }}</span>
  270. <div slot="slot2">Second slot</div>
  271. <template slot="scoped" slot-scope="scope">{{ scope.msg }}</template>
  272. </child>
  273. </div>
  274. `,
  275. methods: {
  276. fn () {
  277. this.msg = 'bye'
  278. }
  279. }
  280. }).$mount()
  281. function assertMarkup () {
  282. expect(parent.$el.innerHTML).toBe(
  283. `<div>` +
  284. `<h2 class="red">${parent.msg}</h2>` +
  285. `<span>${parent.msg}</span> ` +
  286. `<div>Second slot</div>` +
  287. parent.msg +
  288. // static
  289. `<div>Some <span>text</span></div>` +
  290. `<div class="clickable">click me</div>` +
  291. `</div>`
  292. )
  293. }
  294. assertMarkup()
  295. triggerEvent(parent.$el.querySelector('.clickable'), 'click')
  296. waitForUpdate(assertMarkup).then(done)
  297. })
  298. // #8468
  299. it('should normalize nested arrays when use functional components with v-for', () => {
  300. const Foo = {
  301. functional: true,
  302. props: {
  303. name: {}
  304. },
  305. render (h, context) {
  306. return [h('span', 'hi'), h('span', context.props.name)]
  307. }
  308. }
  309. const vm = new Vue({
  310. template: `<div><foo v-for="name in names" :name="name" /></div>`,
  311. data: {
  312. names: ['foo', 'bar']
  313. },
  314. components: { Foo }
  315. }).$mount()
  316. expect(vm.$el.innerHTML).toBe('<span>hi</span><span>foo</span><span>hi</span><span>bar</span>')
  317. })
  318. })