# Composition
Before we compose more complex components and apps, it's important to understand how Lightning web components use shadow DOM to render elements.
Tip
This document explains shadow DOM in the context of Lightning Web Components only. For a conceptual overview of shadow DOM, see Shadow DOM v1: self-contained web components on Web Fundamentals.
# Shadow DOM
Shadow DOM is a web standard that encapsulates the elements of a component to keep styling and behavior consistent in any context. Since not all browsers implement shadow DOM, Lightning Web Components uses a shadow DOM polyfill (@lwc/synthetic-shadow). A polyfill is code that allows a feature to work in a web browser.
Let’s look at an example. This recipe-hello component contains a ui-card component with some markup.
<!-- recipe-hello -->
<template>
<ui-card title="Hello">
<div>Some content in child component</div>
</ui-card>
</template>
When the components render in a browser, the elements are encapsulated in a shadow tree. A shadow tree is part of the DOM that's hidden from the document that contains it. The shadow tree affects how you work with the DOM, CSS, and events.
<recipe-hello>
#shadow-root
| <ui-card>
| #shadow-root
| | <div>Some content in child component</div>
| </ui-card>
</recipe-hello>
Example
See the Hello recipes in the Lightning Web Components recipes app.
The shadow root defines the boundary between the DOM and the shadow tree. This boundary is called the shadow boundary.
Shadow Root
The shadow root is the top-most node in a shadow tree. This node is attached to a regular DOM node called a host element. The shadow root isn’t an element; it’s a document fragment.
Shadow Boundary
The shadow boundary is the line between the shadow root and the host element. It's where the shadow DOM ends and the regular DOM begins. The regular DOM is also called the light DOM to distinguish it from the shadow DOM.
Whether a DOM is a light DOM or shadow DOM depends on the point of view.
-
From the point of view of a component's JavaScript class, the elements in its template belong to the light DOM. The component owns them; they're regular DOM elements.
-
From the point of view of the outside world, those same elements are part of the component's shadow DOM. The outside world can't see them or access them.
<body>
<p>I'm a paragraph element, part of the light DOM</p>
<!-- c-child is part of the light DOM.
But everything within #shadow-root is hidden
because it's part of c-child's shadow DOM.
-->
<c-child>
#shadow-root
| <p>
| In c-child, I'm light DOM.
| To everyone else, I'm shadow DOM.
| </p>
</c-child>
</body>
Let’s look at how to work with the shadow tree in each of these areas.
CSS
DOM queries and CSS selectors can't cross the shadow boundary, which creates encapsulation. This means CSS styles defined in a parent component don’t leak into a child. In our example, a p style defined in the todoApp.css stylesheet doesn’t style the p element in the example-todo-item component, because the styles don’t reach into the shadow tree. See CSS.
Events
If an event bubbles up and crosses the shadow boundary, to hide the internal details of the component that dispatched the event, some property values change to match the scope of the listener. See Event Retargeting.
Access Elements
To access elements a component renders from the component’s JavaScript class, use the template property. See Access Elements the Component Owns.
Access Slots
A slot is a placeholder for markup that a parent component passes into a component’s body. DOM elements that are passed to a component via slots aren’t owned by the component and aren’t in the component’s shadow tree. To access DOM elements passed in via slots, call this.querySelector() and this.querySelectorAll(). The component doesn't own these elements, so you don’t use template. See Pass Markup into Slots. To traverse the DOM tree for slotted content, use the HTMLSlotElement interface.
# DOM APIs
You can access elements the component owns or access elements passed via slots. We don't recommend manipulating the DOM imperatively using JavaScript because The framework handles the shadow root creation and content rendering for you.
Don’t use these DOM APIs to reach into a component’s shadow tree.
Document.prototype.getElementByIdDocument.prototype.querySelectorDocument.prototype.querySelectorAllDocument.prototype.getElementsByClassNameDocument.prototype.getElementsByTagNameDocument.prototype.getElementsByTagNameNSDocument.prototype.getElementsByNamedocument.body.querySelectordocument.body.querySelectorAlldocument.body.getElementsByClassNamedocument.body.getElementsByTagNamedocument.body.getElementsByTagNameNS
Important
The shadow DOM polyfill includes a patch to the MutationObserver interface. If you use MutationObserver to watch changes in a DOM tree, disconnect it or you will cause a memory leak. Note that a component can observe mutations only in its own template. It can't observe mutations within the shadow tree of other custom elements.
If you need to traverse the DOM for cases such as third-party tooling requirements, global styling, etc., we recommend using Light DOM (Developer Preview).
# Composing Apps and Components
You can add components within the body of another component. Composition enables you to build complex components from simpler building-block components. Composing apps and components from a set of smaller components makes code more reusable and maintainable.
Let’s look at a simple app. The components are in the example namespace. The markup is contrived because we want to illustrate the concepts of owner and container.
<!-- todoApp.html -->
<template>
<example-todo-wrapper>
<example-todo-item item-name="Milk"></example-todo-item>
<example-todo-item item-name="Bread"></example-todo-item>
</example-todo-wrapper>
</template>
Owner
The owner is the component that owns the template. In this example, the owner is the example-todo-app component. The owner controls all the composed (child) components that it contains. An owner can:
- Set public properties on composed components
- Call public methods on composed components
- Listen for events dispatched by composed components
Container
A container contains other components but itself is contained within the owner component. In this example, example-todo-wrapper is a container. A container is less powerful than the owner. A container can:
- Read, but not change, public properties in contained components
- Call public methods on composed components
- Listen for some, but not necessarily all, events bubbled up by components that it contains. See Configure Event Propagation.
Parent and child
When a component contains another component, which, in turn, can contain other components, we have a containment hierarchy. In the documentation, we sometimes talk about parent and child components. A parent component contains a child component. A parent component can be the owner or a container.
# Set a Property on a Child Component
To communicate down the containment hierarchy, an owner can set a property on a child component. An attribute in HTML turns into a property assignment in JavaScript.
Let’s look at how the owner, example-todo-app, sets public properties on the two instances of example-todo-item.
In the playground, select todoItem.js. The @api decorator exposes the itemName field as a public property.
Select todoApp.html. To set the public itemName property, it sets the item-name attribute on each example-todo-item component. Change Milk to Hummus to see an item name change.
Property names in JavaScript are in camel case while HTML attribute names are in kebab case (dash-separated) to match HTML standards. In todoApp.html, the item-name attribute in markup maps to the itemName JavaScript property of todoItem.js.
Tip
This example uses the static values Milk and Bread. A real-world component would typically use a for:each iteration over a collection computed in the owner’s JavaScript file, todoApp.js.
Let's look at another example of composition and data binding. The example-contact-tile component exposes a contact public property (@api contact in contactTile.js). The parent component, example-composition-basics, sets the value of contact.
The contact public property in example-contact-tile is initialized based on the contact property value of its owner. The data binding for property values is one way. If the property value changes in the owner, the updated value propagates to the child. For example, in compositionBasics.js, change Name: 'Amy Taylor', to Name: 'Stacy Adams',. The change is reflected in the example-contact-tile child component.
Example
See the Composition recipes in the Lightning Web Components recipes app.
# Send an Event from a Child to an Owner
When a component decorates a field with @api to expose it as a public property, it should set the value only when it initializes the field, if at all. After the field is initialized, only the owner component should set the value.
To trigger a mutation for a property value provided by an owner component, a child component can send an event to the owner. The owner can change the property value, which propagates down to the child.
Example
The Lightning Web Components recipes app has a more realistic example. Click Child-to-Parent and look at the EventWithData example, which has a list of contacts. When a user clicks a contact, the component sends an event to the parent, which sends a new contact object to the child.
# Spread Properties and Event Handlers to a Child Component
To spread properties on a child component, use lwc:spread.
You don't have to manually define each attribute or property in your template like this.
<!-- Replace this with lwc:spread -->
<my-component
name={config.name}
age={config.age}
country={config.country}></my-component>
lwc:spread makes it easier to work with large config or props objects. You can dynamically pass multiple attributes or properties. However, it only spreads top-level properties. Nested objects won't be automatically spread.
In the parent component, childProps uses an object with keys as the property names and values as property values.
The child component uses the properties in the template. It exposes the properties to the parent component using the @api decorator.
lwc:spread is always applied last; it takes precedence to any properties that are declared in the template directly. Only one lwc:spread instance can be used on a directive.
<!-- app.html -->
<template>
<c-child name="lwc" lwc:spread={childProps}></c-child>
</template>
In this example, c-child passes name as Lightning Web Components even though the parent component is passing in name="lwc".
// app.js
import { LightningElement } from 'lwc';
export default class extends LightningElement {
childProps = { name: "Lightning Web Components" };
}
# Reflect HTML Attributes on a Child Component
Most of the HTML attributes are reflected as properties. For example, class attribute is reflected as className property.
Let's say you're passing in properties to a child component.
<!-- app.html -->
<template>
<c-child lwc:spread={spanProps}></c-child>
</template>
In this example, the spanProps property causes the element to be rendered as <c-child class="spanclass" id="mySpan"></c-child>.
// app.js
import { LightningElement } from 'lwc';
export default class extends LightningElement {
spanProps = { className: 'spanclass', id: 'myspan' };
}
# Pass Props to a Lazy Loading Component
When you're lazily instantiating components with lwc:is and don't have your props defined yet, the props that you pass to the component can't be dynamic; you typically pass them in the template HTML itself.
However, the component constructor can change at runtime, which means that the new constructor might need a different set of properties than the earlier one.
With lwc:spread, <lwc:component> and other elements can accept an object that's bound as props at runtime.
<!-- app.html -->
<template>
<lwc:component lwc:is={dynamicCtor} lwc:spread={dynamicProps}></lwc:component>
</template>
Import the child component dynamically to lazy load the component.
// app.js
import { LightningElement, api } from 'lwc';
import Child from 'c/child';
export default class MyApp extends LightningElement {
dynamicCtor = Child;
dynamicProps = { name: 'Dynamic' };
}
To add an event listener dynamically to a child component, we recommend using the lwc:on directive.
In this example, the parent component (spreadOnEvent.html) uses lwc:spread to pass properties to the child component (spreadOnEventChild.html). The parent component uses lwc:on to attach handler for custom event on the child. The child component dispatches events both when it's inserted into the DOM and when a button is clicked, sending messages and updated properties back to the parent. The parent component updates its display based on the detail property that's received from the child component's events.
# Data Flow Considerations
To prevent code complexity and unexpected side effects, data should flow in one direction, from owner to child.
# Primitive Property Values
We recommend using primitive data types for public properties instead of using object data types. Slice complex data structures in a higher-level component and pass the primitive values to the component descendants.
Primitive values require specific @api properties that clearly define the data shape. Accepting an object or an array requires documentation to specify the shape. If an object shape changes, consumers break.
Standard HTML elements accept only primitive values for attributes. When a standard HTML element needs a complex shape, it uses child components. For example, a table element uses tr and td elements. Only primitive types can be defined in HTML. For example, <table data={...}> isn't a value in HTML. However, you could create a table Lightning web component with a data API.
# Objects Passed to Components Are Read-Only
A non-primitive value (like an object or array) passed to a component is read-only. The component cannot change the content of the object or array. If the component tries to change the content, you see an error in the browser console.
To mutate the data, the component can make a shallow copy of the objects it wants to mutate.
More commonly, the component can send an event to the owner of the object. When the owner receives the event, it mutates the data, which is sent back down to the child.
Tip
The next example attempts to help you fully understand that objects passed to a component are read-only. The example doesn't contain best practices, instead, it shows code that doesn't work.
In the following playground, example-contact-tile-object exposes its contact field as a public property. The parent, example-composition-basics-object, defines a contact object and passes it to example-contact-tile-object. The child component can't mutate the object.
- Open the browser's JavaScript console and clear it.
- Click Update contact.name. The
contact-tile-objectcomponent tries and fails to update thenameproperty of the object that it received from its owner.
this.contact.name = 'Jennifer Wu'
The browser console displays the error: Uncaught Error: Invalid mutation: Cannot set "name" on "[object Object]". "[object Object]" is read-only. The component doesn't own the object passed to it and can't mutate it.
This is a tricky but important thing to understand. The contact object passed to example-contact-tile-object is read-only. That object is owned by the component that passed it to the child. However, the child owns its contact field and can set that field to a new value. Let's look at some code to see how this works.
- Click Update contact field. The
contact-tile-objectcomponent successfully sets a new value for thecontactfield.
this.contact = { name: 'Anup Gupta', title: 'VP of Products' };
Again, the contact-tile-object component owns its contact field, and it can assign a new value to that field.
Note
After clicking Update contact field, the child component owns the object, which means that the child can mutate the object. Therefore, if you click Update contact.name again, you may expect the component to display Jennifer Wu, but it doesn't. The value is set, and Jennifer Wu displays in the console, which means that the component did mutate the object. However, the component doesn't rerender. Why? Because LWC can't see that a property of the contact object changed. The contact field is decorated with @api, not @track. (A field can have only one decorator.) The @api decorator simply makes a field public and allows its value to be set from the outside. The @api decorator doesn't tell LWC to observe mutations like @track does.
But why does the component rerender when we assign a new value directly to the contact field? Because LWC observes all fields for mutation. If you assign a new value to a field, the component rerenders. However, if an object is assigned to a field, LWC doesn't observe the internal properties of that object. Therefore, the component doesn't update when you assign a new value to contact.name. To tell LWC to observe the internal properties of an object, decorate the field with @track. See Field Reactivity.
# Call a Method on a Child Component
To expose a public method, decorate it with @api. Public methods are part of a component’s API. To communicate down the containment hierarchy, owner and parent components can call JavaScript methods (or set properties) on child components.
Example
See the Parent-to-Child recipes in the Lightning Web Components recipes app.
# Define a Method
This example exposes isPlaying(), play(), and pause() methods in a example-video-player component by adding the @api decorator to the methods. A parent component that contains example-video-player can call these methods. Here’s the JavaScript file.
// videoPlayer.js
import { LightningElement, api } from 'lwc';
export default class VideoPlayer extends LightningElement {
@api videoUrl;
@api
get isPlaying() {
const player = this.template.querySelector('video');
return player !== null && player.paused === false;
}
@api
play() {
const player = this.template.querySelector('video');
// the player might not be in the DOM just yet
if (player) {
player.play();
}
}
@api
pause() {
const player = this.template.querySelector('video');
if (player) {
// the player might not be in the DOM just yet
player.pause();
}
}
// private method for computed value
get videoType() {
return 'video/' + this.videoUrl.split('.').pop();
}
}
videoUrl is a public reactive property. The @api decorator can be used to define a public reactive property, and a public JavaScript method, on a component. Public reactive properties are another part of the component’s public API.
Note
To access elements that the template owns, the code uses the template property.
Now, let’s look at the HTML file where the video element is defined.
<!-- videoPlayer.html -->
<template>
<div class="fancy-border">
<video autoplay><source src={videoUrl} type={videoType} /></video>
</div>
</template>
In a real-world component, example-video-player would typically have controls to play or pause the video itself. For this example to illustrate the design of a public API, the controls are in the parent component that calls the public methods.
# Call a Method
The example-method-caller component contains example-video-player and has buttons to call the play() and pause() methods in example-video-player. Here’s the HTML.
<!-- methodCaller.html -->
<template>
<div>
<example-video-player video-url={video}></example-video-player>
<button onclick={handlePlay}>Play</button> <button onclick={handlePause}>Pause</button>
</div>
</template>
Clicking the buttons in example-method-caller plays or pauses the video in example-video-player after we wire up the handlePlay and handlePause methods in example-method-caller.
Here’s the JavaScript file for example-method-caller.
// methodCaller.js
import { LightningElement } from 'lwc';
export default class MethodCaller extends LightningElement {
video = 'https://www.w3schools.com/tags/movie.mp4';
handlePlay() {
this.template.querySelector('example-video-player').play();
}
handlePause() {
this.template.querySelector('example-video-player').pause();
}
}
The handlePlay() function in example-method-caller calls the play() method in the example-video-player element. this.template.querySelector('example-video-player') returns the example-video-player element in methodCaller.html. The this.template.querySelector() call is useful to get access to a child component so that you can call a method on the component.
The handlePause() function in example-method-caller calls the pause() method in the example-video-player element.
# Return Values
To return a value from a JavaScript method, use the return statement. For example, see the isPlaying() method in example-video-player.
@api get isPlaying() {
const player = this.template.querySelector('video');
return player !== null && player.paused === false;
}
# Method Parameters
To pass data to a JavaScript method, define one or more parameters for the method. For example, you could define the play() method to take a speed parameter that controls the video playback speed.
@api play(speed) { … }
# Pass Markup into Slots
Add a slot to a component’s HTML file so a parent component can pass markup into the component. A component can have zero or more slots.
A slot is a placeholder for markup that a parent component passes into a component’s body. Slots are part of the Web Component specification.
To define a slot in markup, use the <slot> tag, which has an optional name attribute. Other components can pass elements into a slot, which allows you to compose components dynamically.
A slot can be named or unnamed. A named slot is a <slot> element with a name attribute.
# Unnamed Slots
When a component has a slot, a container component can pass light DOM elements into the slot.
<!-- childSlot.html -->
<template>
<h2>I host other elements via slots</h2>
<slot></slot>
</template>
<!-- container.html -->
<template>
<c-child-slot>
<p>Passing some content to the slot</p>
</c-child-slot>
</template>
The browser renders the flattened tree, which is what you see on the page. The DOM passed into the slot doesn’t become part of the child’s shadow DOM; it’s part of the container’s shadow DOM.
<!-- Flattened DOM -->
<body>
<c-container>
<c-child-slot>
<h2>I host other elements via slots</h2>
<slot>
<!--
I'm not part of the c-child-slot shadow DOM
I'm part of c-container shadow DOM
-->
<p>To c-container, I'm light DOM.</p>
</slot>
</c-child-slot>
</c-container>
</body>
Note
When we use slots, even though the content appears to be rendered inside the slot element, the actual element doesn’t get moved around. Instead, a “pointer” to the original content gets inserted into the slot.
Let's look at another example. In the playground, click slotDemo.html to see an unnamed slot. The slotWrapper component passes content into the slot.
When example-slot-demo is rendered, the unnamed slot is replaced with Content from Slot Wrapper. Here’s the rendered HTML of example-slot-wrapper.
<example-slot-wrapper>
<example-slot-demo>
<h1>Content in Slot Demo</h1>
<div>
<slot><p>Content from Slot Wrapper</p></slot>
</div>
</example-slot-demo>
</example-slot-wrapper>
If a component has more than one unnamed slot, the markup passed into the body of the component is inserted into all the unnamed slots. This UI pattern is unusual. A component usually has zero or one unnamed slot.
# Named Slots
This example component has two named slots and one unnamed slot.
<!-- namedSlots.html -->
<template>
<p>First Name: <slot name="firstName">Default first name</slot></p>
<p>Last Name: <slot name="lastName">Default last name</slot></p>
<p>Description: <slot>Default description</slot></p>
</template>
You can set a dynamic value for the slot attribute of an HTML element. Here, the <span> element has a slot attribute set to the variable dynamicName.
<template>
<c-item>
<span slot={dynamicName}></span>
</c-item>
</template>
The dynamic value passed into the slot attribute is coerced to a string. For example, if you pass the number 4 to the attribute, it is converted to the string "4". If you pass in a data type that can't be converted into a string, such as a Symbol(), a TypeError is thrown.
This change doesn't impact attributes of <slot> elements. For example, you still must pass a static string into the attribute name for a <slot> element.
<template>
<slot name="staticName"></slot>
</template>
Here’s the markup for a parent component that uses example-named-slots.
<!-- slotsWrapper.html -->
<template>
<example-named-slots>
<span slot="firstName">Willy</span> <span slot="lastName">Wonka</span>
<span>Chocolatier</span>
</example-named-slots>
</template>
The example-slots-wrapper component passes:
Willyinto thefirstNameslotWonkainto thelastNameslotChocolatierinto the unnamed slot
Here’s the rendered output.
<example-named-slots>
<p>
First Name: <slot name="firstName"><span slot="firstName">Willy</span></slot>
</p>
<p>
Last Name: <slot name="lastName"><span slot="lastName">Wonka</span></slot>
</p>
<p>
Description: <slot><span>Chocolatier</span></slot>
</p>
</example-named-slots>
# Render Slots Conditionally
To render a slot conditionally, nest the slot in a <template> tag with the lwc:if, lwc:else, and/or lwc:elseif directives.
<template>
<template lwc:if={expression}>
<div class="my-class">
<slot></slot>
</div>
</template>
<template lwc:else>
<slot></slot>
</template>
</template>
The template compiler treats the conditional directives as a valid use case and it knows that the <slot> isn't rendered twice. Contrastingly, the compiler warns about duplicate slots if you use the legacy if:true and if:false directives as it's not clear if <slots> will only render once.
# Run Code on slotchange
All <slot> elements support the slotchange event. The slotchange event fires when a direct child of a node in a <slot> element changes, such as when new content is appended or deleted. Only <slot> elements support this event.
Changes within the children of the <slot> element don’t trigger a slotchange event.
This example contains a <slot> element that handles the slotchange event.
<!-- container.html -->
<template>
<slot onslotchange={handleSlotChange}></slot>
</template>
//container.js
handleSlotChange (e) {
console.log("New slotted content has been added or removed!");
}
The component example-child is passed into the slot.
<example-container>
<example-child></example-child>
<template lwc:if={addOneMore}>
<example-child></example-child>
</template>
</example-container>
The console prints the first time the component is rendered, and if the flag addOneMore is set to true.
<!-- child.html -->
<template>
<button onclick={handleClick}>Toggle Footer</button>
<template lwc:if={showFooter}>
<footer>Footer content</footer>
</template>
</template>
The slotchange event is not triggered even when showFooter is true and the footer element is appended.
# Return Nodes and Elements in Slots
To traverse the DOM tree for slotted content, use the HTMLSlotElement interface. This interface is part of the Shadow DOM API, which enables access to the named and assigned nodes.
HTMLSlotElement.assignedNodes()returns the nodes that are assigned to a slot.HTMLSlotElement.assignedElements()returns the elements that are assined to a slot.Element.prototype.assignedSlotreturns the slot an element is assigned to.
# Query Selectors
The querySelector() and querySelectorAll() methods are standard DOM APIs. querySelector() returns the first element that matches the selector. querySelectorAll() returns an array of DOM Elements.
Call these methods differently depending on whether you want to access elements the component owns or access elements passed via slots.
Important
Don’t pass an id to these query methods. When an HTML template renders, id values are transformed into globally unique values. If you use an id selector in JavaScript, it won’t match the transformed id. If you’re iterating over an array, consider adding some other attribute to the element, like a class or data-* value, and use it to select the element. LWC uses id values for accessibility only.
# Access Elements the Component Owns
To access elements rendered by a component with standard DOM APIs, use the template property to call a query method.
this.template.querySelector();
this.template.querySelectorAll();
- The order of elements is not guaranteed.
- Elements not rendered to the DOM aren’t returned in the
querySelector()result. - Don’t use ID selectors. The IDs that you define in HTML templates may be transformed into globally unique values when the template is rendered. If you use an ID selector in JavaScript, it won’t match the transformed ID.
<!-- example.html -->
<template>
<div>First <slot name="task1">Task 1</slot></div>
<div>Second <slot name="task2">Task 2</slot></div>
</template>
// example.js
import { LightningElement } from 'lwc';
export default class Example extends LightningElement {
renderedCallback() {
this.template.querySelector('div'); // <div>First</div>
this.template.querySelector('span'); // null
this.template.querySelectorAll('div'); // [<div>First</div>, <div>Second</div>]
}
}
Important
Don’t use the window or document global properties to query for DOM elements. Also, we don’t recommend using JavaScript to manipulate the DOM. It's better to use HTML directives to write declarative code. For example, use lwc:if to conditionally display an element or component instead of using appendChild and removeChild.
To locate elements in the DOM without a selector and only query elements contained in a specific template, use refs. Refs are configurable and writable, so refs defined in a component overwrite those from LightningElement.prototype.
We recommend that you use this.refs instead of this.template.querySelector() in shadow DOM or this.querySelector() in light DOM. For shadow DOM components, this.refs refers to elements inside of the shadow DOM. For light DOM components, this.refs refers to elements inside of the light DOM. If lwc:ref is not defined in the template, then this.refs returns undefined.
First, add a ref attribute to an element with the directive lwc:ref and assign it a value. To call that reference, use this.refs. In the following example, the <div> element has the attribute lwc:ref='myDiv', which this.refs references to access the <div> at runtime.
<template>
<div lwc:ref="myDiv"></div>
</template>
export default class extends LightningElement {
renderedCallback() {
console.log(this.refs.myDiv);
}
}
You must define the directive lwc:ref before calling this.refs. If you call this.refs for a nonexistent ref, it returns undefined. If the template contains duplicate lwc:ref directives, this.refs references the last <div>.
<template>
<div lwc:ref="myDiv"></div>
<div lwc:ref="myDiv"></div>
</template>
this.refs is a plain, read-only object. Trying to add, modify, or delete properties from within its component causes a runtime error. Its key is a string, and its value is a DOM element. The syntax for this.refs is the same for referencing elements in light DOM and shadow DOM.
You can't apply lwc:ref to <template> elements, or to <slot> elements in light DOM.
<template lwc:render-mode="light">
<template lwc:if={myTemplate} lwc:ref="myTemplate"></template> <!-- Not allowed -->
</template>
<template lwc:render-mode="light">
<slot lwc:ref="mySlot"></slot> <!-- Not allowed -->
</template>
If you place lwc:ref in a for:each or iterator:* loop, the template compiler throws an error.
<template for:each={items} for:item="item">
<div key={item} lwc:ref="foo"></div> <!-- Not allowed -->
</template>
this.refs refers to the most recently rendered template in a component with more than one template. When the template changes, the this.refs object will change too.
import a from './a.html';
import b from './b.html';
export default class extends LightningElement {
count = 0;
render() {
return this.count % 2 === 0 ? a : b;
}
renderedCallback() {
console.log(this.refs);
}
increment () {
this.count++;
}
}
const cmp = createElement('x-component', { is: Component });
// Logs `this.refs` for a.html
cmp.increment();
// Logs `this.refs` for b.html
cmp.increment();
// Logs `this.refs` for a.html
To conditionally define an element based on template lwc:if={boolean}>, create multiple child templates under one parent template. In this example, this.refs.toggleDarkMode refers to the element inside of whichever child template is rendered.
<template>
<template lwc:if={darkMode}>
<button lwc:ref="toggleDarkMode">Enable Light Mode</button>
</template>
<template lwc:else>
<button lwc:ref="toggleDarkMode">Enable Dark Mode</button>
</template>
</template>
# Access the Parent Element
To access the HTMLElement of a Lightning web component from within renderedCallback or another callback, use this.hostElement in your component. The hostElement property applies to shadow DOM and light DOM. Use the hostElement property to retrieve a property on the HTMLElement class.
This example returns this.hostElement in a light DOM component.
// x-light
import { LightningElement } from 'lwc';
export default class extends LightningElement {
static renderMode = 'light'; // default is 'shadow'
renderedCallback() {
console.log(this.hostElement); // logs <x-light>
}
}
In light DOM, this.template.host returns undefined. In shadow DOM, this.hostElement is interchangeable with this.template.host.
# Access Elements Passed Via Slots
A component doesn’t own DOM elements that are passed to it via slots. These DOM elements aren’t in the component’s shadow tree. To access DOM elements passed in via slots, call this.querySelector() and this.querySelectorAll(). Because the component doesn't own these elements, you don’t use this.template.querySelector() or this.template.querySelectorAll().
This example shows how to get the DOM elements passed to a child component from the child’s context. Pass the selector name, such as an element, to this.querySelector() and this.querySelectorAll(). This example passes the span element.
// namedSlots.js
import { LightningElement } from 'lwc';
export default class NamedSlots extends LightningElement {
renderedCallback() {
this.querySelector('span'); // <span>push the green button.</span>
this.querySelectorAll('span'); // [<span>push the green button</span>, <span>push the red button</span>]
}
}
# Compose Components Using Slots Versus Data
When creating components that contain other components, consider the lifecycle and construction of the component hierarchy using the declarative (slots) or data-driven approach.
# Compose Using Slots
This pattern is common for building components declaratively.
<example-parent>
<example-custom-child></example-custom-child>
<example-custom-child></example-custom-child>
</example-parent>
To support this pattern, the component author uses the slot element. Although convenient for the consumer, the component author must manage the lifecycle of the content passed through the slot element.
Example
The lightning-vertical-navigation component uses slots so consumers can build a list of links declaratively. See the source code in the base-components-recipes repo.
# Use Custom Events to Notify the Parent About Child Availability
The parent component needs to know when a child component is available for communication. On the parent component, attach an event handler on the slot element or on a div element that contains the slot element.
<!-- parent.html -->
<template>
<div onprivateitemregister={handleChildRegister}>
<!-- other markup here -->
<slot></slot>
</div>
</template>
Handle the event to notify the parent of the child component. A globally unique Id is required for the parent component to work with its child components.
handleChildRegister(event) {
// Suppress event if it’s not part of the public API
event.stopPropagation();
const item = event.detail;
const guid = item.guid;
this.privateChildrenRecord[guid] = item;
}
To dispatch the event from the child component, use the connectedCallback() method.
connectedCallback() {
const itemregister = new CustomEvent('privateitemregister', {
bubbles: true,
detail: {
callbacks: {
select: this.select,
},
guid: this.guid,
}
});
this.dispatchEvent(itemregister);
}
# Notify the Parent Component About Unregistered Child Component
To notify the parent that its child component is no longer available, we establish a two-way communication channel between the parent and child component.
- Child sends a callback to the parent during registration.
- Parent calls the child via a callback, passing another callback as an argument.
- Child invokes the callback on the parent when being unregistered.
Note
Since the component has been removed from the page, we can still invoke callbacks but we can't send an event from disconnectedCallback().
The child sends a callback to the parent using an event handler onprivateitemregister.
<!-- parent.html -->
<template>
<slot onprivateitemregister={handleChildRegister}> </slot>
</template>
Handle the event to notify the parent that the child is no longer available.
// parent.js
handleChildRegister(event) {
const item = event.detail;
const guid = item.guid;
this.privateChildrenRecord[guid] = item;
// Add a callback that
// notifies the parent when child is unregistered
item.registerDisconnectCallback(this.handleChildUnregister);
}
handleChildUnregister(event) {
const item = event.detail;
const guid = item.guid;
this.privateChildrenRecord[guid] = undefined;
}
The child component invokes the callback on the parent when being unregistered.
// child.js
connectedCallback() {
const itemregister = new CustomEvent('privateitemregister', {
bubbles: true,
detail: {
callbacks: {
registerDisconnectCallback: this.registerDisconnectCallback
},
guid: this.guid,
}
});
this.dispatchEvent(itemregister);
}
// Store the parent's callback so we can invoke later
registerDisconnectCallback(callback) {
this.disconnectFromParent = callback;
}
The child component notifies the parent that it’s no longer available.
disconnectedCallback() {
this.disconnectFromParent(this.guid);
}
Example
The lightning-vertical-navigation-item component communicates with the lightning-vertical-navigation parent component to handle link selection. See the source code in the base-components-recipes repo.
# Pass Data to Child Components
Once the registration process is complete, we can communicate data between parent and child components via the exposed callback methods.
this.privateChildrenRecord[guid].callbacks.select();
The parent component can pass data to a child component. For example, you can pass a string to a child component so that it can set a value for a setAriaLabelledBy attribute.
this.privateChildrenRecord[guid].callbacks.setAriaLabelledBy('my-custom-id');
The child component sets the string on the attribute.
ariaLabelledby;
setAriaLabelledBy(id) {
this.ariaLabelledby = id;
}
# Compose Using Data
You observed that the declarative way to compose components adds a layer of complexity for the component author. Now consider the data-driven approach. Instead of managing the lifecycle for slot content and requiring granular management between parent and child components, the component gets the changes in a reactive way when data changes.
This example composes a child component using a data-driven approach.
<template>
<div class="example-parent">
<template for:each={itemsData} for:item="itemData">
<example-child onclick={onItemSelect} id={itemData.id} key={itemData.id}>
</example-child>
</template>
</div>
</template>
To pass in data, use a JavaScript object. The child component reacts to data changes exclusively from its parent.
itemsData = [
{
label : 'custom label 1',
id : 'custom-id-1'
selected : false
},
{
label : 'custom label 2',
id : 'custom-id-2'
selected : false
}
]
A data-driven approach is recommended when you have a complex use case.
Example
The lightning-tree component uses a data-driven approach so consumers can build a list of links by passing in data. See the source code in the base-components-recipes repo.
← CSSCustom Forms →