| title | status | created_at | updated_at | champion | pr |
|---|---|---|---|---|---|
Declarative root element |
DRAFTED |
2022-01-19 |
2022-01-19 |
Nolan Lawson (nolanlawson) |
Provides a declarative way to apply reactive attributes, classes, and event listeners to the root (host) element.
In a component's template HTML file:
<template>
<lwc:root
class="static-class"
data-foo={dynamicAttribute}
onclick={onRootClick}
lwc:spread={otherAttributes}
></lwc:root>
<h1>Hello world</h1>
</template>Result:
<x-component class="static-class" data-foo="foo" data-other="other">
#shadow-root
<h1>Hello world</h1>
</x-component>Classes, attributes, and event listeners are applied from the <lwc:root> element to the
root <x-component> element. (The event listener is not shown above.)
A common pattern in LWC components is something like this:
export default class extends LightningElement {
connectedCallback() {
this.template.host.classList.add('my-class');
}
}Developers may want to add a class, set an attribute, or add an event listener to the root (host) element
of the component. Today there is no declarative way to do this, so they resort to doing it manually in the
connectedCallback, constructor, or renderedCallback.
This has several downsides:
- It does not work well with SSR, where it would require shims for DOM APIs like
classList,setAttribute, etc., whereconnectedCallbacktimings may differ between SSR and DOM, and whererenderedCallbackdoesn't fire at all. - It is not easy to make it reactive, e.g. to set a class that updates based on a
@tracked property.
Prior art in other frameworks:
- Stencil:
<Host>functional component - Angular:
@HostBinding - FAST: Host directive template
- Lit: Nonexistent, but considered
The <lwc:root> element is a synthetic element defined in a component's template HTML file. Similar to dynamic components, it does not actually render in the DOM.
<template>
<lwc:root class="foo"></lwc:root> <!-- Doesn't actually render -->
</template>However, many attributes and directives that can be applied to a normal HTMLElement can be applied to <lwc:root>. These include:
- Standard global HTML attributes, such as
classandtabindex - Custom HTML attributes, such as
data-* - Event listeners, such as
onclickoronfocus - The
lwc:spreaddirective
Other directives that apply to normal HTMLElements, such as lwc:inner-html, lwc:ref, and key, are not supported.
A bare <lwc:root></lwc:root> with no attributes/listeners/directives is allowed, but effectively does nothing.
A <lwc:root> element must be placed at the top level (root) of a <template>, and may not be preceded by other elements:
<!-- Valid -->
<template>
<lwc:root></lwc:root>
<div></div>
</template><!-- Invalid -->
<template>
<div>
<lwc:root></lwc:root>
</div>
</template><!-- Invalid -->
<template>
<div></div>
<lwc:root></lwc:root>
</template>Comments are allowed to precede <lwc:root>:
<!-- Valid -->
<template>
<!-- Comment -->
<lwc:root></lwc:root>
</template>However, in the case of lwc:preserve-comments, comments may not precede <lwc:root>:
<!-- Invalid -->
<template lwc:preserve-comments>
<!-- Comment -->
<lwc:root></lwc:root>
</template>The <lwc:root> element may not have any contents other than whitespace and comments:
<!-- Valid -->
<template>
<lwc:root>
</lwc:root>
</template><!-- Valid -->
<template>
<lwc:root>
<!-- Comment -->
</lwc:root>
</template>However, in the case of lwc:preserve-comments, comments inside of <lwc:root> are not allowed:
<!-- Invalid -->
<template lwc:preserve-comments>
<lwc:root>
<!-- Comment -->
</lwc:root>
</template>This restriction is why lwc:inner-html is not supported on <lwc:root>.
In terms of timing, any attributes added to or removed from the root element should follow the timing applied to elements inside of the template:
<template>
<lwc:root class={clazz}></lwc:root>
<div class={clazz}></div>
</template>In other words, in the above example, if clazz changes, then the root element's class attribute should be changed
in the same tick as when the <div>'s class changes.
The above also applies to the timing of when event listeners are attached using on*.
No guarantees are made about the ordering of changes made to <lwc:root> versus elements inside of the template.
Attributes and event listeners added using <lwc:root> may conflict with those added by the parent component:
<!-- parent.html -->
<template>
<x-child class="foo"
data-foo="bar"
onclick={onClick}
lwc:spread={others}>
</x-child>
</template><!-- child.html -->
<template>
<lwc:root class="quux"
data-foo="toto"
onclick={onClick}
lwc:spread={others}>
</lwc:root>
</template>In cases of conflict, the following rules apply:
lwc:spreadprecedence applies as normal within the<lwc:root>element itself.- For event listeners such as
onclick, both event listeners are attached. - For the
classattribute, strings are concatenated with a single whitespace (" ") character (e.g."foo quux"in the above example). No attempt is made to deduplicate duplicate strings. - For all other attributes, the parent overrides the child's attribute (e.g.
data-foowould be"bar"in the above example).
The reason for this is to allow a component to set defaults for its root's behavior, while still allowing consumers to override those defaults.
Implementing this feature does require additional complexity, and moves us further away from standard custom element syntax.
Other frameworks call this feature "host." And there is some precedent in LWC, in that light DOM scoped styles allow for styling the root of a light DOM component using :host in *.scoped.css.
However, this proposal prefers "root" to "host," because "root" is a more generic term that does not imply shadow DOM. This
makes it clear that, for example, a child light DOM component cannot set attributes on a parent shadow DOM component by using this technique. (Whereas a child light DOM component can indeed style its parent's root node using :host in unscoped CSS.)
Historically in LWC, <template> is used to designate a reusable tree of HTML, e.g. in for:each and scoped slots. The root <template> does not actually refer to the root/host element.
Also, because <lwc:root> supports attributes and directives that generally would apply to normal HTMLElements, it makes
sense to represent it as a pseudo-normal HTML element.
Supporting lwc:ref is technically feasible, but doesn't make much sense, as there are already alternatives. In shadow DOM, developers can use this.template.host, and in light DOM, they can use this.
Also, the whole point of this feature is to avoid needing programmatic access to the root/host element. So adding lwc:ref would be counter-productive.
This proposal can be adopted as a net-new feature and does not have backwards-compatibility implications. lwc:root is
not a pre-existing built-in HTML element, and it's extremely unlikely that anyone is creating runtime elements with this name.
It may be possible to write codemods that look for simple usages of e.g. this.template.host.classList.add('foo')
and replace it with <lwc:root>. This may be too complex to be worthwhile, though.
This feature is LWC-specific (not based on an existing web standard) and will have to be taught as such. The existence of similar mechanisms in other frameworks (e.g. Stencil and Angular) does provide some reference points that help with teaching.
However, the fact that, for the most part, we can just say "<lwc:root> behaves like a normal element" makes it easier to teach this. Developers can reuse their existing knowledge to understand how it works.
None at this time.