Skip to content

Commit

Permalink
feat: add capability in engine to accept a factory function as slot c…
Browse files Browse the repository at this point in the history
…ontent
  • Loading branch information
ravijayaramappa committed Oct 5, 2022
1 parent b3396f1 commit 6dd9342
Show file tree
Hide file tree
Showing 6 changed files with 84 additions and 26 deletions.
38 changes: 34 additions & 4 deletions packages/@lwc/engine-core/src/framework/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -44,6 +44,8 @@ import {
VStatic,
Key,
VFragment,
isVScopedSlotContent,
VScopedSlotContent,
} from './vnodes';

const SymbolIterator: typeof Symbol.iterator = Symbol.iterator;
Expand All @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -571,6 +600,7 @@ const api = ObjectFreeze({
gid,
fid,
shc,
ssf,
});

export default api;
Expand Down
2 changes: 1 addition & 1 deletion packages/@lwc/engine-core/src/framework/hydration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
34 changes: 20 additions & 14 deletions packages/@lwc/engine-core/src/framework/rendering.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`);
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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!
Expand Down Expand Up @@ -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)) {
Expand All @@ -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;
Expand Down
6 changes: 3 additions & 3 deletions packages/@lwc/engine-core/src/framework/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}.`
);

Expand Down
9 changes: 6 additions & 3 deletions packages/@lwc/engine-core/src/framework/vm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -135,7 +137,8 @@ export interface VM<N = HostNode, E = HostElement> {
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 };
Expand Down Expand Up @@ -301,7 +304,7 @@ export function createVM<HostNode, HostElement>(
velements: EmptyArray,
cmpProps: create(null),
cmpFields: create(null),
cmpSlots: create(null),
cmpSlots: { slotAssignments: create(null) },
oar: create(null),
cmpTemplate: null,
hydrated: Boolean(hydrated),
Expand Down
21 changes: 20 additions & 1 deletion packages/@lwc/engine-core/src/framework/vnodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Array<VNode | null>>;

Expand All @@ -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;
Expand Down Expand Up @@ -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 {
Expand All @@ -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;
}

0 comments on commit 6dd9342

Please sign in to comment.