diff --git a/packages/@lwc/engine-core/src/framework/api.ts b/packages/@lwc/engine-core/src/framework/api.ts index 036a4a2143..a552f6f080 100644 --- a/packages/@lwc/engine-core/src/framework/api.ts +++ b/packages/@lwc/engine-core/src/framework/api.ts @@ -26,7 +26,7 @@ import { import { logError } from '../shared/logger'; import { invokeEventListener } from './invoker'; -import { getVMBeingRendered } from './template'; +import { getVMBeingRendered, setVMBeingRendered } from './template'; import { EmptyArray, setRefVNode } from './utils'; import { isComponentConstructor } from './def'; import { ShadowMode, SlotSet, VM, RenderMode } from './vm'; @@ -44,6 +44,8 @@ import { VStatic, Key, VFragment, + isVScopedSlotContent, + VScopedSlotContent, } from './vnodes'; const SymbolIterator: typeof Symbol.iterator = Symbol.iterator; @@ -52,6 +54,18 @@ function addVNodeToChildLWC(vnode: VCustomElement) { ArrayPush.call(getVMBeingRendered()!.velements, vnode); } +// [s]coped [s]lot [f]actory +function ssf(factory: (value: any) => VNodes): VScopedSlotContent { + return { + type: VNodeType.ScopedSlotContent, + factory, + owner: getVMBeingRendered()!, + elm: undefined, + sel: undefined, + key: undefined, + }; +} + // [st]atic node function st(fragment: Element, key: Key): VStatic { return { @@ -169,10 +183,25 @@ function s( } if ( !isUndefined(slotset) && - !isUndefined(slotset[slotName]) && - slotset[slotName].length !== 0 + !isUndefined(slotset.slotAssignments) && + !isUndefined(slotset.slotAssignments[slotName]) && + slotset.slotAssignments[slotName].length !== 0 ) { - children = slotset[slotName]; + children = slotset.slotAssignments[slotName].flatMap((vnode) => { + // If the passed slot content is factory, evaluate it and use the produced vnodes + if (vnode && isVScopedSlotContent(vnode)) { + const vmBeingRenderedInception = getVMBeingRendered(); + if (!isUndefined(slotset.owner)) { + // Evaluate in the scope of the slot content's owner + setVMBeingRendered(slotset.owner); + } + const children = vnode.factory(data.slotData); + setVMBeingRendered(vmBeingRenderedInception); + return children; + } else { + return vnode; + } + }); } const vmBeingRendered = getVMBeingRendered()!; const { renderMode, shadowMode } = vmBeingRendered; @@ -571,6 +600,7 @@ const api = ObjectFreeze({ gid, fid, shc, + ssf, }); export default api; diff --git a/packages/@lwc/engine-core/src/framework/hydration.ts b/packages/@lwc/engine-core/src/framework/hydration.ts index 28f6f1c8e1..0482e0e646 100644 --- a/packages/@lwc/engine-core/src/framework/hydration.ts +++ b/packages/@lwc/engine-core/src/framework/hydration.ts @@ -246,7 +246,7 @@ function hydrateCustomElement( vnode.elm = elm; vnode.vm = vm; - allocateChildren(vnode, vm); + allocateChildren(vnode, vm, owner); patchElementPropsAndAttrs(vnode, renderer); // Insert hook section: diff --git a/packages/@lwc/engine-core/src/framework/rendering.ts b/packages/@lwc/engine-core/src/framework/rendering.ts index ad1c3df34a..d6d789c022 100644 --- a/packages/@lwc/engine-core/src/framework/rendering.ts +++ b/packages/@lwc/engine-core/src/framework/rendering.ts @@ -309,7 +309,7 @@ function mountCustomElement( applyStyleScoping(elm, owner, renderer); if (vm) { - allocateChildren(vnode, vm); + allocateChildren(vnode, vm, owner); } else if (vnode.ctor !== UpgradableConstructor) { throw new TypeError(`Incorrect Component Constructor`); } @@ -363,7 +363,7 @@ function patchCustomElement( if (!isUndefined(vm)) { // in fallback mode, the allocation will always set children to // empty and delegate the real allocation to the slot elements - allocateChildren(n2, vm); + allocateChildren(n2, vm, vm.owner!); } // in fallback mode, the children will be always empty, so, nothing @@ -558,7 +558,7 @@ function applyElementRestrictions(elm: Element, vnode: VElement | VStatic) { } } -export function allocateChildren(vnode: VCustomElement, vm: VM) { +export function allocateChildren(vnode: VCustomElement, vm: VM, owner: VM) { // A component with slots will re-render because: // 1- There is a change of the internal state. // 2- There is a change on the external api (ex: slots) @@ -576,7 +576,7 @@ export function allocateChildren(vnode: VCustomElement, vm: VM) { const { renderMode, shadowMode } = vm; if (shadowMode === ShadowMode.Synthetic || renderMode === RenderMode.Light) { // slow path - allocateInSlot(vm, children); + allocateInSlot(vm, children, owner); // save the allocated children in case this vnode is reused. vnode.aChildren = children; // every child vnode is now allocated, and the host should receive none directly, it receives them via the shadow! @@ -611,9 +611,12 @@ function createViewModelHook(elm: HTMLElement, vnode: VCustomElement, renderer: return vm; } -function allocateInSlot(vm: VM, children: VNodes) { - const { cmpSlots: oldSlots } = vm; - const cmpSlots = (vm.cmpSlots = create(null)); +function allocateInSlot(vm: VM, children: VNodes, owner: VM) { + const { + cmpSlots: { slotAssignments: oldSlotsMapping }, + } = vm; + const cmpSlotsMapping = create(null); + vm.cmpSlots = { owner, slotAssignments: cmpSlotsMapping }; for (let i = 0, len = children.length; i < len; i += 1) { const vnode = children[i]; if (isNull(vnode)) { @@ -625,26 +628,29 @@ function allocateInSlot(vm: VM, children: VNodes) { slotName = (vnode.data.attrs?.slot as string) || ''; } - const vnodes: VNodes = (cmpSlots[slotName] = cmpSlots[slotName] || []); + const vnodes: VNodes = (cmpSlotsMapping[slotName] = cmpSlotsMapping[slotName] || []); ArrayPush.call(vnodes, vnode); } if (isFalse(vm.isDirty)) { // We need to determine if the old allocation is really different from the new one // and mark the vm as dirty - const oldKeys = keys(oldSlots); - if (oldKeys.length !== keys(cmpSlots).length) { + const oldKeys = keys(oldSlotsMapping); + if (oldKeys.length !== keys(cmpSlotsMapping).length) { markComponentAsDirty(vm); return; } for (let i = 0, len = oldKeys.length; i < len; i += 1) { const key = oldKeys[i]; - if (isUndefined(cmpSlots[key]) || oldSlots[key].length !== cmpSlots[key].length) { + if ( + isUndefined(cmpSlotsMapping[key]) || + oldSlotsMapping[key].length !== cmpSlotsMapping[key].length + ) { markComponentAsDirty(vm); return; } - const oldVNodes = oldSlots[key]; - const vnodes = cmpSlots[key]; - for (let j = 0, a = cmpSlots[key].length; j < a; j += 1) { + const oldVNodes = oldSlotsMapping[key]; + const vnodes = cmpSlotsMapping[key]; + for (let j = 0, a = cmpSlotsMapping[key].length; j < a; j += 1) { if (oldVNodes[j] !== vnodes[j]) { markComponentAsDirty(vm); return; diff --git a/packages/@lwc/engine-core/src/framework/template.ts b/packages/@lwc/engine-core/src/framework/template.ts index 43f1c65451..e4bfd0e115 100644 --- a/packages/@lwc/engine-core/src/framework/template.ts +++ b/packages/@lwc/engine-core/src/framework/template.ts @@ -77,12 +77,12 @@ function validateSlots(vm: VM, html: Template) { const { cmpSlots } = vm; const { slots = EmptyArray } = html; - for (const slotName in cmpSlots) { + for (const slotName in cmpSlots.slotAssignments) { // eslint-disable-next-line @lwc/lwc-internal/no-production-assert assert.isTrue( - isArray(cmpSlots[slotName]), + isArray(cmpSlots.slotAssignments[slotName]), `Slots can only be set to an array, instead received ${toString( - cmpSlots[slotName] + cmpSlots.slotAssignments[slotName] )} for slot "${slotName}" in ${vm}.` ); diff --git a/packages/@lwc/engine-core/src/framework/vm.ts b/packages/@lwc/engine-core/src/framework/vm.ts index 605753e072..65ef104e1d 100644 --- a/packages/@lwc/engine-core/src/framework/vm.ts +++ b/packages/@lwc/engine-core/src/framework/vm.ts @@ -50,7 +50,9 @@ export interface TemplateCache { } export interface SlotSet { - [key: string]: VNodes; + // Slot assignments by name + slotAssignments: { [key: string]: VNodes }; + owner?: VM; } export const enum VMState { @@ -135,7 +137,8 @@ export interface VM { velements: VCustomElement[]; /** The component public properties. */ cmpProps: { [name: string]: any }; - /** The mapping between the slot names and the slotted VNodes. */ + /** Contains information about the mapping between the slot names and the slotted VNodes, and + * the owner of the slot content. */ cmpSlots: SlotSet; /** The component internal reactive properties. */ cmpFields: { [name: string]: any }; @@ -301,7 +304,7 @@ export function createVM( velements: EmptyArray, cmpProps: create(null), cmpFields: create(null), - cmpSlots: create(null), + cmpSlots: { slotAssignments: create(null) }, oar: create(null), cmpTemplate: null, hydrated: Boolean(hydrated), diff --git a/packages/@lwc/engine-core/src/framework/vnodes.ts b/packages/@lwc/engine-core/src/framework/vnodes.ts index 561babcdd0..5caa1d4a43 100644 --- a/packages/@lwc/engine-core/src/framework/vnodes.ts +++ b/packages/@lwc/engine-core/src/framework/vnodes.ts @@ -17,9 +17,17 @@ export const enum VNodeType { CustomElement, Static, Fragment, + ScopedSlotContent, } -export type VNode = VText | VComment | VElement | VCustomElement | VStatic | VFragment; +export type VNode = + | VText + | VComment + | VElement + | VCustomElement + | VStatic + | VFragment + | VScopedSlotContent; export type VParentElement = VElement | VCustomElement | VFragment; export type VNodes = Readonly>; @@ -35,6 +43,12 @@ export interface BaseVNode { owner: VM; } +export interface VScopedSlotContent extends BaseVNode { + // TODO [#9999]: should the factory return a VFragment instead? + factory: (value: any) => VNodes; + type: VNodeType.ScopedSlotContent; +} + export interface VStatic extends BaseVNode { type: VNodeType.Static; sel: undefined; @@ -106,6 +120,7 @@ export interface VElementData extends VNodeData { // Similar to above, all props are readonly readonly key: Key; readonly ref?: string; + readonly slotData?: any; } export function isVBaseElement(vnode: VNode): vnode is VElement | VCustomElement { @@ -116,3 +131,7 @@ export function isVBaseElement(vnode: VNode): vnode is VElement | VCustomElement export function isSameVnode(vnode1: VNode, vnode2: VNode): boolean { return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel; } + +export function isVScopedSlotContent(vnode: VNode): vnode is VScopedSlotContent { + return vnode.type === VNodeType.ScopedSlotContent; +}