Skip to content
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

bug: Conditional slot rendering removes always the last slot element, using key is a workaround #5263

Closed
3 tasks done
michaelworm opened this issue Jan 16, 2024 · 9 comments · Fixed by #5143
Closed
3 tasks done
Labels
Bug: Validated This PR or Issue is verified to be a bug within Stencil Has Workaround This PR or Issue has a work around detailed within it.

Comments

@michaelworm
Copy link

Prerequisites

Stencil Version

We were using 4.8.2 but it also happens with 4.10.0

Current Behavior

It occurs that always the last slot element will get removed in shadow dom when conditionally rendering the elements and removing the source slot element in the light dom.

experimentalSlotFixes makes no difference but using the "key" prop with a unique name fixes the issue.

Consider the following basic example below.

Expected Behavior

The Header Slot shall be removed in the example but the Footer Slot should be kept.

System Info

npx stencil info

      System: node 20.10.0
    Platform: darwin (23.2.0)
   CPU Model: Apple M1 Pro (10 cpus)
    Compiler: /Users/michaelworm/projects/porsche-design-system/node_modules/@stencil/core/compiler/stencil.js
       Build: 1702322155
     Stencil: 4.8.2 🐳
  TypeScript: 5.2.2
      Rollup: 2.42.3
      Parse5: 7.1.2
      Sizzle: 2.42.3
      Terser: 5.26.0

Steps to Reproduce

When clicking on "Remove Header" the Footer Slot will get removed and the Header Slot will be empty.

<p-example>
  <div slot="header">
    <h1>Header Slot</h1>
  </div>

  <p>Default Slot for Content</p>
  <button onclick="document.querySelector('[slot=header]').remove()">Remove Header</button>

  <div slot="footer">
    <p>Footer Slot</p>
  </div>
</p-example>
import { Component, Element, forceUpdate, h, type JSX } from '@stencil/core';

@Component({
  tag: 'p-example',
  shadow: true,
})
export class Example {
  @Element() public host!: HTMLElement;

  public render(): JSX.Element {
    const hasHeader = !!this.host.querySelector('[slot="header"]');
    const hasFooter = !!this.host.querySelector('[slot="footer"]');

    return (
      <div>
        {hasHeader && <slot name="header" onSlotchange={() => forceUpdate(this.host)} />}
        <slot />
        {hasFooter && <slot name="footer" onSlotchange={() => forceUpdate(this.host)} />}
      </div>
    );
  }
}

It seems that when adding elements between the slots, the bug does not occur.

...
<div>
  {hasHeader && <slot name="header" onSlotchange={() => forceUpdate(this.host)} />}
  <div></div>
  <slot />
  <div></div>
  {hasFooter && <slot name="footer" onSlotchange={() => forceUpdate(this.host)} />}
</div>
...

Code Reproduction URL

https://stackblitz.com/edit/node-dhtsl3?file=package.json,src%2Fcomponents%2Fmy-comp.tsx

Additional Information

Remove the "key" props in the StackBlitz Demo to see the issue.

@tanner-reits
Copy link
Member

Hey @michaelworm 👋

I confirmed the issue you were seeing, but think we may already have a solution in the works! I installed a recent dev build of Stencil and it appears to solve the issue you're seeing. If you want, you can try it out by installing @stencil/core@4.9.1-dev.1704816410.051d38e!

In the meantime, I'll get this issue labeled and tied back to the relevant PR. Thanks for letting us know!

@tanner-reits tanner-reits linked a pull request Jan 17, 2024 that will close this issue
2 tasks
@tanner-reits tanner-reits added Has Workaround This PR or Issue has a work around detailed within it. Bug: Validated This PR or Issue is verified to be a bug within Stencil and removed triage labels Jan 17, 2024
@tanner-reits tanner-reits removed their assignment Jan 17, 2024
alicewriteswrongs added a commit that referenced this issue Jan 26, 2024
This adds a new transformer, `performAutomaticKeyInsertion`, which will
add `key` attributes into certain JSX nodes in the `render()` method of
a Stencil component. This will allow the Stencil runtime to make more
accurate decisions about when to create and destroy child nodes during
re-rendering, especially in circumstances where nodes are changing order
and so on.

There are some limitations on when we can safely insert keys without
possibly introducing incorrect behavior. In particular, we don't want to
insert keys in the following situations:

- when a `render` method has more than one return statement
- when a `render` method has a conditional (ternary) expression in it
- when a JSX node is located inside of a JSX expression (i.e. within
  curly braces like `<div>{ ... }</div>`)

In each of these cases we don't have the static analysis chops to know
when a given JSX syntax tree node is supposed to correspond to the same
HTML element at runtime, whereas in the 'single return, JSX children
case' like:

```tsx
class Component {
  render () {
    return <div><img /></div>
  }
}
```

We know without a doubt in this case that the `div` and `img` JSX nodes
are always supposed to correspond to the same HTML elements at runtime,
so it's no problem to add keys to them.

The key insertion transformer also does not transform JSX nodes which
are not part of Stencil components, so if a project had, for some
reason, a React component or something in it we will leave it alone.

