lit icon indicating copy to clipboard operation
lit copied to clipboard

[labs/ssr] LitElement renders content inside a Declarative Shadow DOM when using `createRenderRoot` server side

Open thescientist13 opened this issue 2 years ago • 13 comments

Description

When using Lit SSR and enabling createRenderRoot for a custom element, Lit is rendering the content inside declarative shadow DOM <template> tag.

Steps to Reproduce

  1. Given the sample Greeting component that uses createRenderRoot
    import { html, css, LitElement } from 'lit';
    
    export class SimpleGreeting extends LitElement {
      static styles = css`p { color: blue }`;
    
      static properties = {
        name: {type: String},
      };
    
      constructor() {
        super();
        this.name = 'Somebody';
      }
    
      createRenderRoot(){
        return this;
      }
    
      render() {
        return html`<p>Hello, ${this.name}!</p>`;
      }
    }
    
    customElements.define('simple-greeting', SimpleGreeting);
    
  2. Server Render the component
    import { render } from '@lit-labs/ssr/lib/render-with-global-dom-shim.js';
    import { html } from 'lit';
    import './greeting.js';
    
    import Koa from 'koa';
    import koaNodeResolve from 'koa-node-resolve';
    import { Readable } from 'stream';
    
    const { nodeResolve } = koaNodeResolve;
    const app = new Koa();
    const port = 8080;
    
    app.use(async (ctx) => {
      ctx.type = 'text/html';
      ctx.body = Readable.from(render(html`
        <simple-greeting></simple-greeting>
        <simple-greeting name="SSR Works!"></simple-greeting>
      `));
    });
    app.use(nodeResolve({}));
    
    app.listen(port, () => {
      console.log(`Server listening on port ${port}`);
    });
    

Live Reproduction Link

https://stackblitz.com/github/thescientist13/lit-ssr-create-render-root

Expected Results

No Declarative Shadow DOM would be present, just like in the Lit Playground

<simple-greeting>
    <style>p { color: blue }</style>
    <!--lit-part EvGichL14uw=--><p>Hello, <!--lit-part-->Somebody<!--/lit-part-->!</p><!--/lit-part-->
</simple-greeting>
Screen Shot 2022-06-23 at 9 27 51 PM

Actual Results

Declarative Shadow DOM is still rendered

<simple-greeting>
  <template shadowroot="open">
    <style>p { color: blue }</style>
    <!--lit-part EvGichL14uw=--><p>Hello, <!--lit-part-->Somebody<!--/lit-part-->!</p><!--/lit-part-->
  </template>
</simple-greeting>
Screen Shot 2022-06-23 at 9 42 18 PM

Browsers Affected

  • [ ] Chrome
  • [ ] Firefox
  • [ ] Edge
  • [ ] Safari 11
  • [ ] Safari 10
  • [ ] IE 11
  • [x] NodeJS

thescientist13 avatar Jun 24 '22 01:06 thescientist13

I'm not sure we're going to be able to support customized render roots like this, so we'll have to add a check and throw an error.

justinfagnani avatar Jun 24 '22 03:06 justinfagnani

So it would just fail entirely? So effectively there would be no way to SSR a LitElement without having to use Declarative Shadow DOM?

thescientist13 avatar Jun 25 '22 13:06 thescientist13

For now, it's likely.

The problem here is that lit-html's hydration relies on shadow roots for nested hydration - that is lit-html's hydrate() function assumes that it owns the whole DOM subtree being hydrate (very similar to render() on the first render). Without shadow DOM, each LitElement having its own render() call would be a problem because of mixed ownership of the DOM tree, but shadow DOM scoping makes it so that each render() call does have it's own subtree.

Setting the render root to this has a lot of negative side-effects, including that we've now mixed DOM ownership of the parent scope. Some DOM comes from the parent, some comes from the child. Client-side this works sometimes because lit-html won't modify nodes it doesn't create. (It only works sometimes because if the light-DOM-rendering child had children of it's own or expressions in it put there by the parent, they would be cleared.) With server rendering all the DOM would be in one tree and hydrate() would get confused when trying to associate DOM with template expressions. It wouldn't know which came from the parent or child.

We could probably add support for this in time, but until then it seems wiser to error than to incorrectly put what should have been light-DOM into the shadow root.

justinfagnani avatar Jun 27 '22 18:06 justinfagnani

Alrighty. Good to know and thanks for the explanation.

thescientist13 avatar Jun 27 '22 22:06 thescientist13

Some password managers (LastPass being the largest) still do not discover input elements in ShadowDOM. Since my main application requires authentication, SSR for the login form is the largest need making the requirement for ShadowDOM only a blocker for me. It's more important that password manager integration function than implementing SSR.

Is there a path forward here for this special case? If that means I have to write custom hydration logic for these specific cases that is completely acceptable. Said another way - I don't expect this to be a common requirement for most developers and the solution can be technically difficult for me to implement.

ChadKillingsworth avatar Mar 08 '23 12:03 ChadKillingsworth

@ChadKillingsworth talking with more SSR users recently makes me think we'll need to support this - it's just a question of when we can get to it.

Technically what we need to do is leave an indicator on the root child part that tells any outer hydration pass to just skip it completely - it doesn't even count as a part as far the outer template is concerned. This seems like good practice anyway for all render roots. It'll allow mixing of imperatively created render roots.

Then we'll need to make sure that LitElement's hydration can pick up the right child part when the render root is customized.

cc @kevinpschaaf and @augustjk

justinfagnani avatar Mar 08 '23 17:03 justinfagnani

That makes sense and I agree on the priority as well. Thanks!

ChadKillingsworth avatar Mar 08 '23 17:03 ChadKillingsworth

This would greatly help our team out, too, as we're considering lit SSR + light DOM only for our new architecture.

Is there any way to use any of the shadyDOM JS related code to convert a render to shady DOM? That's actually our current ideal for this new architecture R&D -- then we get (logical) CSS isolation automatically but the markup and CSS rules are all in the light DOM which is seeming to us the best of both worlds.

So if we could convert something like this:

collectResultSync(render(html`<item-tile mdapi="..."><item-stat>...</item-stat></item-tile>`)))

to shady DOM on the server and send that out to the client, we'd get the setup

  • working on all 2%+ browsers
  • no browser JS needed for initial render, no FOUC, no layout shift
  • still able to client hydrate the rest and SPA-like repaint the page on content link clicks/changes, etc. (for browsers w/ JS enabled)

(Seems worth mentioning we've got SPA and rendertron pages as well -- but are interested in moving back towards isomorphic code SSR for SEO reasons, rendertron deprecated, and more)

traceypooh avatar Mar 21 '23 21:03 traceypooh

Support for this will be very useful for us too 👍

ruud avatar Mar 22 '23 15:03 ruud

We would also be very happy with this issue being fixed. The biggest, if not only, issue keeping us from using Lit SSR at the moment. Keep up the amazing work.

hvdmeulen avatar Mar 24 '23 10:03 hvdmeulen

Hi there! May I ask if there is any update on this? We would ❤️ for this to be supported too! :)

fetishcoder avatar Dec 04 '23 09:12 fetishcoder

Just wanted to circle back on this again, now in light of support for server only templates ( #2441 ), that being able to have server only components (without DSD) would be a great companion feature!

A similar thread around doing async work on the server, by which we could leveraging the component model for the concepts of pages and layouts would bring a nice continuity to using Lit on the client, the server, and everything in between! 💯

thescientist13 avatar Jan 03 '24 22:01 thescientist13

Any updates?

Flrande avatar Apr 12 '24 04:04 Flrande