Skip to content

Commit 7988a55

Browse files
committed
feat: support scoped-slot usage with $slot
1 parent 9132730 commit 7988a55

File tree

5 files changed

+146
-10
lines changed

5 files changed

+146
-10
lines changed

Diff for: flow/compiler.js

+8-1
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ declare type ASTDirective = {
8585
end?: number;
8686
};
8787

88-
declare type ASTNode = ASTElement | ASTText | ASTExpression;
88+
declare type ASTNode = ASTElement | ASTText | ASTExpression
8989

9090
declare type ASTElement = {
9191
type: 1;
@@ -167,6 +167,9 @@ declare type ASTElement = {
167167

168168
// weex specific
169169
appendAsTree?: boolean;
170+
171+
// 2.6 $slot check
172+
has$Slot?: boolean
170173
};
171174

172175
declare type ASTExpression = {
@@ -179,6 +182,8 @@ declare type ASTExpression = {
179182
ssrOptimizability?: number;
180183
start?: number;
181184
end?: number;
185+
// 2.6 $slot check
186+
has$Slot?: boolean
182187
};
183188

184189
declare type ASTText = {
@@ -190,6 +195,8 @@ declare type ASTText = {
190195
ssrOptimizability?: number;
191196
start?: number;
192197
end?: number;
198+
// 2.6 $slot check
199+
has$Slot?: boolean
193200
};
194201

195202
// SFC-parser related declarations

Diff for: src/compiler/codegen/index.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export class CodegenState {
2727
this.dataGenFns = pluckModuleFunction(options.modules, 'genData')
2828
this.directives = extend(extend({}, baseDirectives), options.directives)
2929
const isReservedTag = options.isReservedTag || no
30-
this.maybeComponent = (el: ASTElement) => el.component || !isReservedTag(el.tag)
30+
this.maybeComponent = (el: ASTElement) => !!el.component || !isReservedTag(el.tag)
3131
this.onceId = 0
3232
this.staticRenderFns = []
3333
this.pre = false

Diff for: src/compiler/optimizer.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export function optimize (root: ?ASTElement, options: CompilerOptions) {
3030

3131
function genStaticKeys (keys: string): Function {
3232
return makeMap(
33-
'type,tag,attrsList,attrsMap,plain,parent,children,attrs,start,end,rawAttrsMap' +
33+
'type,tag,attrsList,attrsMap,plain,parent,children,attrs,start,end,rawAttrsMap,has$Slot' +
3434
(keys ? ',' + keys : '')
3535
)
3636
}
@@ -43,6 +43,7 @@ function markStatic (node: ASTNode) {
4343
// 2. static slot content fails for hot-reloading
4444
if (
4545
!isPlatformReservedTag(node.tag) &&
46+
!node.component &&
4647
node.tag !== 'slot' &&
4748
node.attrsMap['inline-template'] == null
4849
) {

Diff for: src/compiler/parser/index.js

+76-7
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { parseHTML } from './html-parser'
55
import { parseText } from './text-parser'
66
import { parseFilters } from './filter-parser'
77
import { genAssignmentCode } from '../directives/model'
8-
import { extend, cached, no, camelize, hyphenate } from 'shared/util'
8+
import { extend, cached, no, camelize, hyphenate, hasOwn } from 'shared/util'
99
import { isIE, isEdge, isServerRendering } from 'core/util/env'
1010

1111
import {
@@ -44,6 +44,7 @@ let postTransforms
4444
let platformIsPreTag
4545
let platformMustUseProp
4646
let platformGetTagNamespace
47+
let maybeComponent
4748

4849
export function createASTElement (
4950
tag: string,
@@ -73,6 +74,8 @@ export function parse (
7374
platformIsPreTag = options.isPreTag || no
7475
platformMustUseProp = options.mustUseProp || no
7576
platformGetTagNamespace = options.getTagNamespace || no
77+
const isReservedTag = options.isReservedTag || no
78+
maybeComponent = (el: ASTElement) => !!el.component || !isReservedTag(el.tag)
7679

7780
transforms = pluckModuleFunction(options.modules, 'transformNode')
7881
preTransforms = pluckModuleFunction(options.modules, 'preTransformNode')
@@ -98,7 +101,7 @@ export function parse (
98101

99102
function closeElement (element) {
100103
if (!inVPre && !element.processed) {
101-
element = processElement(element, options, currentParent)
104+
element = processElement(element, options)
102105
}
103106
// tree management
104107
if (!stack.length && element !== root) {
@@ -152,7 +155,7 @@ export function parse (
152155
{ start: el.start }
153156
)
154157
}
155-
if (el.attrsMap.hasOwnProperty('v-for')) {
158+
if (hasOwn(el.attrsMap, 'v-for')) {
156159
warnOnce(
157160
'Cannot use v-for on stateful component root element because ' +
158161
'it renders multiple elements.',
@@ -376,8 +379,7 @@ function processRawAttrs (el) {
376379

377380
export function processElement (
378381
element: ASTElement,
379-
options: CompilerOptions,
380-
parent: ASTElement | undefined
382+
options: CompilerOptions
381383
) {
382384
processKey(element)
383385

@@ -390,7 +392,7 @@ export function processElement (
390392
)
391393

392394
processRef(element)
393-
processSlot(element, parent)
395+
processSlot(element)
394396
processComponent(element)
395397
for (let i = 0; i < transforms.length; i++) {
396398
element = transforms[i](element, options) || element
@@ -581,19 +583,86 @@ function processSlot (el) {
581583
)
582584
}
583585
el.slotScope = slotScope
586+
if (process.env.NODE_ENV !== 'production' && nodeHas$Slot(el)) {
587+
warn('Unepxected mixed usage of `slot-scope` and `$slot`.', el)
588+
}
589+
} else {
590+
// 2.6 $slot support
591+
// Context: https://github.com/vuejs/vue/issues/9180
592+
// Ideally, all slots should be compiled as functions (this is what we
593+
// are doing in 3.x), but for 2.x e want to preserve complete backwards
594+
// compatibility, and maintain the exact same compilation output for any
595+
// code that does not use the new syntax.
596+
597+
// recursively check component children for presence of `$slot` in all
598+
// expressions until running into a nested child component.
599+
if (maybeComponent(el) && childrenHas$Slot(el)) {
600+
processScopedSlots(el)
601+
}
584602
}
585603
const slotTarget = getBindingAttr(el, 'slot')
586604
if (slotTarget) {
587605
el.slotTarget = slotTarget === '""' ? '"default"' : slotTarget
588606
// preserve slot as an attribute for native shadow DOM compat
589607
// only for non-scoped slots.
590-
if (el.tag !== 'template' && !el.slotScope) {
608+
if (el.tag !== 'template' && !el.slotScope && !nodeHas$Slot(el)) {
591609
addAttr(el, 'slot', slotTarget, getRawBindingAttr(el, 'slot'))
592610
}
593611
}
594612
}
595613
}
596614

615+
function childrenHas$Slot (el): boolean {
616+
return el.children ? el.children.some(nodeHas$Slot) : false
617+
}
618+
619+
const $slotRE = /\$slot/
620+
function nodeHas$Slot (node): boolean {
621+
// caching
622+
if (hasOwn(node, 'has$Slot')) {
623+
return (node.has$Slot: any)
624+
}
625+
if (node.type === 1) { // element
626+
for (const key in node.attrsMap) {
627+
if (dirRE.test(key) && $slotRE.test(node.attrsMap[key])) {
628+
return (node.has$Slot = true)
629+
}
630+
}
631+
return (node.has$Slot = childrenHas$Slot(node))
632+
} else if (node.type === 2) { // expression
633+
// TODO more robust logic for checking $slot usage
634+
return (node.has$Slot = $slotRE.test(node.expression))
635+
}
636+
return false
637+
}
638+
639+
function processScopedSlots (el) {
640+
// 1. group children by slot target
641+
const groups: any = {}
642+
for (let i = 0; i < el.children.length; i++) {
643+
const child = el.children[i]
644+
const target = child.slotTarget || '"default"'
645+
if (!groups[target]) {
646+
groups[target] = []
647+
}
648+
groups[target].push(child)
649+
}
650+
// 2. for each slot group, check if the group contains $slot
651+
for (const name in groups) {
652+
const group = groups[name]
653+
if (group.some(nodeHas$Slot)) {
654+
// 3. if a group contains $slot, all nodes in that group gets assigned
655+
// as a scoped slot to el and removed from children
656+
el.plain = false
657+
const slots = el.scopedSlots || (el.scopedSlots = {})
658+
const slotContainer = slots[name] = createASTElement('template', [], el)
659+
slotContainer.children = group
660+
slotContainer.slotScope = '$slot'
661+
el.children = el.children.filter(c => group.indexOf(c) === -1)
662+
}
663+
}
664+
}
665+
597666
function processComponent (el) {
598667
let binding
599668
if ((binding = getBindingAttr(el, 'is'))) {

Diff for: test/unit/features/component/component-scoped-slot.spec.js

+59
Original file line numberDiff line numberDiff line change
@@ -613,4 +613,63 @@ describe('Component scoped slot', () => {
613613
expect(vm.$el.innerHTML).toBe('<p>hello</p>')
614614
}).then(done)
615615
})
616+
617+
// 2.6 $slot usage
618+
describe('$slot support', () => {
619+
it('should work', () => {
620+
const vm = new Vue({
621+
template: `<foo><div>{{$slot.foo}}</div></foo>`,
622+
components: { foo: { template: `<div><slot foo="hello"/></div>` }}
623+
}).$mount()
624+
expect(vm.$el.innerHTML).toBe(`<div>hello</div>`)
625+
})
626+
627+
it('should work for use of $slots in attributes', () => {
628+
const vm = new Vue({
629+
template: `<foo><div :id="$slot.foo"></div></foo>`,
630+
components: { foo: { template: `<div><slot foo="hello"/></div>` }}
631+
}).$mount()
632+
expect(vm.$el.innerHTML).toBe(`<div id="hello"></div>`)
633+
})
634+
635+
it('should work for root text nodes', () => {
636+
const vm = new Vue({
637+
template: `<foo>{{$slot.foo}}</foo>`,
638+
components: { foo: { template: `<div><slot foo="hello"/></div>` }}
639+
}).$mount()
640+
expect(vm.$el.innerHTML).toBe(`hello`)
641+
})
642+
643+
it('should work for mix of root text nodes and elements', () => {
644+
const vm = new Vue({
645+
template: `<foo>hi <div>{{ $slot.foo }}</div>{{$slot.foo}}</foo>`,
646+
components: { foo: { template: `<div><slot foo="hello"/></div>` }}
647+
}).$mount()
648+
expect(vm.$el.innerHTML).toBe(`hi <div>hello</div>hello`)
649+
})
650+
651+
it('should work for named slots', () => {
652+
const vm = new Vue({
653+
template: `<foo><div slot="foo">{{ $slot.foo }}</div></foo>`,
654+
components: { foo: { template: `<div><slot name="foo" foo="hello"/></div>` }}
655+
}).$mount()
656+
expect(vm.$el.innerHTML).toBe(`<div>hello</div>`)
657+
})
658+
659+
it('should work for mixed default and named slots', () => {
660+
const vm = new Vue({
661+
template: `<foo>{{ $slot.foo }}<div>{{ $slot.foo }}</div><div slot="foo">{{ $slot.foo }}</div></foo>`,
662+
components: { foo: { template: `<div><slot foo="default"/><slot name="foo" foo="foo"/></div>` }}
663+
}).$mount()
664+
expect(vm.$el.innerHTML).toBe(`default<div>default</div><div>foo</div>`)
665+
})
666+
667+
it('should work for mixed $slot and non-$slot slots', () => {
668+
const vm = new Vue({
669+
template: `<foo>{{ $slot.foo }}<div slot="foo">static</div><div>{{ $slot.foo }}</div></foo>`,
670+
components: { foo: { template: `<div><slot foo="default"/><slot name="foo"/></div>` }}
671+
}).$mount()
672+
expect(vm.$el.innerHTML).toBe(`default<div>default</div><div>static</div>`)
673+
})
674+
})
616675
})

0 commit comments

Comments
 (0)