3

I have a generic component that renders a form in a slot.

I want the layout of the component to be changeable. E.g. move the form from the right hand side of the screen to the top.

The layout changes are such that it's not really feasible to create it with a single DOM structure and then move form via CSS - the DOM itself needs to change structure.

For example (simplified massively to illustrate):

<lightning-layout if:true={showTopForm}>
    <lightning-layout-item>
        <slot name="form"></slot>
    </lightning-layout-item>
</lightning-layout>

<lightning-layout>
    <lightning-layout-item>
        <p>Some things</p>
    </lightning-layout-item>
    <lightning-layout-item if:true={showRightForm}>
        <slot name="form"></slot>
    </lightning-layout-item>
</lightning-layout>

I'm fully aware that a single node can only have a single parent, and therefore it would not be valid to render both instances of the slot at the same time.

However, showTopForm and showRightForm are mutually exclusive and therefore at no point are both versions rendered.

The slot is rendered correctly and everything appears to behave, except when flicking back and forth, once the earlier slot has been rendered the latter rendered version of the slot loses its event handlers and the form becomes unusable.

I'm not surprised that this doesn't work, but was hoping that the re-render would reparent the slot content as it was re-rendered and everything would work since, in theory, at no time is the slot rendered twice - it could have a single parent at all times.

Is anyone aware of a mechanism that can be used in this instance to reuse the same slot in the two places without having to either duplicate the markup or split the form into its own component and use that twice?

Edit: I am aware that I could accomplish this with 2 templates, but would prefer to avoid that duplication.

Edit 2: migration of if:true to lwc:if has no effect.

Edit 3: It may be more specifically related to picklists...

Child.html

<template>

    <!-- Child component handles the layout of a larger page, including where the form goes and the fact it can be moved -->

    <lightning-button label="Toggle" onclick={toggle}></lightning-button>

    <lightning-layout if:true={showFormOnTop}>
        <lightning-layout-item>
            <slot name="form"></slot>
        </lightning-layout-item>
    </lightning-layout>

    <lightning-layout>
        <lightning-layout-item>
            <p>Some things</p>
        </lightning-layout-item>
        <lightning-layout-item if:true={showFormOnRight}>
            <slot name="form"></slot>
        </lightning-layout-item>
    </lightning-layout>

</template>

Child.js

import { LightningElement } from "lwc";

export default class Child extends LightningElement {

  showFormOnTop = false;
  showFormOnRight = true;

  toggle() {
    this.showFormOnTop = !this.showFormOnTop;
    this.showFormOnRight = !this.showFormOnRight;
  }
}

Parent.html

<template>

    <!-- Parent component includes the child and passes it a form, including a picklist (combobox) -->

    <c-child>
        <span slot="form">
            <lightning-button onclick={handleClick} label="Action"></lightning-button>

            <lightning-combobox class="slds-p-right_small" label="Picklist" value="" options={options}
            onchange={handlePicklistChanged}></lightning-combobox>
        </span>
    </c-child>
</template>

Parent.js

import { LightningElement } from "lwc";

export default class App extends LightningElement {

    options = [ {label:'one', value: '1' }, {label:'two', value: '2' } ];

    handleClick() {
        alert('Action is working');
    }

    handlePicklistChanged( event ) {
        alert( event.currentTarget.value );
    }
}

LWC Studio: https://app.lwc.studio/edit/ESRW7uiWaqldFbEUhtTj/src/child.html?p=stories

Notice:

  • The Toggle is in the child component - the parent is intentionally not aware that the child can present different layouts
  • Clicking the Button always seems to work - you get the alert.
  • Opening the picklist initially works when the layout has it on the right.
  • Switch to 'top' and the picklist continues to work.
  • Switch to 'right' and the picklist no longer opens.
  • Switch to 'top' and the picklist once again works.
4
  • 1
    So... I basically copy-pasted your code and wired everything up, and it works as expected. As such, I believe the devil's in the details (which you've omitted). The basic premise of the slot toggling works fine, it might just be the specific element you're using; some system lightning components don't like being messed with a lot, and this might be one of those cases.
    – sfdcfox
    Feb 18, 2023 at 12:46
  • I knew I should have built the simple test case before posting! Sorry! I'll do that and see where I get. Thanks. Feb 18, 2023 at 12:54
  • 1
    No worries! It almost always helps to have a second pair of eyes on a problem, even if just to eliminate what isn't the problem.
    – sfdcfox
    Feb 18, 2023 at 13:25
  • I have a replication that is still simple... it looks like Comboboxes in particular don't work. It may not be limited to just those, but they are certainly a problem. Feb 18, 2023 at 16:03

