Skip to content

Commit 8cd2d0b

Browse files
committed
support functional templates
1 parent 71f7df8 commit 8cd2d0b

File tree

6 files changed

+122
-16
lines changed

6 files changed

+122
-16
lines changed

lib/component-normalizer.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
module.exports = function normalizeComponent (
22
rawScriptExports,
33
compiledTemplate,
4+
functionalTemplate,
45
scopeId,
56
cssModules
67
) {
@@ -23,6 +24,12 @@ module.exports = function normalizeComponent (
2324
if (compiledTemplate) {
2425
options.render = compiledTemplate.render
2526
options.staticRenderFns = compiledTemplate.staticRenderFns
27+
options.compiled = true
28+
}
29+
30+
// functional template
31+
if (functionalTemplate) {
32+
options.functional = true
2633
}
2734

2835
// scopedId

lib/loader.js

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,10 @@ module.exports = function (content) {
162162
if (type === 'template' && loader !== defaultLoaders.html) {
163163
loader = defaultLoaders.html + '!' + loader
164164
}
165+
// if template is functional add functional to loader sting
166+
if (type === 'template' && loader === defaultLoaders.html) {
167+
loader += '&functional=' + part.functional
168+
}
165169
// inject rewriter before css/html loader for
166170
// extractTextPlugin use cases
167171
if (rewriterInjectRE.test(loader)) {
@@ -176,7 +180,8 @@ module.exports = function (content) {
176180
// unknown lang, infer the loader to be used
177181
switch (type) {
178182
case 'template':
179-
return defaultLoaders.html + '!' + templateLoaderPath + '?raw&engine=' + lang + '!'
183+
const options = '?raw&engine=' + lang + '&functional=' + !!part.functional + '!'
184+
return defaultLoaders.html + '!' + templateLoaderPath + options
180185
case 'styles':
181186
loader = addCssModulesToLoader(defaultLoaders.css, part, index)
182187
return loader + '!' + rewriter + ensureBang(ensureLoader(lang))
@@ -296,6 +301,7 @@ module.exports = function (content) {
296301
// normalizeComponent(
297302
// scriptExports,
298303
// compiledTemplate,
304+
// functionalTemplate,
299305
// scopeId,
300306
// cssModules
301307
// )
@@ -331,6 +337,11 @@ module.exports = function (content) {
331337
}
332338
output += ',\n'
333339

340+
// <template functional>
341+
output += ' /* template functional */\n '
342+
output += template && template.functional ? 'true' : 'null'
343+
output += ',\n'
344+
334345
// scopeId
335346
output += ' /* scopeId */\n '
336347
output += (hasScoped ? JSON.stringify(moduleId) : 'null') + ',\n'
@@ -359,15 +370,6 @@ module.exports = function (content) {
359370
'})) {' +
360371
'console.error("named exports are not supported in *.vue files.")' +
361372
'}\n'
362-
// check functional components used with templates
363-
if (template) {
364-
output +=
365-
'if (Component.options.functional) {' +
366-
'console.error("' +
367-
'[vue-loader] ' + fileName + ': functional components are not ' +
368-
'supported with templates, they should use render functions.' +
369-
'")}\n'
370-
}
371373
}
372374

373375
if (!query.inject) {

lib/template-compiler.js

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,18 @@ module.exports = function (html) {
7070
})
7171
code = 'module.exports={render:function(){},staticRenderFns:[]}'
7272
} else {
73-
var bubleOptions = vueOptions.buble
73+
var bubleOptions = Object.assign({}, vueOptions.buble || {})
74+
bubleOptions.transforms = Object.assign({}, bubleOptions.transforms || {}, {
75+
stripWithFunctional: query.functional
76+
})
77+
var staticRenderFns = compiled.staticRenderFns.map((fn) => {
78+
return toFunction(fn, query.functional)
79+
})
7480
code = transpile('module.exports={' +
75-
'render:' + toFunction(compiled.render) + ',' +
76-
'staticRenderFns: [' + compiled.staticRenderFns.map(toFunction).join(',') + ']' +
81+
'render:' + toFunction(compiled.render, query.functional) + ',' +
82+
'staticRenderFns: [' + staticRenderFns.join(',') + ']' +
7783
'}', bubleOptions)
84+
7885
// mark with stripped (this enables Vue to use correct runtime proxy detection)
7986
if (!isProduction && (
8087
!bubleOptions ||
@@ -99,8 +106,8 @@ module.exports = function (html) {
99106
return code
100107
}
101108

102-
function toFunction (code) {
103-
return 'function (){' + beautify(code, {
109+
function toFunction (code, functional) {
110+
return 'function (_h' + functional ? ',_vm' : '' + '){' + beautify(code, {
104111
indent_size: 2 // eslint-disable-line camelcase
105112
}) + '}'
106113
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@
7676
"stylus-loader": "^2.0.0",
7777
"sugarss": "^0.2.0",
7878
"url-loader": "^0.5.7",
79-
"vue": "^2.1.0",
79+
"vue": "2.1.10",
8080
"vue-server-renderer": "^2.1.10",
8181
"vue-template-compiler": "^2.1.0",
8282
"webpack": "^2.2.0"

test/fixtures/functional.vue

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<template functional>
2+
<div>
3+
<h2 class="red">{{ props.msg }}</h2>
4+
<slot></slot>
5+
<slot name="slot2"></slot>
6+
<slot :msg="props.msg" name="scoped"></slot>
7+
<div>Some <span>text</span></div>
8+
<div v-if="false">Not exist</div>
9+
</div>
10+
</template>
11+
12+
<script>
13+
export default {
14+
props: {
15+
msg: {
16+
type: String,
17+
default: 'hello'
18+
}
19+
}
20+
}
21+
</script>
22+
23+
<style>
24+
comp-a h2 {
25+
color: #f00;
26+
}
27+
</style>

test/test.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ var jsdom = require('jsdom')
55
var webpack = require('webpack')
66
var MemoryFS = require('memory-fs')
77
var expect = require('chai').expect
8+
var Vue = require('vue')
89
var genId = require('../lib/gen-id')
910
var SSR = require('vue-server-renderer')
1011
var compiler = require('../lib/template-compiler')
@@ -689,4 +690,66 @@ describe('vue-loader', function () {
689690
done()
690691
})
691692
})
693+
694+
// Vue required tests for more complete test cases
695+
it('should allow functional template', done => {
696+
test({
697+
entry: './test/fixtures/functional.vue',
698+
vue: {
699+
preserveWhitespace: false
700+
}
701+
}, (window, module) => {
702+
const vm = new Vue({
703+
render (h) {
704+
const _vm = this
705+
return _vm._c(
706+
module,
707+
{
708+
scopedSlots: {
709+
scoped: function (props) {
710+
return [_vm._v(_vm._s(props.msg))]
711+
}
712+
}
713+
},
714+
[
715+
_vm._c('div', [_vm._v('Default slot')]),
716+
_vm._c('p', { slot: 'slot2' }, [_vm._v('Second slot')])
717+
]
718+
)
719+
}
720+
}).$mount()
721+
722+
expect(module.compiled).to.equal(true)
723+
expect(module.functional).to.equal(true)
724+
expect(module.staticRenderFns).to.exist
725+
expect(module.render).to.be.a('function')
726+
727+
728+
// Mount results
729+
const vnode = vm._vnode
730+
const children = vnode.children
731+
// Root vnode
732+
expect(vnode.tag).to.equal('div')
733+
// Basic vnode
734+
expect(children[0].data.staticClass).to.equal('red')
735+
expect(children[0].children[0].text).to.equal('hello')
736+
// Default slot vnode
737+
expect(children[1].tag).to.equal('div')
738+
expect(children[1].children[0].text).to.equal('Default slot')
739+
// Named slot vnode
740+
expect(children[2].tag).to.equal('p')
741+
expect(children[2].children[0].text).to.equal('Second slot')
742+
// Scoped slot vnode
743+
expect(children[3].text).to.equal('hello')
744+
// Static content vnode
745+
expect(children[4].tag).to.equal('div')
746+
expect(children[4].children[0].text).to.equal('Some ')
747+
expect(children[4].children[1].tag).to.equal('span')
748+
expect(children[4].children[1].children[0].text).to.equal('text')
749+
// v-if vnode
750+
expect(children[5].text).to.equal('')
751+
752+
done()
753+
})
754+
})
692755
})

0 commit comments

Comments
 (0)