-
Notifications
You must be signed in to change notification settings - Fork 393
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(template-compiler): implement handling for scoped slot directives in template #3077
Changes from 16 commits
52f0d2f
45c217d
90b9b0e
3c90967
4b9166d
e0e49fe
3cd6be4
812ff70
3e51fa9
188e84f
94edd67
3be71d6
505e592
4a426fe
3576c5e
1bbcbb4
21e7c11
54ef4b5
7840bfa
3a4edf5
2a95159
1798af7
ce36e81
8313089
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,12 +21,13 @@ import { | |
isUndefined, | ||
StringReplace, | ||
toString, | ||
ArrayConcat, | ||
} from '@lwc/shared'; | ||
|
||
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 +45,8 @@ import { | |
VStatic, | ||
Key, | ||
VFragment, | ||
isVScopedSlotContent, | ||
VScopedSlotContent, | ||
} from './vnodes'; | ||
|
||
const SymbolIterator: typeof Symbol.iterator = Symbol.iterator; | ||
|
@@ -52,6 +55,19 @@ function addVNodeToChildLWC(vnode: VCustomElement) { | |
ArrayPush.call(getVMBeingRendered()!.velements, vnode); | ||
} | ||
|
||
// [s]coped [s]lot [f]actory | ||
function ssf(factory: (value: any) => VNodes, slotName?: string): VScopedSlotContent { | ||
return { | ||
type: VNodeType.ScopedSlotContent, | ||
factory, | ||
owner: getVMBeingRendered()!, | ||
elm: undefined, | ||
sel: undefined, | ||
key: undefined, | ||
slotName, | ||
}; | ||
} | ||
|
||
// [st]atic node | ||
function st(fragment: Element, key: Key): VStatic { | ||
return { | ||
|
@@ -169,10 +185,30 @@ 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].reduce((acc, vnode) => { | ||
// If the passed slot content is factory, evaluate it and use the produced vnodes | ||
if (vnode && isVScopedSlotContent(vnode)) { | ||
const vmBeingRenderedInception = getVMBeingRendered(); | ||
let children: VNodes = []; | ||
if (!isUndefined(slotset.owner)) { | ||
// Evaluate in the scope of the slot content's owner | ||
setVMBeingRendered(slotset.owner); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This branch should always evaluate, in other words there shouldn't be a case where There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I could remove the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I think this is preferable since it better models the actual runtime behavior. It's impossible in active to have slotted content without an owner. |
||
try { | ||
children = vnode.factory(data.slotData); | ||
This comment was marked as resolved.
Sorry, something went wrong. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Working on a separate PR for testing reactivity #3105 |
||
} finally { | ||
setVMBeingRendered(vmBeingRenderedInception); | ||
} | ||
return ArrayConcat.call(acc, children); | ||
} else { | ||
// If the slot content is a static list of child nodes provided by the parent, nothing to do | ||
return ArrayConcat.call(acc, vnode); | ||
} | ||
}, [] as VNodes); | ||
} | ||
const vmBeingRendered = getVMBeingRendered()!; | ||
const { renderMode, shadowMode } = vmBeingRendered; | ||
|
@@ -571,6 +607,7 @@ const api = ObjectFreeze({ | |
gid, | ||
fid, | ||
shc, | ||
ssf, | ||
}); | ||
|
||
export default api; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -50,7 +50,9 @@ export interface TemplateCache { | |
} | ||
|
||
export interface SlotSet { | ||
[key: string]: VNodes; | ||
// Slot assignments by name | ||
slotAssignments: { [key: string]: VNodes }; | ||
owner?: VM; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why isn't the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The root vm will not have an owner for its slotset(its There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I think this is a preferable approach to avoid allocating 2 empty objects for all the components that don't accept slots. |
||
} | ||
|
||
export const enum VMState { | ||
|
@@ -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 }; | ||
|
@@ -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), | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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>>; | ||
|
||
|
@@ -35,6 +43,13 @@ export interface BaseVNode { | |
owner: VM; | ||
} | ||
|
||
export interface VScopedSlotContent extends BaseVNode { | ||
// TODO [#9999]: should the factory return a VFragment instead? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 9999? 🙃 |
||
factory: (value: any) => VNodes; | ||
type: VNodeType.ScopedSlotContent; | ||
slotName?: string; | ||
} | ||
|
||
export interface VStatic extends BaseVNode { | ||
type: VNodeType.Static; | ||
sel: undefined; | ||
|
@@ -106,6 +121,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 { | ||
|
@@ -120,3 +136,7 @@ export function isSameVnode(vnode1: VNode, vnode2: VNode): boolean { | |
export function isVCustomElement(vnode: VBaseElement): vnode is VCustomElement { | ||
return vnode.type === VNodeType.CustomElement; | ||
} | ||
|
||
export function isVScopedSlotContent(vnode: VNode): vnode is VScopedSlotContent { | ||
return vnode.type === VNodeType.ScopedSlotContent; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Side note,
setVMBeingRendered
is used in 2 different places with this PR: for template evaluation and for fragment evaluation. I was thinking about a way to abstract this since LWC template is just a supercharged fragment.I haven't found an elegant way to do this for now. It's certainly something worth keeping in the back of our head for the next time we touch this part of the code.