Sunday, 14 May 2023

Light DOM Scoped Slots in Summer 23

Introduction

It's that time again - another Salesforce release is a few weeks away, the preview release notes are out, and preview scratch orgs can be spun up to try out some of the new features. There's a few changes around Lightning Components in the upcoming release, and the first one that caught my eye was the concept of scoped slots, made possible by the general availability of the light DOM.

The light DOM is what we used to call the DOM before web components came along - the standard Document Object Model representing the structure of a web page that is visible and accessible to any JavaScript that cares to look. The light aspect comes from the fact that web components by default use shadow DOM - a hidden DOM structure attached to the regular DOM that only the component can see. 

Scoped slots are an inversion of the usual slot behaviour seen with Lightning Web Components. The standard case allows a parent component to pass markup into a child component to render in specific locations. Scoped slots turns this on its head and allows child components to pass information up to parent components that can be rendered in a specific slot. All of which sounds very cool, but at first glance it seemed a solution looking for a problem. The example in the release notes didn't do much to change this view, as they showed a child iterating a list and the parent rendering the contents of the list item. Given that the parent can iterate the list just as easily as the child, I couldn't see the value that was added.

One thing I've found with Lightning Web Components is a lot of the new features have equivalents in another JavaScript framework - Vue.js seems to be the prime candidate at the moment, and sure enough there's the concept of scoped slots there. Unfortunately the various blogs that I read on this topic didn't help my understanding enormously - when you aren't familiar with the framework then the examples aren't always helpful. What I did take away from this is the value in scoped slots is to separate the generation of the data from the rendering, so that it is available for re-use. The release notes example didn't really provide this as the list iteration is the same regardless of whether you do it in the parent or child, so I needed to figure out another use case.

The Sample

Simple list iteration is easy, but what about the case where I have a JavaScript object created from an Apex Map and want to iterate that? I can't do this in markup, instead I have to create a list of properties from the object. This feels like a situation where a child component that can handle any conversion and iteration required and simply send the properties back to the parent. 

My component that handles this is called mapIterator, and when it receives an object via an @api property, it creates a list from it:

set objMap(value) {
    this._objMap=value;
    if (this._objMap) {
        this.values=[];
        for (let key in this._objMap) {
            this.values.push(this._objMap[key]);
        }
    }
}

and the HTML markup simply iterates this and makes each entry available to the parent via the lwc:slot-bind directive, remembering to render in light DOM mode:

<template lwc:render-mode="light"> 
    <template for:each={values} for:item="value">
        <slot key={value} lwc:slot-bind={value}></slot>
    </template>
</template>

I have several parent components that make use of this - one to generate a simple list of accounts, one to generate a list of account cards, and one to generate a list of opportunity cards. The mapIterator provides the conversion and iteration of the object properties to each of this with no changes required. You can find all of these at the Github repository, but here's the markup from the simple list :

<c-map-iterator obj-map={accountsMap}>
    <template lwc:slot-data="value">
        <div class="slds-p-left_small slds-p-bottom_xx-small">
            <strong>ID:</strong> {value.Id}
        </div>
        <div class="slds-p-left_large slds-p-bottom_small">
            <strong>Name:</strong>{value.Name}
        </div>
    </template>
</c-map-iterator>

Access to the element from the iterator is provided by the lwc:slot-data directive, and as you can see the parent handles all the presentation side of things.

Conclusion

There is definite value here in separating the conversion/iteration from the rendering of the content. That said, I think you might run into issues if you are rendering the iterated elements using markup that must be a direct descendant of a containing element. In that case you won't be able to separate everything, as you get the child element markup in the way. I think there you'd likely need specialisations of the iterator that also renders the container, which won't feel quite as clean.

More Information