From 7988a5541cd6e9a9be28b4905d69c44ddcb3878e Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 7 Jan 2019 22:26:44 -0500 Subject: [PATCH] feat: support scoped-slot usage with $slot --- flow/compiler.js | 9 +- src/compiler/codegen/index.js | 2 +- src/compiler/optimizer.js | 3 +- src/compiler/parser/index.js | 83 +++++++++++++++++-- .../component/component-scoped-slot.spec.js | 59 +++++++++++++ 5 files changed, 146 insertions(+), 10 deletions(-) diff --git a/flow/compiler.js b/flow/compiler.js index de1e2df9a..509633e33 100644 --- a/flow/compiler.js +++ b/flow/compiler.js @@ -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 diff --git a/src/compiler/codegen/index.js b/src/compiler/codegen/index.js index d09bacdf7..bdd76be00 100644 --- a/src/compiler/codegen/index.js +++ b/src/compiler/codegen/index.js @@ -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 diff --git a/src/compiler/optimizer.js b/src/compiler/optimizer.js index 5f56e74cd..39b8737b2 100644 --- a/src/compiler/optimizer.js +++ b/src/compiler/optimizer.js @@ -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 ) { diff --git a/src/compiler/parser/index.js b/src/compiler/parser/index.js index 7905d84b5..9cb95b3e4 100644 --- a/src/compiler/parser/index.js +++ b/src/compiler/parser/index.js @@ -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'))) { diff --git a/test/unit/features/component/component-scoped-slot.spec.js b/test/unit/features/component/component-scoped-slot.spec.js index c327141bd..aa9e12cbf 100644 --- a/test/unit/features/component/component-scoped-slot.spec.js +++ b/test/unit/features/component/component-scoped-slot.spec.js @@ -613,4 +613,63 @@ describe('Component scoped slot', () => { expect(vm.$el.innerHTML).toBe('

hello

') }).then(done) }) + + // 2.6 $slot usage + describe('$slot support', () => { + it('should work', () => { + const vm = new Vue({ + template: `
{{$slot.foo}}
`, + components: { foo: { template: `
` }} + }).$mount() + expect(vm.$el.innerHTML).toBe(`
hello
`) + }) + + it('should work for use of $slots in attributes', () => { + const vm = new Vue({ + template: `
`, + components: { foo: { template: `
` }} + }).$mount() + expect(vm.$el.innerHTML).toBe(`
`) + }) + + it('should work for root text nodes', () => { + const vm = new Vue({ + template: `{{$slot.foo}}`, + components: { foo: { template: `
` }} + }).$mount() + expect(vm.$el.innerHTML).toBe(`hello`) + }) + + it('should work for mix of root text nodes and elements', () => { + const vm = new Vue({ + template: `hi
{{ $slot.foo }}
{{$slot.foo}}
`, + components: { foo: { template: `
` }} + }).$mount() + expect(vm.$el.innerHTML).toBe(`hi
hello
hello`) + }) + + it('should work for named slots', () => { + const vm = new Vue({ + template: `
{{ $slot.foo }}
`, + components: { foo: { template: `
` }} + }).$mount() + expect(vm.$el.innerHTML).toBe(`
hello
`) + }) + + it('should work for mixed default and named slots', () => { + const vm = new Vue({ + template: `{{ $slot.foo }}
{{ $slot.foo }}
{{ $slot.foo }}
`, + components: { foo: { template: `
` }} + }).$mount() + expect(vm.$el.innerHTML).toBe(`default
default
foo
`) + }) + + it('should work for mixed $slot and non-$slot slots', () => { + const vm = new Vue({ + template: `{{ $slot.foo }}
static
{{ $slot.foo }}
`, + components: { foo: { template: `
` }} + }).$mount() + expect(vm.$el.innerHTML).toBe(`default
default
static
`) + }) + }) })