Composition

You can add components within the body of another component. Composition enables you to build complex components from simpler building-block components.

It’s useful to compose apps and components from a set of smaller components to make the code more reusable and maintainable.

Let’s look at a simple app. The components are in the example namespace. For information about naming, see Component Bundles.

The markup is contrived because we want to illustrate the concepts of owner and container. In a real app, the number of example-todo-item instances would be variable and populated dynamically in a for:each loop.

<!-- 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 components that it contains. An owner can:

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:

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 from the owner to a child, an owner can set a property or attribute 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.

Look at todoItem.js. The @api decorator marks itemName as a public property.

Look at 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. This example has a slightly more complex data structure.

When you add a component in markup, you can initialize public property values in the component based on property values of the owner component. The data binding for property values is one-way. If the property value changes in the owner component, the updated value propagates to the child component.

The child component must treat property values passed from the owner component as read-only. If the child component tries to change a value passed from an owner component, you see an error in the browser console.

To trigger a mutation for the property value provided by the owner component, the child component can send an event to the parent. If the parent owns the data, the parent can change the property value, which propagates down to the child component via the one-way data binding.

Example

See the Composition recipes in the Lightning Web Components recipes app.

Primitive Property Values

We recommend using primitive data types for 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.

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.

Unnamed Slots

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>

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:

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>

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 if:true={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 if:true={showFooter}>
       <footer>Footer content</footer>
   </template>
 </template>

The slotchange event is not triggered even when showFooter is true and the footer element is appended.

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 may be 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.

Access Elements the Component Owns

To access elements rendered by your component, use the template property to call a query method.

this.template.querySelector();
this.template.querySelectorAll();
<!-- 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.

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

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.

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);
}

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 setArialLabelledBy attribute.

this.privateChildrenRecord[guid].callbacks.setAriaLabelledBy('my-custom-id');

The child component sets the string on the attribute.

@track 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.

Shadow DOM

The elements in each Lightning web component 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.

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. A polyfill is code that allows a feature to work in a web browser.

To better understand how to work with the shadow tree, let’s look at some markup. This markup contains two Lightning web components: example-todo-app and example-todo-item. The shadow root defines the boundary between the DOM and the shadow tree. This boundary is called the shadow boundary.

<example-todo-app>
  #shadow-root
    <div>
        <p>Your To Do List</p>
    </div>
    <example-todo-item>
      #shadow-root
        <div>
            <p>Go to the store</p>
        </div>
    </example-todo-item>
</example-todo-app>

Note

The shadow root isn’t an element, it’s a document fragment.

Let’s look at how to work with the shadow tree in each of these areas.

CSS

CSS styles defined in a parent component don’t leak into a child. In our example, a p style defined in the todoApp.css style sheet 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.