Skip to content

Commit c8a016a

Browse files
blake-newmanyyx990803
authored andcommitted
feat: (wip) support for functional template compilation
1 parent eea5c17 commit c8a016a

File tree

6 files changed

+133
-68
lines changed

6 files changed

+133
-68
lines changed

lib/component-normalizer.js

+8
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
module.exports = function normalizeComponent (
77
rawScriptExports,
88
compiledTemplate,
9+
functionalTemplate,
910
injectStyles,
1011
scopeId,
1112
moduleIdentifier /* server only */
@@ -29,6 +30,12 @@ module.exports = function normalizeComponent (
2930
if (compiledTemplate) {
3031
options.render = compiledTemplate.render
3132
options.staticRenderFns = compiledTemplate.staticRenderFns
33+
options.compiled = true
34+
}
35+
36+
// functional template
37+
if (functionalTemplate) {
38+
options.functional = true
3239
}
3340

3441
// scopedId
@@ -69,6 +76,7 @@ module.exports = function normalizeComponent (
6976
var existing = functional
7077
? options.render
7178
: options.beforeCreate
79+
7280
if (!functional) {
7381
// inject component registration as beforeCreate hook
7482
options.beforeCreate = existing

lib/loader.js

+10-10
Original file line numberDiff line numberDiff line change
@@ -90,14 +90,18 @@ module.exports = function (content) {
9090
var parts = parse(content, fileName, this.sourceMap)
9191
var hasScoped = parts.styles.some(function (s) { return s.scoped })
9292
var hasComment = parts.template && parts.template.attrs && parts.template.attrs.comments
93+
var functional = parts.template && parts.template.functional
94+
var bubleTemplateOptions = Object.create(options.buble || {})
95+
bubleTemplateOptions.transforms = Object.create(bubleTemplateOptions.transforms || {})
96+
bubleTemplateOptions.transforms.stripWithFunctional = functional
9397

9498
var templateCompilerOptions = '?' + JSON.stringify({
9599
id: moduleId,
96100
hasScoped: hasScoped,
97101
hasComment: hasComment,
98102
transformToRequire: options.transformToRequire,
99103
preserveWhitespace: options.preserveWhitespace,
100-
buble: options.buble,
104+
buble: bubleTemplateOptions,
101105
// only pass compilerModules if it's a path string
102106
compilerModules: typeof options.compilerModules === 'string'
103107
? options.compilerModules
@@ -261,6 +265,10 @@ module.exports = function (content) {
261265
output += 'var __vue_template__ = null\n'
262266
}
263267

268+
// template functional
269+
output += '/* template functional */\n '
270+
output += 'var __vue_template_functional__ = ' + ((template && template.functional) ? 'true' : 'null') + '\n'
271+
264272
// style
265273
output += '/* styles */\n'
266274
output += 'var __vue_styles__ = ' + (parts.styles.length ? 'injectStyle' : 'null') + '\n'
@@ -277,6 +285,7 @@ module.exports = function (content) {
277285
output += 'var Component = normalizeComponent(\n' +
278286
' __vue_script__,\n' +
279287
' __vue_template__,\n' +
288+
' __vue_template_functional__,\n' +
280289
' __vue_styles__,\n' +
281290
' __vue_scopeId__,\n' +
282291
' __vue_module_identifier__\n' +
@@ -293,15 +302,6 @@ module.exports = function (content) {
293302
'})) {' +
294303
'console.error("named exports are not supported in *.vue files.")' +
295304
'}\n'
296-
// check functional components used with templates
297-
if (template) {
298-
output +=
299-
'if (Component.options.functional) {' +
300-
'console.error("' +
301-
'[vue-loader] ' + fileName + ': functional components are not ' +
302-
'supported with templates, they should use render functions.' +
303-
'")}\n'
304-
}
305305
}
306306

307307
// add requires for customBlocks

lib/template-compiler/index.js

+10-9
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,14 @@ module.exports = function (html) {
5252
: 'module.exports={render:function(){},staticRenderFns:[]}'
5353
} else {
5454
var bubleOptions = options.buble
55+
var stripWithFunctional = bubleOptions.transforms.stripWithFunctional
56+
var stripWith = bubleOptions.transforms.stripWith !== false
57+
58+
var staticRenderFns = compiled.staticRenderFns.map((fn) => toFunction(fn, stripWithFunctional))
59+
5560
code = transpile(
56-
'var render = ' + toFunction(compiled.render) + '\n' +
57-
'var staticRenderFns = [' + compiled.staticRenderFns.map(toFunction).join(',') + ']',
61+
'var render = ' + toFunction(compiled.render, stripWithFunctional) + '\n' +
62+
'var staticRenderFns = [' + staticRenderFns.join(',') + ']',
5863
bubleOptions
5964
) + '\n'
6065

@@ -64,11 +69,7 @@ module.exports = function (html) {
6469
}
6570

6671
// mark with stripped (this enables Vue to use correct runtime proxy detection)
67-
if (!isProduction && (
68-
!bubleOptions ||
69-
!bubleOptions.transforms ||
70-
bubleOptions.transforms.stripWith !== false
71-
)) {
72+
if (!isProduction && stripWith) {
7273
code += `render._withStripped = true\n`
7374
}
7475
var exports = `{ render: render, staticRenderFns: staticRenderFns }`
@@ -91,8 +92,8 @@ module.exports = function (html) {
9192
return code
9293
}
9394

94-
function toFunction (code) {
95-
return 'function () {' + code + '}'
95+
function toFunction (code, stripWithFunctional) {
96+
return 'function (' + (stripWithFunctional ? '_h,_vm' : '') + ') {' + code + '}'
9697
}
9798

9899
function pad (html) {

test/fixtures/functional-root.vue

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<template>
2+
<div>
3+
<functional>
4+
<span>hello</span>
5+
<div slot="slot2">Second slot</div>
6+
<template slot="scoped" scope="scope">{{ scope.msg }}</template>
7+
</functional>
8+
</div>
9+
</template>
10+
11+
<script>
12+
import Functional from './functional.vue'
13+
export default {
14+
components: {
15+
Functional
16+
}
17+
}
18+
</script>

test/fixtures/functional.vue

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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 class="clickable" @click="parent.fn"></div>
10+
</div>
11+
</template>
12+
13+
<script>
14+
export default {
15+
props: {
16+
msg: {
17+
type: String,
18+
default: 'hello'
19+
}
20+
}
21+
}
22+
</script>

test/test.js

+65-49
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ var MemoryFS = require('memory-fs')
88
var expect = require('chai').expect
99
var hash = require('hash-sum')
1010
var SSR = require('vue-server-renderer')
11+
var Vue = require('vue/dist/vue.runtime.js')
1112
// var compiler = require('../lib/template-compiler')
1213
var normalizeNewline = require('normalize-newline')
1314
var ExtractTextPlugin = require('extract-text-webpack-plugin')
@@ -86,38 +87,10 @@ function test (options, assert) {
8687
})
8788
}
8889

89-
function mockRender (options, data) {
90-
function h (tag, data, children) {
91-
if (Array.isArray(data)) {
92-
children = data
93-
data = null
94-
}
95-
return {
96-
tag: tag,
97-
data: data,
98-
children: children
99-
}
100-
}
101-
function e (text = '') {
102-
return {
103-
text: text,
104-
isComment: true
105-
}
106-
}
107-
return options.render.call(Object.assign({
108-
_v (val) {
109-
return val
110-
},
111-
_self: {},
112-
_e: e,
113-
$createElement: h,
114-
_m (index) {
115-
return options.staticRenderFns[index].call(this)
116-
},
117-
_s (str) {
118-
return String(str)
119-
}
120-
}, data), h)
90+
function mockRender (options, data = {}) {
91+
const vm = new Vue(Object.assign({}, options, { data () { return data } }))
92+
vm.$mount()
93+
return vm._vnode
12194
}
12295

12396
function interopDefault (module) {
@@ -134,10 +107,11 @@ describe('vue-loader', function () {
134107
var vnode = mockRender(module, {
135108
msg: 'hi'
136109
})
110+
137111
// <h2 class="red">{{msg}}</h2>
138112
expect(vnode.tag).to.equal('h2')
139113
expect(vnode.data.staticClass).to.equal('red')
140-
expect(vnode.children[0]).to.equal('hi')
114+
expect(vnode.children[0].text).to.equal('hi')
141115

142116
expect(module.data().msg).to.contain('Hello from Component A!')
143117
var style = window.document.querySelector('style').textContent
@@ -222,11 +196,11 @@ describe('vue-loader', function () {
222196
// <svg><template><p></p></template></svg>
223197
// </div>
224198
expect(vnode.children[0].tag).to.equal('div')
225-
expect(vnode.children[1]).to.equal(' ')
199+
expect(vnode.children[1].text).to.equal(' ')
226200
expect(vnode.children[2].tag).to.equal('p')
227201
expect(vnode.children[2].data.staticClass).to.equal('abc def')
228-
expect(vnode.children[4][0].tag).to.equal('p')
229-
expect(vnode.children[4][0].data.staticClass).to.equal('test')
202+
expect(vnode.children[4].tag).to.equal('p')
203+
expect(vnode.children[4].data.staticClass).to.equal('test')
230204

231205
var style = window.document.querySelector('style').textContent
232206
style = normalizeNewline(style)
@@ -264,7 +238,7 @@ describe('vue-loader', function () {
264238
var vnode = mockRender(module)
265239
// '<div><h1>hello</h1></div>'
266240
expect(vnode.children[0].tag).to.equal('h1')
267-
expect(vnode.children[0].children[0]).to.equal('hello')
241+
expect(vnode.children[0].children[0].text).to.equal('hello')
268242
done()
269243
})
270244
})
@@ -401,7 +375,7 @@ describe('vue-loader', function () {
401375
// <div class="msg">{{ msg }}</div>
402376
expect(vnode.tag).to.equal('div')
403377
expect(vnode.data.staticClass).to.equal('msg')
404-
expect(vnode.children[0]).to.equal('Hello from mocked service!')
378+
expect(vnode.children[0].text).to.equal('Hello from mocked service!')
405379
done()
406380
})
407381
})
@@ -522,7 +496,7 @@ describe('vue-loader', function () {
522496
msg: 'success'
523497
})
524498
expect(vnode.tag).to.equal('div')
525-
expect(vnode.children[0]).to.equal('success')
499+
expect(vnode.children[0].text).to.equal('success')
526500
expect(new Module().msg === 'success')
527501
done()
528502
})
@@ -541,7 +515,7 @@ describe('vue-loader', function () {
541515
})
542516
expect(vnode.tag).to.equal('h2')
543517
expect(vnode.data.staticClass).to.equal('red')
544-
expect(vnode.children[0]).to.equal('hi')
518+
expect(vnode.children[0].text).to.equal('hi')
545519

546520
expect(rawModule.default.data().msg).to.contain('Hello from Component A!')
547521
done()
@@ -635,7 +609,7 @@ describe('vue-loader', function () {
635609
// <h2 id="-msg-">{{msg}}</h2>
636610
expect(vnode.tag).to.equal('h2')
637611
expect(vnode.data.attrs.id).to.equal('-msg-')
638-
expect(vnode.children[0]).to.equal('hi')
612+
expect(vnode.children[0].text).to.equal('hi')
639613
done()
640614
})
641615
})
@@ -816,7 +790,7 @@ describe('vue-loader', function () {
816790
// <h2 class="green">{{msg}}</h2>
817791
expect(vnode.tag).to.equal('h2')
818792
expect(vnode.data.staticClass).to.equal('green')
819-
expect(vnode.children[0]).to.equal('hi')
793+
expect(vnode.children[0].text).to.equal('hi')
820794

821795
expect(module.data().msg).to.contain('Changed!')
822796
var style = window.document.querySelector('style').textContent
@@ -846,7 +820,7 @@ describe('vue-loader', function () {
846820
msg: JSON.parse(module.__i18n).en.hello,
847821
blog: module.__blog
848822
})
849-
expect(vnode.children[0].children[0]).to.equal('hello world')
823+
expect(vnode.children[0].children[0].text).to.equal('hello world')
850824
expect(vnode.children[2].data.domProps.innerHTML).to.equal('<h2 id="foo">foo</h2>')
851825
done()
852826
})
@@ -872,10 +846,10 @@ describe('vue-loader', function () {
872846
}, (window, module) => {
873847
var results = []
874848
// var vnode =
875-
mockRender(module, {
876-
$processStyle: style => results.push(style),
877-
transform: 'translateX(10px)'
878-
})
849+
mockRender(
850+
Object.assign(module, { methods: { $processStyle: style => results.push(style) }}),
851+
{ transform: 'translateX(10px)' }
852+
)
879853
expect(results).to.deep.equal([
880854
{ 'flex-direction': 'row' },
881855
{ 'transform': 'translateX(10px)' }
@@ -931,7 +905,7 @@ describe('vue-loader', function () {
931905
// <div class="foo">hi</div>
932906
expect(vnode.tag).to.equal('div')
933907
expect(vnode.data.class).to.equal('foo')
934-
expect(vnode.children[0]).to.equal('functional')
908+
expect(vnode.children[0].text).to.equal('functional')
935909

936910
var style = window.document.querySelector('style').textContent
937911
style = normalizeNewline(style)
@@ -951,10 +925,52 @@ describe('vue-loader', function () {
951925
expect(vnode.tag).to.equal('div')
952926
expect(vnode.children.length).to.equal(2)
953927
expect(vnode.children[0].data.staticClass).to.equal('red')
954-
expect(vnode.children[0].children[0]).to.equal('hi')
928+
expect(vnode.children[0].children[0].text).to.equal('hi')
955929
expect(vnode.children[1].isComment).to.true
956930
expect(vnode.children[1].text).to.equal(' comment here ')
957931
done()
958932
})
959933
})
934+
935+
// Vue required tests for more complete test cases
936+
it('should allow functional template', done => {
937+
test({
938+
entry: './test/fixtures/functional-root.vue',
939+
vue: {
940+
preserveWhitespace: false
941+
}
942+
}, (window, module) => {
943+
expect(module.components.Functional.compiled).to.equal(true)
944+
expect(module.components.Functional.functional).to.equal(true)
945+
expect(module.components.Functional.staticRenderFns).to.exist
946+
expect(module.components.Functional.render).to.be.a('function')
947+
948+
const vnode = mockRender(module, {
949+
fn () {
950+
done()
951+
}
952+
}).children[0]
953+
954+
// Basic vnode
955+
expect(vnode.children[0].data.staticClass).to.equal('red')
956+
expect(vnode.children[0].children[0].text).to.equal('hello')
957+
// Default slot vnode
958+
expect(vnode.children[1].tag).to.equal('span')
959+
expect(vnode.children[1].children[0].text).to.equal('hello')
960+
// Named slot vnode
961+
expect(vnode.children[2].tag).to.equal('div')
962+
expect(vnode.children[2].children[0].text).to.equal('Second slot')
963+
// // Scoped slot vnode
964+
expect(vnode.children[3].text).to.equal('hello')
965+
// // Static content vnode
966+
expect(vnode.children[4].tag).to.equal('div')
967+
expect(vnode.children[4].children[0].text).to.equal('Some ')
968+
expect(vnode.children[4].children[1].tag).to.equal('span')
969+
expect(vnode.children[4].children[1].children[0].text).to.equal('text')
970+
// // v-if vnode
971+
expect(vnode.children[5].text).to.equal('')
972+
973+
vnode.children[6].data.on.click()
974+
})
975+
})
960976
})

0 commit comments

Comments
 (0)