Skip to content
Open
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
297 changes: 297 additions & 0 deletions text/0000-dual-light-shadow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
---
title: Dual light/shadow components
status: DRAFTED
created_at: 2022-02-08
updated_at: 2022-02-08
champion: Nolan Lawson (nolanlawson)
pr: https://github.com/salesforce/lwc-rfcs/pull/77
---

# Dual light/shadow components

## Summary

[Light DOM components](https://rfcs.lwc.dev/rfcs/lwc/0115-light-dom) are a useful feature in certain contexts.
However, LWC currently requires a component to be either light or shadow – it cannot be both at once.

This RFC proposes a "dual" mode where the same component can run in either light DOM mode or shadow DOM mode.

## Basic example

A component can declare its `lwc:render-mode` to be `"dual"`:

```html
<!-- component.html -->
<template lwc:render-mode="dual">
<h1>Hello world</h1>
</template>
Comment on lines +25 to +27
Copy link

@AllanOricil AllanOricil Feb 9, 2023

Choose a reason for hiding this comment

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

What if dual is the default render mode for every component?

If all components were always both light or shadow, couldn't the render-mode directive be simplified to a boolean to force the render mode to be light?

if lwc:light-dom directive is present
     render mode = light
else 
     render mode = shadow

In my opinion, using this directive as a boolean looks better because light mode is an exception to the framework, and there is no 3rd option. I mean, shadow dom must be used unless something tells the contrary.

If this render mode directive could be simplified to a boolean, I would like to suggest these options:

lwc:light-dom
lwc:light-mode
lwc:light

//child

<template>
      <h1>
           Im dual by default.  
           If my  parent does not say that I must be rendered using light mode,
           Im going to default to shadow mode
       </h1>
</template>

//parent forces child to use light mode

<template>
      <x-child lwc:light>render light</x-child>
</template>

//parent does not say anything, therefore child is rendered using shadow mode

<template>
      <x-child>render shadow</x-child>
</template>

For the use case where a component forces the render mode to light in every context, I believe that using a variable called useLightDom inside the render() method would look better. It seems better because this variable, like template and styleSheet, also changes the default render behavior of the component.

//child

export default class Child extends LightningElement {
  render(){
      return { 
           useLightDom: true,
           template: ...,
           styleSheet: ...,
           ... //any other attribute that changes the default render behavior
      }
  }
}

If the child is configured to use light mode, the parent would not need to add the lwc:light directive

//parent

<template>
      <x-child>render light</x-child> //light mode is used because the child is forcing it
</template>

3rd party devs would have to document which components render in light mode no matter the context, because developers wouldn't be able to determine this information just reading the parent component's template.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@AllanOricil Thanks for the feedback! I appreciate it a lot. 🙂

couldn't the render-mode directive be simplified to a boolean

I believe the reason we went with a string enum originally was because we weren't sure if someday we would want to support more exotic cases, like closed shadow mode or "open-stylable" shadow mode. Although maybe that would argue that we should use a different name for "dual" – perhaps "any" or something.

What if dual is the default render mode for every component?

I can see the case for "dual by default," but the main issue I see is that components that were not designed with light DOM in mind (or with shadow DOM in mind, for light DOM components) may be forced into a mode that breaks them. Whereas with my proposal, light/shadow components only render in that one mode unless they explicitly opt-in.

I believe that using a variable called useLightDom inside the render()

This would be very convenient from the component authoring perspective, but unfortunately it would greatly complicate how the engine is implemented for the component to switch between light and shadow mode dynamically during its lifetime. For instance, if light-DOM styles were added to the <head>, now those have to be converted into shadow DOM styles in the shadow root (or vice versa). And while it's possible to attach a new shadow root to an existing light DOM component, it's not possible to remove a shadow root (the browser does not provide a "remove" analog to attachShadow).

3rd party companies would have document which components render in light mode no matter the context, because developers wouldn't be able to determine this information by just reading the parent component's template.

I don't think they would need to document it either way, because we should be able to expose the static renderMode in the metadata. So it would be available to e.g. a drag-and-drop builder UI.

Copy link
Contributor

Choose a reason for hiding this comment

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

What if dual is the default render mode for every component?

That is non-backward compatible for you. Additionally, we could say that you can, as a consumer of the component, always only select the mode that you know the consumer component works... saying that it is a responsibility of the consumer to always specify has two big drawbacks that I can't really sign off on:

  1. if the current version of the component does work in both modes (just by pure luck), and the consumer select light, then the owner of the component modifies that, and inadvertently breaks light, he never really sign on for that, now we have breaking changes.
  2. now the consumer has to learn that there is to mode, and which components are broken and needs to be forced to a particular mode of operation, that seems very wrong to me.

Choose a reason for hiding this comment

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

I understand it now 👍

```

```js
// component.js
export default class extends LightningElement {
static renderMode = 'dual';
}
```

Then, a parent component can use `lwc:render-mode` to choose to render the child as either light DOM or shadow DOM:

```html
<template>
<x-component lwc:render-mode="light"></x-component>
</template>
```

```html
<template>
<x-component lwc:render-mode="shadow"></x-component>
</template>
```

This will result in `<x-component>` being rendered as either light DOM:

```html
<x-component>
<h1>Hello world</h1>
</x-component>
```

or shadow DOM:

```html
<x-component>
#shadow-root
<h1>Hello world</h1>
</x-component>
```

## Motivation

Light DOM and shadow DOM have tradeoffs relative to each other. Each may be useful in certain cases.

Also, the design of LWC lends itself to largely shadow-DOM-neutral component authoring. For instance, LWC supports scoped styles, light DOM `<slot>`s, and `lwc:ref` – all of which work roughly the same between shadow components and light components. So there is not a strong reason for a component to declare upfront whether it is light or shadow.

So naturally, a request we've heard from component authors is that they would like to be able to author a single component and have it render as either light or shadow DOM.

To be fair, there is already a workaround today: composition. I.e., a component could be authored in light DOM, and then a wrapper component could be authored in shadow DOM.

However, this workaround has several downsides:

1. All properties have to be reflected between the shadow parent and light child.
2. All slots have to be propagated from the parent to the child.
3. An extra wrapper component is required, which may be undesired for common base components (e.g. buttons, icons).
4. The two components have different external tag names (e.g. `my-button` and `my-button-shadow`).

This RFC has none of these downsides.

## Detailed design

### Declaring a dual-mode component

Currently the `lwc:render-mode` directive / `static renderMode` property only allows two values: `"light"` and `"shadow"`.

This proposal adds a third value: `"dual"`.

```html
<!-- component.html -->
<template lwc:render-mode="dual">
<h1>Hello world</h1>
</template>
```

```js
// component.js
export default class extends LightningElement {
static renderMode = 'dual';
}
```

### Switching between light and shadow mode

Currently, `lwc:render-mode` can only be applied to the top-level `<template>`. In this proposal,
it can also be applied to LWC components referenced inside a template:

```html
<template>
<x-component lwc:render-mode="light"></x-component>
<x-component lwc:render-mode="shadow"></x-component>
</template>
```

When used in this way, the value of `lwc:render-mode` can only be a static string, and it can only be `"light"` or `"shadow"`.

If `"dual"` is used in this context, a compile-time error is thrown:

```html
<template>
<!-- Invalid -->
<x-component lwc:render-mode="dual"></x-component>
</template>
```

If a template is declared to be `lwc:render-mode="dual"`, then the corresponding component must also have
`static renderMode = 'dual'`, and vice versa.

If not, an error will be thrown at runtime. (This is the same as what currently happens if `"shadow"` and `"light"`
are mixed between the two.)

### Non-LWC components

`lwc:render-mode` can only be applied to LWC components. If it's applied to any other element, including an
`lwc:external` component, then a compile-time error is thrown:

```html
<template>
<!-- Invalid -->
<third-party lwc:render-mode="light"></third-party>
</template>
```

### No explicit `lwc:render-mode`

If a dual-mode component is referenced _without_ an explicit `lwc:render-mode`, then it is considered to be a shadow component:

```html
<template>
<!-- Valid, defaults to shadow mode -->
<x-component></x-component>
</template>
```

### Non-dual-mode components

If `lwc:render-mode` is applied to a component that is _not_ a dual-mode component, then a runtime error is thrown:

```html
<template>
<!-- Invalid -->
<x-not-dual lwc:render-mode="shadow"></x-not-dual>
</template>
```

```js
// notDual.js
export default class extends LightningElement {
}
```

(This error is thrown regardless of whether the component would have rendered in shadow DOM or light mode.)

### Dynamic components

`lwc:render-mode` is also supported on [dynamic components](https://github.com/salesforce/lwc-rfcs/pull/71), since it
is already known in advance that the dynamic component is an LWC component.

```html
<template>
<!-- Valid if the component is always dual-mode -->
<lwc:component lwc:is={ctor} lwc:render-mode="light"></lwc:component>
</template>
```

However, if a non-dual-mode component is rendered, then a runtime error is thrown.

### Shadow DOM mixed mode

A dual-mode component can also use the `static shadowSupportMode` property to control whether it renders in native or
synthetic shadow. This only applies when it is running in shadow mode.

```js
export default class extends LightningElement {
static renderMode = 'dual';
static shadowSupportMode = 'any'; // use native shadow when in shadow mode
}
```

### Restrictions

In short:

1. Any restrictions that apply to either light DOM components or shadow DOM components also apply to dual components.
2. Component authors are responsible for handling runtime differences, e.g. `this.querySelector` vs `this.template.querySelector`.

#### Light DOM restrictions that apply

Following [light DOM restrictions](https://rfcs.lwc.dev/rfcs/lwc/0115-light-dom), a dual-mode component cannot have
a `slotchange` event listener on a `<slot>`. This throws a compile-time error:

```html
<template lwc-render-mode="dual">
<!-- Invalid -->
<slot onslotchange={onSlotChange}></slot>
</template>
```

However, unlike light DOM components, a dual-mode component can use `lwc:dom="manual"` (see next section).

#### Shadow DOM restrictions that apply

`lwc:dom="manual"` must be used for manual DOM operations if a dual-mode component is running in synthetic shadow mode. If not,
an error will be logged (as is currently the case with normal shadow components).

[Scoped slots](https://github.com/salesforce/lwc-rfcs/pull/63) are not allowed, because they are not supported by
shadow DOM components. If a component author uses `lwc:slot-bind` in a dual-mode template, then a compile-time error is thrown
(the same as with normal shadow components).

#### Behavior that differs at runtime between light and shadow

Some behavior will differ at runtime between light DOM mode and shadow DOM mode. Dual-mode components are responsible
for handling these differences themselves.

A non-exhaustive list:

- `::slotted` selectors in CSS are permitted, but will only work when running in native shadow mode.
- `this.template` is undefined for light DOM components.
- Slots are lazy in light DOM and eager in native shadow DOM.
- Scoped and non-scoped stylesheets will both behave differently in light versus shadow mode.

#### Runtime modifications

A component cannot dynamically modify its `static renderMode`; changing it after instantiation has no effect.

Due to the design of the `lwc:render-mode` property in this RFC, it will also be impossible for the LWC engine to render
a component in one mode, and then switch to another mode later for the same instance.

In other words, the following example will result in a complete destruction and recreation of the component in question
if the `lwc:if` condition changes:

```html
<template>
<template lwc:if={useLightDom}>
<x-component lwc:render-mode="light"></x-component>
</template>
<template lwc:else>
<x-component lwc:render-mode="shadow"></x-component>
</template>
</template>
```

This relies on the existing behavior of the LWC engine, and does not introduce any new modifications.

## Drawbacks

Implementing this feature adds additional complexity and may create confusion about which is better: shadow mode, light mode,
or dual mode.

It also introduces the possibility that a component author will only test in one mode (light/shadow)
and not realize that their component is broken in another mode.

## Alternatives

None considered.

## Adoption strategy

This is net-new functionality and developers can adopt it incrementally without backwards-compatibility issues.

# How we teach this

In addition to teaching the differences between light and shadow mode (which we already do), we would need to teach
why it might be useful to support dual mode.

In practice, dual mode is probably only going to be needed for specific use cases, namely generic components that
may be reused in a variety of environments, and so most component authors will probably not use it.

# Unresolved questions

None at this time.