1 Answer 1

2

There are several components that appear to do special things upon rendering in renderedCallback, or use custom getter/setter methods for some properties, and thus don't always behave correctly when you do certain things with them. One specific example I recall was that lightning-datatable wouldn't always render new values, even when using @track, simply because lightning-datatable wouldn't be "dirty" and therefore wouldn't trigger a render cycle.

I don't have time to dig into why this is happening right this second, but clearly there's a problem with lightning-combobox not fully rendering for whatever reason. I took your repro, and added a await Promise.resolve() between toggling the two Boolean values, thinking that maybe a race condition rendered a situation where there were two copies of the element were technically in the DOM at once. This ultimately yielded the same behavior, but one toggle cycle earlier.

I'm going to log an issue with the LWR team, we'll see if they can identify the problem. I'll also take another stab at troubleshooting this over the extended weekend to see if I can come up with a more concrete reason why this happens. I can tell you with certainty that there are no errors happening, so it feels like the component is being recreated rather than moved, but some steps end up being skipped during component initialization or rehydration. You'll notice that the broken copy always ends up being the same one. That means the components are probably cached, so no amount of toggling should alternate the pattern.

In the interim, I have a workaround for you. You can use lwc:dom="manual" to put the slot in the correct place. We prevent the slot from ever being lost or re-rendered, except when properties should be updated. Despite the slot not being in its original place, it is still reactive. I suspected it would be, but it's good to have confirmation of this.

First, in your template, we'll have a temporary holding element:

<div data-temp-container lwc:dom="manual"></div>

The new areas are now under manual DOM control as well:

<lightning-layout if:true={showFormOnTop}>
    <div data-form-container lwc:dom="manual"></div>
</lightning-layout>

<lightning-layout>
    <lightning-layout-item>
        <p>Some things</p>
    </lightning-layout-item>
    <div if:true={showFormOnRight} data-form-container lwc:dom="manual"></div>
</lightning-layout>

Next, when we know we're going to trigger a render cycle, we'll safely move the slot into that space:

const temp = this.template.querySelector('[data-temp-container]');
const slot = this.template.querySelector('slot[name="form"]');
temp.appendChild(slot);

Next, when we render again, we'll move the slot to its new home:

renderedCallback() {
  const slot = this.template.querySelector('slot[name="form"]');
  const target = this.template.querySelector('div[data-form-container]').appendChild(slot);
}

I don't know if this arrangement will have other side effects. I believe it is safe, but do test thoroughly. It appears that this pattern eliminates both the missing dropdown options I experienced with your original code, as well as the missing event handlers. As a bonus, the component also remembers its values and state, since we're not actually rendering it in the normal sense.

Demo.

4
  • This is an amazing response, and as soon as I get a chance (probably first thing Monday) I'll try to implement this and see how close to working it is. Even though I've not tested it, I'm giving you the green tick because I know that this is far further than anyone else can take it, and even if the answer is "it's a bug in the combobox LWC", then that sounds like the right answer to me. Good to hear I wasn't trying to do something crazy. Feb 18, 2023 at 18:22
  • 1
    @RobBaillie No problem. By the way, when I reduced it down even further, it then shows a rendering error. I suspect your nested components are somehow swallowing that error. This is definitely a bug, but my solution should work, since we avoid the rendering engine entirely this way.
    – sfdcfox
    Feb 18, 2023 at 18:27
  • I had to take a look - I couldn't resist. It 95% works - but now I'm seeing a problem with an OTB User Lookup. I can't help but feel like this is a rabbit hole, and the solution (for now) is to resort to two templates until such time as Salesforce fixes the bugs. Thanks for taking such a look at this, and for following it up with your contacts. Feb 18, 2023 at 18:52
  • Have reverted the code back to using two templates and it behaves itself properly. I don't know if that knowledge helps when it comes to looking at what's going wrong, but I admit that when I did the revert I was clenching my teeth expecting the bug to still be there. I'm slightly surprised that the rendering behaviour isn't just exactly the same. Feb 20, 2023 at 10:28

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .