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

RFC: Light DOM Support #44

Merged
merged 30 commits into from Sep 10, 2021
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
cb55a94
RFC: Add Light DOM RFC
abdulsattar Sep 3, 2020
72fe402
chore: remove misleading line
abdulsattar Mar 17, 2021
a306839
fix: html template example
abdulsattar Mar 17, 2021
9bf37b0
chore: simplify language
abdulsattar Mar 17, 2021
bad70a8
fix: address review feedback
abdulsattar Mar 18, 2021
6b6572e
fix: move implementation details out, tweak text
abdulsattar Mar 18, 2021
f791aec
fix: fix grammar
abdulsattar Mar 18, 2021
990bc6a
chore: add section on querying
abdulsattar Mar 22, 2021
dea55cf
Update style section based on feedback
pmdartus Apr 1, 2021
0a87dea
Add open questions section
pmdartus Apr 1, 2021
a9b1c55
chore:
pmdartus Apr 12, 2021
ea27928
Replace MacroElement with static field
pmdartus Apr 12, 2021
3a62c00
chore: update based on discussions
abdulsattar Apr 15, 2021
fbcab6b
chore: minor copyedit tweaks
nolanlawson Apr 15, 2021
b2df06f
chore: add details on slots and update based on latest convo
nolanlawson Apr 15, 2021
36a77b8
fix: add more details on light DOM global styles
nolanlawson Apr 20, 2021
8313490
fix: respond to comments
nolanlawson Apr 21, 2021
340f4b9
chore: fix security section
abdulsattar Apr 26, 2021
a00f719
Simplify RFC structure
pmdartus Apr 26, 2021
653b680
fix: s/shadowDOM/shadow/g
nolanlawson Apr 26, 2021
846f6b9
fix: update text/0000-light-dom.md
nolanlawson Apr 30, 2021
74f6da6
fix: update text/0000-light-dom.md
nolanlawson May 5, 2021
458cc0f
fix: update text/0000-light-dom.md
nolanlawson May 5, 2021
619721a
fix: put lwc:no-shadow on <template>, not on <slot>
nolanlawson May 6, 2021
798268f
fix: add some usecases for light versus shadow
nolanlawson May 6, 2021
a3f6d71
fix: reword section to make it clearer
nolanlawson May 6, 2021
c8a76f0
feat: add render-mode instead of static `shadow`
abdulsattar May 20, 2021
e735d3e
fix: add text explaining need for directive
nolanlawson May 21, 2021
3460e3a
chore: remove references to shadow
abdulsattar Jun 1, 2021
baa79bf
Reflect updated state of Light DOM
pmdartus Sep 9, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
355 changes: 355 additions & 0 deletions text/0000-light-dom.md
@@ -0,0 +1,355 @@
---
title: Light DOM Support
nolanlawson marked this conversation as resolved.
Show resolved Hide resolved
status: DRAFTED
created_at: 2020-09-10
updated_at: 2020-09-10
champion: Philippe Riand (priand), Ted Conn (tconn)
pr: https://github.com/salesforce/lwc-rfcs/pull/44
---

# RFC: Light DOM Support

## Summary

LWC currently enforces the use of Shadow DOM for every component. This proposal aims to provide a new option, a toggle, which instead lets a component attach its content as children in the Light DOM.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would rephrase this sentence. Shadow DOM is not an enforcement, it is just the default.

This proposal is not a toggle, because it is not a 1-1 mapping between shadow DOM and light DOM. It will certainly require developers to rewrite partially their component. I see this proposal as a brand new type of components (with new requirements and restrictions).

Suggested change
LWC currently enforces the use of Shadow DOM for every component. This proposal aims to provide a new option, a toggle, which instead lets a component attach its content as children in the Light DOM.
As of today, all the LWC components inheriting from `LightningElement` render their content to the shadow DOM. This proposal introduces a brand new kind of component, which renders its content as children in the Light DOM.


## Basic example
nolanlawson marked this conversation as resolved.
Show resolved Hide resolved

When the Shadow DOM option is turned off for a component, then its content is not attached to its shadow-root, but to the Element itself. Here is an example, showing whenever Shadow DOM is on or off:

_Shadow DOM_

```html
<app-container-blue-shadow>
▼ #shadow-root (open)
<div>
<b>Blue Shadow:</b>
<span class="counter">...</span>
<button type="button">Add one</button>
</div>
pmdartus marked this conversation as resolved.
Show resolved Hide resolved
</app-counter-blue-shadow>
```

_Light DOM_

```html
<app-container-blue-light>
▼ <div>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<div>
<div>

<b>Blue Light:</b>
<span class="counter">...</span>
<button type="button">Add one</button>
</div>
</app-counter-blue-light>
```

As a result, when the content of a component lives in its children, it can be accessed like any other content in the Document host, and thus behave like any other content (styling, APIs, accessibility, third party tooling...).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
As a result, when the content of a component lives in its children, it can be accessed like any other content in the Document host, and thus behave like any other content (styling, APIs, accessibility, third party tooling...).
As a result, when the content of a component resides in the Light DOM, it can be accessed like any other content in the Document host, and thus behave like any other content (styling, APIs, accessibility, third party tooling...).


Shadow DOM provides a wonderful, native component encapsulation and composition model.

The two main reasons for using Shadow DOM are:

**Native Component Encapsulation**

The Shadow DOM encapsulation model provides a way for the component author to keep the component internals hidden, with no effect from the broader environment. This means that global styles have no effect on those internals and those internals cannot be queried from outside of the component. This provides a way for widget-type components to be more portable (eg: embedding into 3rd party apps) at the cost of some complexity that makes the creation of a component or an application harder.

**Native Component Composition**

The Shadow DOM gives custom element developers a way to allow consumers to compose some content that it should render. Think about native elements like `<select>`. This is done via `<slot>` elements that are automatically filled by the content from the light DOM. Without Shadow DOM there is no native component composition and this functionality must be provided by the framework.

> A single component that needs to stand on its own with its own set of functionality is a good candidate for shadow DOM. Salesforce Lightning components are a good example of that.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would remove this section, it doesn't bring much to this proposal.

Suggested change
Shadow DOM provides a wonderful, native component encapsulation and composition model.
The two main reasons for using Shadow DOM are:
**Native Component Encapsulation**
The Shadow DOM encapsulation model provides a way for the component author to keep the component internals hidden, with no effect from the broader environment. This means that global styles have no effect on those internals and those internals cannot be queried from outside of the component. This provides a way for widget-type components to be more portable (eg: embedding into 3rd party apps) at the cost of some complexity that makes the creation of a component or an application harder.
**Native Component Composition**
The Shadow DOM gives custom element developers a way to allow consumers to compose some content that it should render. Think about native elements like `<select>`. This is done via `<slot>` elements that are automatically filled by the content from the light DOM. Without Shadow DOM there is no native component composition and this functionality must be provided by the framework.
> A single component that needs to stand on its own with its own set of functionality is a good candidate for shadow DOM. Salesforce Lightning components are a good example of that.


## Motivation

Consumer applications require DOM traversal and observability of an application’s anatomy from the document root. Without this, theming becomes hard and 3rd party applications do not run properly:

- **Theming and branding**

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As Dean said in DRB today, UIP has the same (near-term future) requirements around theming and branding for LEX and LWR apps as Communities.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't believe the LEX requirements are the same as what Communities is requesting because the number of actors in the system is very different. Let's discuss offline.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hsterlingsfdc Could you make sure to bring the LWC team into the loop when this will be discussed/designed? I would like to better understand the LEX and LWR requirements in this regard.


Because of the CSS isolation, theming is made harder. Typically, theming is done via APIs (CSS properties) and/or CSS theming parts (`::part`). Both come with caveats and issues. Theming is critical for consumer apps, where custom branding is a must have. Some new APIs, like `::theme`, are investigated but they won’t be pervasively available before years.
[Styling is critical to web component reuse, but may prove difficult in practice](https://component.kitchen/blog/posts/styling-is-critical-to-web-component-reuse-but-may-prove-difficult-in-practice)

- **Third party integrations**

Third party tools need to traverse the DOM, which breaks with Shadow DOM and the existing browser APIs (querySelector, ...). Note that the use of Light DOM fixes for Light DOM components, but for for native Shadow DOM ones if the page contains some.

- Analytics tools, Personalization platforms, Commerce tools like PriceSpider or Honey...

- **Testing software**

They faced the same issues than third party tools when it comes to traverse the DOM

Furthermore, **the goal of Shadow DOM is not to enclose the entire application in a single shadow-bound element**. We want to build UIs which are comprised of multiple web components, not UIs which are a single, top-level web component. Web components shouldn’t be the fundamental mechanism for building everything in an app, otherwise they negate the usefulness of standard semantic HTML. This is why we think the current model is fundamentally flawed.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Furthermore, **the goal of Shadow DOM is not to enclose the entire application in a single shadow-bound element**. We want to build UIs which are comprised of multiple web components, not UIs which are a single, top-level web component. Web components shouldn’t be the fundamental mechanism for building everything in an app, otherwise they negate the usefulness of standard semantic HTML. This is why we think the current model is fundamentally flawed.
Introducing components rendering to the Light DOM opens new possibilities where applications can render most of their content in the Light DOM. But also keep individual widgets and leaf components are rendered in the Shadow DOM. This approach combines the best of both worlds by solving the issues presented above and offering strong encapsulation when needed.

![shadow spectrum](./shadow-spectrum.png?raw=true "Shadow Spectrum")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image content: Remove "Today" and "Where we want to be" label. The RFC should not present points of view but rather preset facts.


### Prior Art

Most of the libraries designed to support Shadow DOM also propose a Light DOM option, with a variety of Shadow DOM features (slots, scoped styles, etc.). It includes:

- [StencilJS](https://stenciljs.com/docs/styling#shadow-dom-in-stencil)
- [LitElement](https://lit-element.polymer-project.org/api/classes/_lit_element_.litelement.html#createrenderroot)
- [MS Fast Element](https://fast.design/docs/fast-element/working-with-shadow-dom#shadow-dom-configuration)
- ...
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- ...


Or frameworks that are built for Light DOM offer a Shadow DOM option:

- [Angular](https://angular.io/guide/component-styles#view-encapsulation)
- [React](https://github.com/Wildhoney/ReactShadow) (community library)
- [Vue.js](https://github.com/karol-f/vue-custom-element) (community library)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The list here gives examples of how other frameworks integrate with Shadow DOM. I don't think they related to this proposal.

Suggested change
Or frameworks that are built for Light DOM offer a Shadow DOM option:
- [Angular](https://angular.io/guide/component-styles#view-encapsulation)
- [React](https://github.com/Wildhoney/ReactShadow) (community library)
- [Vue.js](https://github.com/karol-f/vue-custom-element) (community library)


## Detailed design

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The list of invariants and constraints is missing. From the live discussion on this RFC today it became obvious that there are many you're working with.

  • Support all standard Light DOM APIs at runtime
  • What the developer codes in the template is what they see in the rendered DOM
  • Align with standards where possible; when no standard exist align with pre-existing LWC concepts, and finally other frameworks
  • A constraint is that the LWC compiler operates on 1 file at a time, not one component (eg HTML + Javascript)

There are many others that need to get captured here. This'll bring visibility to the rationale driving this design.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still fuzzy about:

What the developer codes in the template is what they see in the rendered DOM

what I see in the PR is that the content of the slot is flatten, so the slot element itself never gets rendered.


### Selecting Light DOM vs Shadow DOM

In raw JS, developer has the choice to use or not use shadow DOM for their custom element. For example,

```js
// uses Shadow DOM
class MyShadowElement extends HTMLElement {
connectedCallback() {
this.attachShadow({ mode: "open" });
this.shadowRoot.innerHTML = "<p>My custom element that uses Shadow DOM";
}
}

// doesn't use Shadow DOM
class MyLightElement extends HTMLElement {
connectedCallback() {
this.innerHTML = "<p>My element that does not use Shadow DOM";
}
}
```

In LWC as well, the developer will retain the choice: they can choose to inherit from `LightningElement` to select Shadow DOM (which already exists) or inherit from a new class, `MacroElement`, to opt into Light DOM.

```js
import { MacroElement } from "lwc";

export default class MyLightComponent extends MacroElement {}
```

`MacroElement` is a new class that will be exported from the `lwc` module. Its API and functionality remain identical to `LightningElement` with the primary difference of not using Shadow DOM.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit concerned about exposing light DOM via MacroElement. I can break my concerns into two groups: 1) deviation from other WC frameworks, and 2) potential class proliferation.

Deviation from other WC frameworks

Here is how other frameworks handle light DOM:

Stencil

@Component({
  tag: 'my-component',
  shadow: false // or omitted
})
class MyComponent {}

Fast

@customElement({
  name: 'my-component',
  template,
  shadowOptions: null // or `{ mode: 'closed' }` for closed shadow
})
class MyComponent extends FASTElement {}

Lit

class MyComponent extends LitElement {
  createRenderRoot() {
    return this // or return `this.attachShadow(...)` here
  }
}

So we have 2 frameworks opting for decorators, and another opting for an overridable method. LWC would be the only WC library using separate classes (unless I missed one).

Potential class proliferation

In the future, we may want to support closed shadow roots (edit: my mistake, didn't realize we already did 🤦 ). Would that be another class?

import { ClosedShadowElement } from 'lwc'

There is also a proposal for "open stylable roots" (WICG/webcomponents#909). Would this be another one?

import { OpenStylableElement } from 'lwc'

In that proposal, there is even some talk of attachShadow() eventually looking like this:

this.attachShadow({
  mode: 'open',
  styles: 'closed'
})

If the options bag for attachShadow continues to grow like this, then we could end up with a combinatorial explosion of classes to handle every possible case.

My personal preference would be for decorators or mixins, but mostly I'm just concerned about choosing classes here.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the original reason we chose a different class was to emphasize that these are different kinds of components because the programming model is different. I'm not sure if we still feel that is the case.

I'm curious how the createElement API will change which currently takes the mode.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tedconn Ah my mistake, I didn't even know we supported closed shadow roots. 🙂 It seems that createElement would be the right place to put the options bag (if it becomes a big bag!).

So it looks like my concerns about class proliferation might not apply, since we only plan on having MacroElement for this one case (light DOM). But then why not have light DOM as an option in createElement?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But then why not have light DOM as an option in createElement

Because we need an API on the component, so component authors can toggle, not on the engine. Something like this would be nicer though, right?

import { LightningElement } from 'lwc';

export default class LightDomElement extends LightningElement {

  static shadowOptions = {
    mode: null
  }

}

Copy link
Member

@pmdartus pmdartus Mar 22, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We discussed in the past 2 options, using a static field or using a brand new base class. Until the decorator proposal goes back to stage 3, I don't think it is safe to introduce a brand new decorator.

The main advantage of introducing a brand new class over static fields is around component inheritance. You can shadow (by inadvertence or intentionally) a base class static field. It means that you can turn a Shadow DOM component into a Light DOM one (and vice-versa) by extending from it. Such override can't be done when inheriting from a different base class.

In the following example, let say that we introduce a new shadowDOM static boolean to indicate whether the component rendered in the Light DOM or in the Shadow DOM. It is something we should discourage.

import { LigthningElement } from 'lwc';

class ShadowDOMComponent extends LightningElement {
  static shadowDOM = true;
}

class LigthDOMComponent extends ShadowDOMComponent {
  static shadowDOM = false;
}

Now that you speak about class proliferation, I am more favorable to adding a signal via a static field.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After discussion with the team, we landed on the static field rather than MacroElement, correct? AFAICR, we concluded:

  • Any solution we land on (classes, field, method, decorators, etc.) needs to be resolved at runtime. In other words, developers can always do shenanigans such that you need to do a runtime evaluation to figure out whether the element is shadow or light. Since the LWC compiler to know at compile-time, we would just have to teach developers to use patterns that are friendly for compile-time evaluation.
  • Decorators are not standard yet
  • Classes may result in class proliferation
  • Static fields seem to have nice ergonomics
  • So we should go with static fields

Is this right? @pmdartus @abdulsattar

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right.


### Component features when using Light DOM

Some of the LWC component capabilities are directly inherited from Shadow DOM, or emulated by the synthetic-shadow. Despite the use of Light DOM, we’d like to keep these features available, even if their behavior is slightly adapted to the Light DOM:
pmdartus marked this conversation as resolved.
Show resolved Hide resolved

- **Slots**

As we mentioned before, the component composition model in LWC is provided by slots. Light DOM will provide the same mental model for developers building Light DOM components.

In Light DOM, `<slot>` will denote the place where the slotted component will be attached. The `<slot>` element itself won't be rendered. The slotted content (or the placeholder content) will be flattened to the parent element at runtime.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
In Light DOM, `<slot>` will denote the place where the slotted component will be attached. The `<slot>` element itself won't be rendered. The slotted content (or the placeholder content) will be flattened to the parent element at runtime.
In Light DOM, `<slot>` will denote the place where the slotted component will be attached. The `<slot>` element itself won't be rendered. The slotted content (or the fallback content) will be flattened to the parent element at runtime.


Since the `<slot>` element itself isn't rendered, adding any event listeners to the `<slot>` element in the template will throw a compiler error.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Since the `<slot>` element itself isn't rendered, adding any event listeners to the `<slot>` element in the template will throw a compiler error.
Since the `<slot>` element itself isn't rendered, adding attributes or event listeners to the `<slot>` element in the template will throw a compiler error.


- **Scoped Styles**
pmdartus marked this conversation as resolved.
Show resolved Hide resolved

Shadow DOM styles are scoped to the component they belong. Styles don't leak out to the rest of the page and the page styles don't leak into this component. In Native shadow, it is enforced by the browser whereas in Synthetic shadow it is enforced by adding few attributes to the elements and scoping all CSS rules by those attributes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Shadow DOM styles are scoped to the component they belong. Styles don't leak out to the rest of the page and the page styles don't leak into this component. In Native shadow, it is enforced by the browser whereas in Synthetic shadow it is enforced by adding few attributes to the elements and scoping all CSS rules by those attributes.
In the Shadow DOM, styles are scoped to the enclosing shadow tree. Styles don't leak out to the rest of the page and the page styles don't leak into this component. In Native shadow, it is enforced by the browser whereas in Synthetic shadow it is enforced by adding few attributes to the elements and scoping all CSS rules by those attributes.


Styles scoping in Light DOM will be done differently. The styles will be scoped not just to the component they belong, but also to its children. These styles won't leak out to the parents (or their children) though. This is similar to how Aura styles its components.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Styles scoping in Light DOM will be done differently. The styles will be scoped not just to the component they belong, but also to its children. These styles won't leak out to the parents (or their children) though. This is similar to how Aura styles its components.
Styles scoping in Light DOM will be done differently. The styles will be scoped not just to the component they belong, but also to its children. These styles won't leak out to the parents (or their children) though.


E.g.

```html
<x-a>
<style>
p {
color: blue;
}
</style>

<p></p> <!-- will be blue -->
<p class="red"></p> <!-- will still be blue. red class from child won't leak out here -->

<x-b>
<style>
p.red {
color: red;
}
</style>

<p></p> <!-- will be blue. Style from x-a will be applied -->
<p class="red"></p> <!-- will be red -->
<p class="red"></p> <!-- This paragraph is coming from A and slotted into B. Will also be red. -->
</x-b>
</x-a>
```

Opting out of this scoping is not supported. There's no way for a component author to say a CSS rule shouldn't be scoped to that specific component (and its children). For example, CSS modules provides a `:global` selector that can be used to opt out of this local scoping. LWC won't provide any such selector. If global scoping is desired, a global stylesheet can be injected manually.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:global is not a standard CSS selector. I don't think it worth referencing this here.

Suggested change
Opting out of this scoping is not supported. There's no way for a component author to say a CSS rule shouldn't be scoped to that specific component (and its children). For example, CSS modules provides a `:global` selector that can be used to opt out of this local scoping. LWC won't provide any such selector. If global scoping is desired, a global stylesheet can be injected manually.
Opting out of this scoping is not supported. There's no way for a component author to say a CSS rule shouldn't be scoped to that specific component (and its children). If global scoping is desired, a global stylesheet can be injected manually.


- **`this.template`**

In `LightningElement`, `this.template` returns the shadow-root. It will return `null` in `MacroElement`.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will return null in MacroElement.

How do I reference the root element in a light dom component?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With LightningElement, all the properties and methods on the this value interacts with the host element (this.getAttribute, this.querySelector, this.classList). And this.template allows to interact with the shadow root. However, LWC never exposes the host element itself to the component instance. You can get access to it, via this.template.host but it is more a hack than anything else.

The same model is preserved with MacroElement. It is possible to interact with the host element using methods and properties on the this value. But there is currently no workaround (like this.template.host) to access the host element from a MacroElement component instance.

If accessing the host element is necessary, I would vote for making it available to both LitghtningElement and MacroElement in a similar fashion. For example via a host getter.


### Security (WIP)

- In some applications, light-dom components may not be allowed... it’s up to the app context
- **what is the behavior when it’s not allowed?**
- Some applications might disable light-dom as a whole
- Some applications might disable light-dom selectively using a “privileged code” model
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

way more detail necessary here. Does this mean that a component was authored in the LightDom but app requested it in the shadow dom for ex?


### Component Migration

There is no migration of the existing components. Existing components inherit from `LightningElement` and light DOM components will inherit from `MacroElement` and they are two different things. If a component author wishes to use Light DOM for any of their existing components, they will have to make the relevant changes manually.

### Synthetic Shadow DOM
Copy link
Collaborator

@tedconn tedconn Mar 17, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think many people are asking "if everything is light DOM then what purpose does Synthetic Shadow DOM serve? The way I tried to answer that question today is "there will always be components somehow in the shadow DOM so we will always need an emulation." but maybe I am wrong?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are trying to get out of this model, where some components will always need emulation.

Synthetic shadow purpose was created to emulate shadow DOM on browsers that don't support shadow DOM natively. As we are sunsetting IE11 support, we shouldn't need synthetic shadow anymore.


The selection of Light DOM should not be impacted by the use of synthetic shadow instead of native shadow. Now, the goal is to get rid of synthetic shadow, but this is hardly possible today because of:

- Salesforce Lightning Components do not work with native shadow today
- Shadow DOM has accessibility issues

Could we think of a lightweight synthetic shadow that do not override the global methods but provides enough functions to the base components to work while letting third party integration tools work? This can be a different DOM option, which is an hybrid between Shadow DOM and Light DOM.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this section belongs to the proposal.
It overlaps more with @ravijayaramappa's work on Shadow DOM mixed mode.


## Internal Implementation
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would extract this section out of the RFC and move this content to a spike document.
The internal implementation might change over time, while the RFC should remain accurate on the long run.


Fortunately, most of the code already exists in the LWC core runtime, as it has been implemented to support synthetic shadow. This makes the implementation much easier, and only touching a few code blocks.

### Class Structure

LWC will be refactored into a base class `BaseLightningElement` that will be used throughout the `lwc` codebase. `LightningElement` class will inherit `BaseLightningElement` and will have only Shadow DOM specific logic. For example, `renderer.attachShadow(elm, ...)` will only exist in `LightningElement`.

`MacroElement` will also inherit `BaseLightningElement` and will have light dom specific logic. For example, `this.template` will return `null`.

Developers can't inherit from `BaseLightningElement` directly - compiler will prevent that from happening.

### View Model

Each LWC component has a VM (View Model) associated to it which carries the component runtime information. The VM class has an attribute `cmpRoot` which is of type `ShadowRoot`. We'll have to change it to `ShadowRoot | null` to denote that it will be null in case of `MacroElement`.

```typescript
export interface VM<N = HostNode, E = HostElement> {
cmpRoot: ShadowRoot | null;
}
```

This can also be used to check whether a `vm` is associated with a Shadow DOM element or a Light DOM element.

### Template

Template compiler, when compiling a template that belongs to a Light DOM component, should throw compiler errors when event listeners are found on `slot` elements or when `lwc:dom="manual"` directive is found.

Currently, the template compiler has no context of the Javascript file and thus can't know whether the template being compiled is for a Light DOM component or a Shadow DOM component.

To solve the above problem, the template in a Light DOM component, will have a special `macro` attribute at the root `<template>` tag.

```html
<template macro>
<h1>My Light DOM element</h1>

<slot onslotchange={handleSlotChange}></slot> <!-- throws error -->
<div lwc:dom="manual"></div> <!-- throws error -->
</slot>
</template>
```

In the presence of the `macro` attribute, the template-compiler will do additional validations. Currently, there doesn't seem to be a need to modify the compiler output in anyway.

### Scoped Styles

Style scoping will be done at the host level. The host will have a special attribute (similar to the one that is used in Synthetic shadow) and that attribute selector will be used to prefix all the CSS selectors in the style.

`:host` and `:host()` will also be replaced with the host token.

e.g. for the following CSS,

```css
p {
color: red;
}
```

the output of style-compiler will be (note the addition of `macroSelector`)

```js
function stylesheet(hostSelector, shadowSelector, nativeShadow, macroSelector) {
return [macroSelector, "p", shadowSelector, " {color: red;}"].join("");
}
export default [stylesheet];
```

which when evaulated as follows:

```js
const macroSelector = isMacroElement ? `[${tokens.hostAttribute}]` : '';

content = evaluateStylesheetsContent(
stylesheets,
hostSelector,
shadowSelector,
!syntheticShadow,
macroSelector
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
content = evaluateStylesheetsContent(
stylesheets,
hostSelector,
shadowSelector,
!syntheticShadow,
macroSelector
content = evaluateStylesheetsContent(
stylesheets,
hostSelector,
shadowSelector,
!syntheticShadow,
macroSelector
)

```

will result in

```css
[x-test_test_a] p {
color: red;
}
```

Runtime will also add the attribute `x-test_test_a` to the host element here:

```js
if (renderer.syntheticShadow || !cmpRoot) {
updateSyntheticShadowAttributes(vm, html);
}
```

### Slotting

Slotting will mostly reuse synthetic shadow slotting. However, there is one major difference: the slot element itself won't be rendered.

Instead of the slot, two empty text nodes will be inserted to denote the start and end of the slot. This can be done by introducing a new `VNode` called `VFakeSlot`:

```js
export interface VFakeSlot extends VElement {
start: VText;
end: VText;
}
```

Creating a node for `VFakeSlot` will mean creating the two text nodes. Adding/removing children to the slot will result in adding or removing children _between_ the two text nodes.

For example:

```js
// updating dynamic children
const endElm = vnode.end.elm!;
const parentElm = endElm.parentNode!;
updateDynamicChildren(parentElm, oldCh, newCh, endElm); // inserted before endElm

// in updating static children
const startIndex = Array.prototype.indexOf.call(parentElm, startElm);
addVnodes(parentElm, null, newCh, startIndex, newChLength);
```

`updateDynamicChildren` here is the same function that is used for synthetic shadow.

### Server Side Rendering

The engine-server module should provide the SSR capability to seamlessly render Shadow DOM or Light DOM. It should include the component children, as well as the scoped styles.

### POC

More implementation details available through this POC:

- Git repo: [https://github.com/salesforce/lwc/tree/abdulsattar/spikes/light-dom](https://github.com/salesforce/lwc/tree/abdulsattar/spikes/light-dom)

**Earlier POC by Philippe Riand**

- Demo: [https://priandsf.github.io/lwc-light-dom-static](https://priandsf.github.io/lwc-light-dom-static)
- Git repo: [https://github.com/priandsf/lwc/blob/light-dom-1.7.7/packages/sample-app/README.md](https://github.com/priandsf/lwc/blob/light-dom-1.7.7/packages/sample-app/README.md)

## Adoption strategy

This new feature does not break any existing components, it simply adds a new feature that developers have to opt for. There is no migration of the existing components needed.

This feature should be exposed and explained to the component library developers as they might change how they develop their components internally.

## How we teach this

Shadow DOM and Light DOM are already names accepted by the industry, see: [Terminology: light DOM vs. shadow DOM](https://developers.google.com/web/fundamentals/web-components/shadowdom?hl=en).
We need to provide the proper documentation to educate the LWC developers:

- What are the differences between Shadow DOM and Light DOM
- We need a guide on when to use one or the other
Binary file added text/shadow-spectrum.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.