diff --git a/flow/compiler.js b/flow/compiler.js index ad09a2e2ebe..9482ba7ae48 100644 --- a/flow/compiler.js +++ b/flow/compiler.js @@ -131,7 +131,8 @@ declare type ASTElement = { slotTarget?: ?string; slotTargetDynamic?: boolean; slotScope?: ?string; - scopedSlots?: { [name: string]: ASTElement }; + scopedSlots?: Array; + scopedSlotsMap?: { [name: string]: number }; ref?: string; refInFor?: boolean; diff --git a/src/compiler/codegen/index.js b/src/compiler/codegen/index.js index c96b3a38511..bcad797b93a 100644 --- a/src/compiler/codegen/index.js +++ b/src/compiler/codegen/index.js @@ -359,22 +359,21 @@ function genInlineTemplate (el: ASTElement, state: CodegenState): ?string { function genScopedSlots ( el: ASTElement, - slots: { [key: string]: ASTElement }, + slots: Array, state: CodegenState ): string { // by default scoped slots are considered "stable", this allows child // components with only scoped slots to skip forced updates from parent. // but in some cases we have to bail-out of this optimization // for example if the slot contains dynamic names, has v-if or v-for on them... - let needsForceUpdate = el.for || Object.keys(slots).some(key => { - const slot = slots[key] - return ( + let needsForceUpdate = el.for || slots.some(slot => + ( slot.slotTargetDynamic || slot.if || slot.for || containsSlotChild(slot) // is passing down slot from parent which may be dynamic ) - }) + ) // #9534: if a component with scoped slots is inside a conditional branch, // it's possible for the same component to be reused but with different @@ -404,8 +403,8 @@ function genScopedSlots ( } } - const generatedSlots = Object.keys(slots) - .map(key => genScopedSlot(slots[key], state)) + const generatedSlots = slots + .map(slot => genScopedSlot(slot, state)) .join(',') return `scopedSlots:_u([${generatedSlots}]${ diff --git a/src/compiler/helpers.js b/src/compiler/helpers.js index 7231d684cf4..3c69c2d2b56 100644 --- a/src/compiler/helpers.js +++ b/src/compiler/helpers.js @@ -229,3 +229,27 @@ function rangeSetItem ( } return item } + +export function addScopedSlot ( + target: ASTElement, + name: string, + slot: ASTElement, + append?: boolean +) { + const scopedSlots = target.scopedSlots || (target.scopedSlots = []) + if (append) { + // don't check the name of the scoped slot + // for example, dynamic v-slot and v-for are used on the same element + scopedSlots.push(slot) + } else { + // $flow-disable-line + const scopedSlotsMap = target.scopedSlotsMap || (target.scopedSlotsMap = Object.create(null)) + const i = scopedSlotsMap[name] + if (i >= 0) { + // overwrite it if already has same name scoped slot + scopedSlots[i] = slot + } else { + scopedSlotsMap[name] = scopedSlots.push(slot) - 1 + } + } +} diff --git a/src/compiler/parser/index.js b/src/compiler/parser/index.js index cdeb257eda4..16dd828c494 100644 --- a/src/compiler/parser/index.js +++ b/src/compiler/parser/index.js @@ -14,6 +14,7 @@ import { baseWarn, addHandler, addDirective, + addScopedSlot, getBindingAttr, getAndRemoveAttr, getRawBindingAttr, @@ -143,8 +144,18 @@ export function parse ( // scoped slot // keep it in the children list so that v-else(-if) conditions can // find it as the prev node. - const name = element.slotTarget || '"default"' - ;(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element + if ( + element.slotTargetDynamic && + element.for && + element.slotTarget + ) { + // #10271 + // dynamic v-slot and v-for are used on the same element + // TODO: checking if dynamic slot target actually use scope variables in v-for + addScopedSlot(currentParent, element.slotTarget, element, true) + } else { + addScopedSlot(currentParent, element.slotTarget || '"default"', element) + } } currentParent.children.push(element) element.parent = currentParent @@ -690,9 +701,9 @@ function processSlotContent (el) { } } // add the component's children to its default slot - const slots = el.scopedSlots || (el.scopedSlots = {}) const { name, dynamic } = getSlotName(slotBinding) - const slotContainer = slots[name] = createASTElement('template', [], el) + const slotContainer = createASTElement('template', [], el) + addScopedSlot(el, name, slotContainer) slotContainer.slotTarget = name slotContainer.slotTargetDynamic = dynamic slotContainer.children = el.children.filter((c: any) => { diff --git a/test/unit/features/component/component-scoped-slot.spec.js b/test/unit/features/component/component-scoped-slot.spec.js index 28369814f48..70d43e23db3 100644 --- a/test/unit/features/component/component-scoped-slot.spec.js +++ b/test/unit/features/component/component-scoped-slot.spec.js @@ -1325,4 +1325,32 @@ describe('Component scoped slot', () => { expect(vm.$el.textContent).toMatch(`1`) }).then(done) }) + + // #10271 + it('should work when dynamic slot name used in v-for', () => { + const Foo = { + template: ` +
+ + + +
+ ` + } + const vm = new Vue({ + data: { + item: 'c' + }, + template: ` + + + + + + `, + components: { Foo } + }).$mount() + + expect(vm.$el.textContent.trim()).toBe('A a B b C c') + }) })