-
Notifications
You must be signed in to change notification settings - Fork 26
RFC: Dual light/shadow components #77
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
Open
nolanlawson
wants to merge
6
commits into
master
Choose a base branch
from
nolan/dual-mode
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 3 commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
804220b
RFC: Dual light/shadow components
nolanlawson 728c1ce
fix: add clarifications
nolanlawson 7839d3a
fix: fix typo
nolanlawson 2fc6ca5
fix: add lwc:external
nolanlawson 8e1e974
fix: instantiation wording
nolanlawson ba1403f
fix: add inheritance
nolanlawson File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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> | ||
| ``` | ||
|
|
||
| ```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> | ||
nolanlawson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| </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> | ||
nolanlawson marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| </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. | ||
nolanlawson marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| 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. | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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?
In my opinion, using this directive as a boolean looks better because
lightmode 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-domlwc:light-modelwc:light//child
//parent forces child to use light mode
//parent does not say anything, therefore child is rendered using shadow mode
For the use case where a component forces the render mode to
lightin every context, I believe that using a variable calleduseLightDominside therender()method would look better. It seems better because this variable, liketemplateandstyleSheet, also changes the default render behavior of the component.//child
If the child is configured to use
lightmode, the parent would not need to add thelwc:lightdirective//parent
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.
There was a problem hiding this comment.
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. 🙂
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.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.
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 toattachShadow).I don't think they would need to document it either way, because we should be able to expose the static
renderModein the metadata. So it would be available to e.g. a drag-and-drop builder UI.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand it now 👍