mirror of
https://github.com/vuejs/vue.git
synced 2024-11-22 04:39:46 +00:00
feat: support scoped-slot usage with $slot
This commit is contained in:
parent
9132730035
commit
7988a5541c
@ -85,7 +85,7 @@ declare type ASTDirective = {
|
||||
end?: number;
|
||||
};
|
||||
|
||||
declare type ASTNode = ASTElement | ASTText | ASTExpression;
|
||||
declare type ASTNode = ASTElement | ASTText | ASTExpression
|
||||
|
||||
declare type ASTElement = {
|
||||
type: 1;
|
||||
@ -167,6 +167,9 @@ declare type ASTElement = {
|
||||
|
||||
// weex specific
|
||||
appendAsTree?: boolean;
|
||||
|
||||
// 2.6 $slot check
|
||||
has$Slot?: boolean
|
||||
};
|
||||
|
||||
declare type ASTExpression = {
|
||||
@ -179,6 +182,8 @@ declare type ASTExpression = {
|
||||
ssrOptimizability?: number;
|
||||
start?: number;
|
||||
end?: number;
|
||||
// 2.6 $slot check
|
||||
has$Slot?: boolean
|
||||
};
|
||||
|
||||
declare type ASTText = {
|
||||
@ -190,6 +195,8 @@ declare type ASTText = {
|
||||
ssrOptimizability?: number;
|
||||
start?: number;
|
||||
end?: number;
|
||||
// 2.6 $slot check
|
||||
has$Slot?: boolean
|
||||
};
|
||||
|
||||
// SFC-parser related declarations
|
||||
|
@ -27,7 +27,7 @@ export class CodegenState {
|
||||
this.dataGenFns = pluckModuleFunction(options.modules, 'genData')
|
||||
this.directives = extend(extend({}, baseDirectives), options.directives)
|
||||
const isReservedTag = options.isReservedTag || no
|
||||
this.maybeComponent = (el: ASTElement) => el.component || !isReservedTag(el.tag)
|
||||
this.maybeComponent = (el: ASTElement) => !!el.component || !isReservedTag(el.tag)
|
||||
this.onceId = 0
|
||||
this.staticRenderFns = []
|
||||
this.pre = false
|
||||
|
@ -30,7 +30,7 @@ export function optimize (root: ?ASTElement, options: CompilerOptions) {
|
||||
|
||||
function genStaticKeys (keys: string): Function {
|
||||
return makeMap(
|
||||
'type,tag,attrsList,attrsMap,plain,parent,children,attrs,start,end,rawAttrsMap' +
|
||||
'type,tag,attrsList,attrsMap,plain,parent,children,attrs,start,end,rawAttrsMap,has$Slot' +
|
||||
(keys ? ',' + keys : '')
|
||||
)
|
||||
}
|
||||
@ -43,6 +43,7 @@ function markStatic (node: ASTNode) {
|
||||
// 2. static slot content fails for hot-reloading
|
||||
if (
|
||||
!isPlatformReservedTag(node.tag) &&
|
||||
!node.component &&
|
||||
node.tag !== 'slot' &&
|
||||
node.attrsMap['inline-template'] == null
|
||||
) {
|
||||
|
@ -5,7 +5,7 @@ import { parseHTML } from './html-parser'
|
||||
import { parseText } from './text-parser'
|
||||
import { parseFilters } from './filter-parser'
|
||||
import { genAssignmentCode } from '../directives/model'
|
||||
import { extend, cached, no, camelize, hyphenate } from 'shared/util'
|
||||
import { extend, cached, no, camelize, hyphenate, hasOwn } from 'shared/util'
|
||||
import { isIE, isEdge, isServerRendering } from 'core/util/env'
|
||||
|
||||
import {
|
||||
@ -44,6 +44,7 @@ let postTransforms
|
||||
let platformIsPreTag
|
||||
let platformMustUseProp
|
||||
let platformGetTagNamespace
|
||||
let maybeComponent
|
||||
|
||||
export function createASTElement (
|
||||
tag: string,
|
||||
@ -73,6 +74,8 @@ export function parse (
|
||||
platformIsPreTag = options.isPreTag || no
|
||||
platformMustUseProp = options.mustUseProp || no
|
||||
platformGetTagNamespace = options.getTagNamespace || no
|
||||
const isReservedTag = options.isReservedTag || no
|
||||
maybeComponent = (el: ASTElement) => !!el.component || !isReservedTag(el.tag)
|
||||
|
||||
transforms = pluckModuleFunction(options.modules, 'transformNode')
|
||||
preTransforms = pluckModuleFunction(options.modules, 'preTransformNode')
|
||||
@ -98,7 +101,7 @@ export function parse (
|
||||
|
||||
function closeElement (element) {
|
||||
if (!inVPre && !element.processed) {
|
||||
element = processElement(element, options, currentParent)
|
||||
element = processElement(element, options)
|
||||
}
|
||||
// tree management
|
||||
if (!stack.length && element !== root) {
|
||||
@ -152,7 +155,7 @@ export function parse (
|
||||
{ start: el.start }
|
||||
)
|
||||
}
|
||||
if (el.attrsMap.hasOwnProperty('v-for')) {
|
||||
if (hasOwn(el.attrsMap, 'v-for')) {
|
||||
warnOnce(
|
||||
'Cannot use v-for on stateful component root element because ' +
|
||||
'it renders multiple elements.',
|
||||
@ -376,8 +379,7 @@ function processRawAttrs (el) {
|
||||
|
||||
export function processElement (
|
||||
element: ASTElement,
|
||||
options: CompilerOptions,
|
||||
parent: ASTElement | undefined
|
||||
options: CompilerOptions
|
||||
) {
|
||||
processKey(element)
|
||||
|
||||
@ -390,7 +392,7 @@ export function processElement (
|
||||
)
|
||||
|
||||
processRef(element)
|
||||
processSlot(element, parent)
|
||||
processSlot(element)
|
||||
processComponent(element)
|
||||
for (let i = 0; i < transforms.length; i++) {
|
||||
element = transforms[i](element, options) || element
|
||||
@ -581,19 +583,86 @@ function processSlot (el) {
|
||||
)
|
||||
}
|
||||
el.slotScope = slotScope
|
||||
if (process.env.NODE_ENV !== 'production' && nodeHas$Slot(el)) {
|
||||
warn('Unepxected mixed usage of `slot-scope` and `$slot`.', el)
|
||||
}
|
||||
} else {
|
||||
// 2.6 $slot support
|
||||
// Context: https://github.com/vuejs/vue/issues/9180
|
||||
// Ideally, all slots should be compiled as functions (this is what we
|
||||
// are doing in 3.x), but for 2.x e want to preserve complete backwards
|
||||
// compatibility, and maintain the exact same compilation output for any
|
||||
// code that does not use the new syntax.
|
||||
|
||||
// recursively check component children for presence of `$slot` in all
|
||||
// expressions until running into a nested child component.
|
||||
if (maybeComponent(el) && childrenHas$Slot(el)) {
|
||||
processScopedSlots(el)
|
||||
}
|
||||
}
|
||||
const slotTarget = getBindingAttr(el, 'slot')
|
||||
if (slotTarget) {
|
||||
el.slotTarget = slotTarget === '""' ? '"default"' : slotTarget
|
||||
// preserve slot as an attribute for native shadow DOM compat
|
||||
// only for non-scoped slots.
|
||||
if (el.tag !== 'template' && !el.slotScope) {
|
||||
if (el.tag !== 'template' && !el.slotScope && !nodeHas$Slot(el)) {
|
||||
addAttr(el, 'slot', slotTarget, getRawBindingAttr(el, 'slot'))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function childrenHas$Slot (el): boolean {
|
||||
return el.children ? el.children.some(nodeHas$Slot) : false
|
||||
}
|
||||
|
||||
const $slotRE = /\$slot/
|
||||
function nodeHas$Slot (node): boolean {
|
||||
// caching
|
||||
if (hasOwn(node, 'has$Slot')) {
|
||||
return (node.has$Slot: any)
|
||||
}
|
||||
if (node.type === 1) { // element
|
||||
for (const key in node.attrsMap) {
|
||||
if (dirRE.test(key) && $slotRE.test(node.attrsMap[key])) {
|
||||
return (node.has$Slot = true)
|
||||
}
|
||||
}
|
||||
return (node.has$Slot = childrenHas$Slot(node))
|
||||
} else if (node.type === 2) { // expression
|
||||
// TODO more robust logic for checking $slot usage
|
||||
return (node.has$Slot = $slotRE.test(node.expression))
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function processScopedSlots (el) {
|
||||
// 1. group children by slot target
|
||||
const groups: any = {}
|
||||
for (let i = 0; i < el.children.length; i++) {
|
||||
const child = el.children[i]
|
||||
const target = child.slotTarget || '"default"'
|
||||
if (!groups[target]) {
|
||||
groups[target] = []
|
||||
}
|
||||
groups[target].push(child)
|
||||
}
|
||||
// 2. for each slot group, check if the group contains $slot
|
||||
for (const name in groups) {
|
||||
const group = groups[name]
|
||||
if (group.some(nodeHas$Slot)) {
|
||||
// 3. if a group contains $slot, all nodes in that group gets assigned
|
||||
// as a scoped slot to el and removed from children
|
||||
el.plain = false
|
||||
const slots = el.scopedSlots || (el.scopedSlots = {})
|
||||
const slotContainer = slots[name] = createASTElement('template', [], el)
|
||||
slotContainer.children = group
|
||||
slotContainer.slotScope = '$slot'
|
||||
el.children = el.children.filter(c => group.indexOf(c) === -1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function processComponent (el) {
|
||||
let binding
|
||||
if ((binding = getBindingAttr(el, 'is'))) {
|
||||
|
@ -613,4 +613,63 @@ describe('Component scoped slot', () => {
|
||||
expect(vm.$el.innerHTML).toBe('<p>hello</p>')
|
||||
}).then(done)
|
||||
})
|
||||
|
||||
// 2.6 $slot usage
|
||||
describe('$slot support', () => {
|
||||
it('should work', () => {
|
||||
const vm = new Vue({
|
||||
template: `<foo><div>{{$slot.foo}}</div></foo>`,
|
||||
components: { foo: { template: `<div><slot foo="hello"/></div>` }}
|
||||
}).$mount()
|
||||
expect(vm.$el.innerHTML).toBe(`<div>hello</div>`)
|
||||
})
|
||||
|
||||
it('should work for use of $slots in attributes', () => {
|
||||
const vm = new Vue({
|
||||
template: `<foo><div :id="$slot.foo"></div></foo>`,
|
||||
components: { foo: { template: `<div><slot foo="hello"/></div>` }}
|
||||
}).$mount()
|
||||
expect(vm.$el.innerHTML).toBe(`<div id="hello"></div>`)
|
||||
})
|
||||
|
||||
it('should work for root text nodes', () => {
|
||||
const vm = new Vue({
|
||||
template: `<foo>{{$slot.foo}}</foo>`,
|
||||
components: { foo: { template: `<div><slot foo="hello"/></div>` }}
|
||||
}).$mount()
|
||||
expect(vm.$el.innerHTML).toBe(`hello`)
|
||||
})
|
||||
|
||||
it('should work for mix of root text nodes and elements', () => {
|
||||
const vm = new Vue({
|
||||
template: `<foo>hi <div>{{ $slot.foo }}</div>{{$slot.foo}}</foo>`,
|
||||
components: { foo: { template: `<div><slot foo="hello"/></div>` }}
|
||||
}).$mount()
|
||||
expect(vm.$el.innerHTML).toBe(`hi <div>hello</div>hello`)
|
||||
})
|
||||
|
||||
it('should work for named slots', () => {
|
||||
const vm = new Vue({
|
||||
template: `<foo><div slot="foo">{{ $slot.foo }}</div></foo>`,
|
||||
components: { foo: { template: `<div><slot name="foo" foo="hello"/></div>` }}
|
||||
}).$mount()
|
||||
expect(vm.$el.innerHTML).toBe(`<div>hello</div>`)
|
||||
})
|
||||
|
||||
it('should work for mixed default and named slots', () => {
|
||||
const vm = new Vue({
|
||||
template: `<foo>{{ $slot.foo }}<div>{{ $slot.foo }}</div><div slot="foo">{{ $slot.foo }}</div></foo>`,
|
||||
components: { foo: { template: `<div><slot foo="default"/><slot name="foo" foo="foo"/></div>` }}
|
||||
}).$mount()
|
||||
expect(vm.$el.innerHTML).toBe(`default<div>default</div><div>foo</div>`)
|
||||
})
|
||||
|
||||
it('should work for mixed $slot and non-$slot slots', () => {
|
||||
const vm = new Vue({
|
||||
template: `<foo>{{ $slot.foo }}<div slot="foo">static</div><div>{{ $slot.foo }}</div></foo>`,
|
||||
components: { foo: { template: `<div><slot foo="default"/><slot name="foo"/></div>` }}
|
||||
}).$mount()
|
||||
expect(vm.$el.innerHTML).toBe(`default<div>default</div><div>static</div>`)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user