fixes #1968
fixes #5263
STENCIL-893
github-merge-queue bot pushed a commit that referenced this issue Jan 26, 2024
…5143)

* test(slot): add karma test for #1968

this commit adds a karma test for #1968, based on the minimal
reproduction case that we received for said issue

* feat(runtime): automatically insert `key` attrs during compilation

This adds a new transformer, `performAutomaticKeyInsertion`, which will
add `key` attributes into certain JSX nodes in the `render()` method of
a Stencil component. This will allow the Stencil runtime to make more
accurate decisions about when to create and destroy child nodes during
re-rendering, especially in circumstances where nodes are changing order
and so on.

There are some limitations on when we can safely insert keys without
possibly introducing incorrect behavior. In particular, we don't want to
insert keys in the following situations:

- when a `render` method has more than one return statement
- when a `render` method has a conditional (ternary) expression in it
- when a JSX node is located inside of a JSX expression (i.e. within
  curly braces like `<div>{ ... }</div>`)

In each of these cases we don't have the static analysis chops to know
when a given JSX syntax tree node is supposed to correspond to the same
HTML element at runtime, whereas in the 'single return, JSX children
case' like:

```tsx
class Component {
  render () {
    return <div><img /></div>
  }
}
```

We know without a doubt in this case that the `div` and `img` JSX nodes
are always supposed to correspond to the same HTML elements at runtime,
so it's no problem to add keys to them.

The key insertion transformer also does not transform JSX nodes which
are not part of Stencil components, so if a project had, for some
reason, a React component or something in it we will leave it alone.

fixes #1968
fixes #5263
STENCIL-893

---------

Co-authored-by: Ryan Waskiewicz <ryanwaskiewicz@gmail.com>
@rwaskiewicz
Copy link
Member

The fix for this issue has gone out in today's v4.12.0 release

@michaelworm
Copy link
Author

@rwaskiewicz Awesome, thank you!

@michaelworm
Copy link
Author

michaelworm commented Jan 31, 2024

@rwaskiewicz I can still reproduce the bug on 4.12.0 release when removing the manually added keys (footer slot is also gone when deleting header slot) => https://stackblitz.com/edit/node-wacxnr?file=src%2Fcomponents%2Fmy-comp.tsx

Is there something I am missing here?

@alicewriteswrongs
Copy link
Contributor

Hey @michaelworm, thanks for checking back in to see about this issue!

I just took at look at that stackblitz repro and I'm seeing something a bit puzzling - I'm only able to reproduce the issue immediately after doing a full-page refresh of the stackblitz environment, and if I refresh the stackblitz 'browser-within-a-browser' I can than no longer reproduce the issue. I'm using this link: https://stackblitz.com/edit/node-wacxnr?file=src%2Fcomponents%2Fmy-comp.tsx

Here's what I see:

Screen.Recording.2024-02-01.at.9.49.35.AM.mov

to sort of narrate that, I'm loading up the link and once the stackblitz env boots and starts running the project I am initially able to reproduce the issue. However, if I then click the refresh button for the little stackblitz browser it won't reproduce any more.

This made me think there might be some caching behavior on stackblitz' side which is contributing to this, so I copied your component code and index.html into a local project I created with npm init stencil, where I wasn't able to reproduce the issue locally using Stencil 4.12.0. It's possible that it still can show up under some circumstances though - I may be missing something here.

So all that said, could you possibly take a look at putting together a local reproduction case? You can create a Stencil project quickly with npm init stencil component $PROJECTNAME and, after creating a reproduction case, push it up to github. If we have a reliable reproduction case we can then re-open the issue and further diagnose what's going on here.

@michaelworm
Copy link
Author

@alicewriteswrongs Yes sure, I will try and provide a local example asap.

In the meantime, do you have any idea why the issue never occurs when manually adding keys to the slots (as it is in the first demo on Stackblitz)?

Like this:

return (
      <div>
        {hasHeader && <slot key="header" name="header" onSlotchange={() => forceUpdate(this.host)} />}
        <slot />
        {hasFooter && <slot key="footer" name="footer" onSlotchange={() => forceUpdate(this.host)} />}
      </div>
    );

@alicewriteswrongs
Copy link
Contributor

Yeah, I would expect that in versions of Stencil prior to 4.12.0 adding keys there should fix the issue because the keys will allow the Stencil runtime to do a more accurate job of matching up children across component re-renders, so that the footer won't get dropped when another child disappears. In 4.12.0 we added automatic insertion of randomly-generated keys into JSX nodes where it is safe to do so - in this case, that would be into the un-named <slot /> element, so that should fix the issue as well.

@michaelworm
Copy link
Author

@alicewriteswrongs Actually I could not reproduce it locally, you are right. So it must be something with Stackblitz. Thank you for the support!

@alicewriteswrongs
Copy link
Contributor

Oh good, glad we were able to get to the bottom of it! I suspect it's something in the caching that stackblitz does to get their environment to boot so rapidly in the browser, but who knows 🤷‍♀️

good luck with your project!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug: Validated This PR or Issue is verified to be a bug within Stencil Has Workaround This PR or Issue has a work around detailed within it.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants