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 4 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
330 changes: 330 additions & 0 deletions text/0000-light-dom.md
@@ -0,0 +1,330 @@
---
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. While one or more components as part of an application might not need Shadow DOM, as their intended use is much clearer and their markup less fragile.
pmdartus marked this conversation as resolved.
Show resolved Hide resolved

## Motivation

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

- **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

### Selecting Light DOM vs Shadow DOM

The selection of Light DOM vs Shadow DOM is under the control of the component developer. It is done at the component class level, by inheriting a different base class (`MacroElement`, not `LightningElement`). The LWC compiler can also use this information to check any issue and warn the developer.
pmdartus marked this conversation as resolved.
Show resolved Hide resolved

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

export default class MyComponent extends MacroElement {}
```

### 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. In Native shadow, it is enforced by the browser while in Synthetic shadow the styles are scoped by some attributes which are also added to the element at runtime.

In Light DOM, the styles will be scoped not just to the component they belong, but also to its children. This is similar to how Aura styles its components.

E.g.

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

<p item=1></p>

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

<p item=2></p>
<p item=3></p> <!-- This paragraph is coming from A and slotted into B -->
</x-b>
</x-a>
```

In this case, item 1 will be in blue and item 2 and 3 will be in red. Also note that in `<x-a>`, we can write a selector of higher specificity `x-b p` to override the styles in `x-b`.
pmdartus marked this conversation as resolved.
Show resolved Hide resolved

Global scoping is not supported. If desired, a global stylesheet can be injected manually.
pmdartus marked this conversation as resolved.
Show resolved Hide resolved

- **`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 needed. The behaviors of existing components using Shadow DOM remain the same.
pmdartus marked this conversation as resolved.
Show resolved Hide resolved

### 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.