functional.spec.ts 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  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 = vi.fn()
  50. const bar = vi.fn()
  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.mock.calls[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 expose scopedSlots on render context', () => {
  76. const vm = new Vue({
  77. template:
  78. '<div><wrap>foo<p slot="p" slot-scope="a">{{ a }}</p></wrap></div>',
  79. components: {
  80. wrap: {
  81. functional: true,
  82. render(h, { scopedSlots }) {
  83. return [
  84. // scoped
  85. scopedSlots.p('a'),
  86. // normal slot content should be exposed as well
  87. scopedSlots.default()
  88. ]
  89. }
  90. }
  91. }
  92. }).$mount()
  93. expect(vm.$el.textContent).toBe('afoo')
  94. })
  95. it('should support returning more than one root node', () => {
  96. const vm = new Vue({
  97. template: `<div><test></test></div>`,
  98. components: {
  99. test: {
  100. functional: true,
  101. render(h) {
  102. return [h('span', 'foo'), h('span', 'bar')]
  103. }
  104. }
  105. }
  106. }).$mount()
  107. expect(vm.$el.innerHTML).toBe('<span>foo</span><span>bar</span>')
  108. })
  109. it('should support slots', () => {
  110. const vm = new Vue({
  111. data: { test: 'foo' },
  112. template:
  113. '<div><wrap><div slot="a">foo</div><div slot="b">bar</div></wrap></div>',
  114. components: {
  115. wrap: {
  116. functional: true,
  117. props: ['msg'],
  118. render(h, { slots }) {
  119. slots = slots()
  120. return h('div', null, [slots.b, slots.a])
  121. }
  122. }
  123. }
  124. }).$mount()
  125. expect(vm.$el.innerHTML).toBe('<div><div>bar</div><div>foo</div></div>')
  126. })
  127. it('should let vnode raw data pass through', done => {
  128. const onValid = vi.fn()
  129. const vm = new Vue({
  130. data: { msg: 'hello' },
  131. template: `<div>
  132. <validate field="field1" @valid="onValid">
  133. <input type="text" v-model="msg">
  134. </validate>
  135. </div>`,
  136. components: {
  137. validate: {
  138. functional: true,
  139. props: ['field'],
  140. render(h, { props, children, data: { on } }) {
  141. props.child = children[0]
  142. return h('validate-control', { props, on })
  143. }
  144. },
  145. 'validate-control': {
  146. props: ['field', 'child'],
  147. render() {
  148. return this.child
  149. },
  150. mounted() {
  151. this.$el.addEventListener('input', this.onInput)
  152. },
  153. destroyed() {
  154. this.$el.removeEventListener('input', this.onInput)
  155. },
  156. methods: {
  157. onInput(e) {
  158. const value = e.target.value
  159. if (this.validate(value)) {
  160. this.$emit('valid', this)
  161. }
  162. },
  163. // something validation logic here
  164. validate(val) {
  165. return val.length > 0
  166. }
  167. }
  168. }
  169. },
  170. methods: { onValid }
  171. }).$mount()
  172. document.body.appendChild(vm.$el)
  173. const input = vm.$el.querySelector('input')
  174. expect(onValid).not.toHaveBeenCalled()
  175. waitForUpdate(() => {
  176. input.value = 'foo'
  177. triggerEvent(input, 'input')
  178. })
  179. .then(() => {
  180. expect(onValid).toHaveBeenCalled()
  181. })
  182. .then(() => {
  183. document.body.removeChild(vm.$el)
  184. vm.$destroy()
  185. })
  186. .then(done)
  187. })
  188. it('create empty vnode when render return null', () => {
  189. const child = {
  190. functional: true,
  191. render() {
  192. return null
  193. }
  194. }
  195. const vm = new Vue({
  196. components: {
  197. child
  198. }
  199. })
  200. const h = vm.$createElement
  201. const vnode = h('child')
  202. expect(vnode).toEqual(createEmptyVNode())
  203. })
  204. // #7282
  205. it('should normalize top-level arrays', () => {
  206. const Foo = {
  207. functional: true,
  208. render(h) {
  209. return [h('span', 'hi'), null]
  210. }
  211. }
  212. const vm = new Vue({
  213. template: `<div><foo/></div>`,
  214. components: { Foo }
  215. }).$mount()
  216. expect(vm.$el.innerHTML).toBe('<span>hi</span>')
  217. })
  218. it('should work when used as named slot and returning array', () => {
  219. const Foo = {
  220. template: `<div><slot name="test"/></div>`
  221. }
  222. const Bar = {
  223. functional: true,
  224. render: h => [h('div', 'one'), h('div', 'two'), h(Baz)]
  225. }
  226. const Baz = {
  227. functional: true,
  228. render: h => h('div', 'three')
  229. }
  230. const vm = new Vue({
  231. template: `<foo><bar slot="test"/></foo>`,
  232. components: { Foo, Bar }
  233. }).$mount()
  234. expect(vm.$el.innerHTML).toBe(
  235. '<div>one</div><div>two</div><div>three</div>'
  236. )
  237. })
  238. it('should apply namespace when returning arrays', () => {
  239. const Child = {
  240. functional: true,
  241. render: h => [h('foo'), h('bar')]
  242. }
  243. const vm = new Vue({
  244. template: `<svg><child/></svg>`,
  245. components: { Child }
  246. }).$mount()
  247. expect(vm.$el.childNodes[0].namespaceURI).toContain('svg')
  248. expect(vm.$el.childNodes[1].namespaceURI).toContain('svg')
  249. })
  250. it('should work with render fns compiled from template', done => {
  251. const render = function (_h, _vm) {
  252. const _c = _vm._c
  253. return _c(
  254. 'div',
  255. [
  256. _c('h2', { staticClass: 'red' }, [_vm._v(_vm._s(_vm.props.msg))]),
  257. _vm._t('default'),
  258. _vm._t('slot2'),
  259. _vm._t('scoped', null, { msg: _vm.props.msg }),
  260. _vm._m(0),
  261. _c(
  262. 'div',
  263. { staticClass: 'clickable', on: { click: _vm.parent.fn } },
  264. [_vm._v('click me')]
  265. )
  266. ],
  267. 2
  268. )
  269. }
  270. const staticRenderFns = [
  271. function (_h, _vm) {
  272. const _c = _vm._c
  273. return _c('div', [_vm._v('Some '), _c('span', [_vm._v('text')])])
  274. }
  275. ]
  276. const child = {
  277. functional: true,
  278. _compiled: true,
  279. render,
  280. staticRenderFns
  281. }
  282. const parent = new Vue({
  283. components: {
  284. child
  285. },
  286. data: {
  287. msg: 'hello'
  288. },
  289. template: `
  290. <div>
  291. <child :msg="msg">
  292. <span>{{ msg }}</span>
  293. <div slot="slot2">Second slot</div>
  294. <template slot="scoped" slot-scope="scope">{{ scope.msg }}</template>
  295. </child>
  296. </div>
  297. `,
  298. methods: {
  299. fn() {
  300. this.msg = 'bye'
  301. }
  302. }
  303. }).$mount()
  304. function assertMarkup() {
  305. expect(parent.$el.innerHTML).toBe(
  306. `<div>` +
  307. `<h2 class="red">${parent.msg}</h2>` +
  308. `<span>${parent.msg}</span> ` +
  309. `<div>Second slot</div>` +
  310. parent.msg +
  311. // static
  312. `<div>Some <span>text</span></div>` +
  313. `<div class="clickable">click me</div>` +
  314. `</div>`
  315. )
  316. }
  317. assertMarkup()
  318. triggerEvent(parent.$el.querySelector('.clickable'), 'click')
  319. waitForUpdate(assertMarkup).then(done)
  320. })
  321. // #8468
  322. it('should normalize nested arrays when use functional components with v-for', () => {
  323. const Foo = {
  324. functional: true,
  325. props: {
  326. name: {}
  327. },
  328. render(h, context) {
  329. return [h('span', 'hi'), h('span', context.props.name)]
  330. }
  331. }
  332. const vm = new Vue({
  333. template: `<div><foo v-for="name in names" :name="name" /></div>`,
  334. data: {
  335. names: ['foo', 'bar']
  336. },
  337. components: { Foo }
  338. }).$mount()
  339. expect(vm.$el.innerHTML).toBe(
  340. '<span>hi</span><span>foo</span><span>hi</span><span>bar</span>'
  341. )
  342. })
  343. })