Normally closed shadow DOM/root will not allow the access of child component’s DOM. Consider below example:
<main-component>
<parent>
<child>
<iteration>
<grand-child>
</iteration>
How can you change styles/add classes OR invoke methods directly in grand-child from main-component? Ofcourse you can have public API methods at each level but this will make it very complicated with many children. So, is there a more simpler and elegant way?
Yes! You can have simple generic registration for all elements in main-component. Then, which ever child component (in any hierarchy) has to be accessed, fire a custom registration event with appropriate details.
Grand child HTML:
<template>
<div id="main" class="main">
<div class="my-class">
This is grand-child component body
</div>
<div class="dynamic"></div>
</div>
</template>
You need a main class which wraps whole content.
Grand child JS:
export default class GrandChild extends LightningElement {
renderedCallback() {
if (!this.guid) {
this.guid = this.template.querySelector('.main').getAttribute('id');
this.dispatchEvent(
new CustomEvent('itemregister', {
bubbles: true,
composed: true,
detail: {
callbacks: {
dynamicData: this.dynamicData
},
template: this.template,
guid: this.guid,
name: 'c-grand-child'
}
})
);
}
}
dynamicData = (data) => {
this.template.querySelector('.dynamic').innerText = data;
}
}
You need to dispatch custom event with name, guid, template and callbacks. This cannot be done in connectedCallback because in its scope elements will not be created and so elements will be undefined. In renderedCallback, we will be dispatching event only once with help of checking guid.
- Template can be used to modify styles or add classes or any similar functionality.
- Callbacks will have the exposed functions references
- Name and guid are used for registartion
Child HTML
<template for:each={iteration} for:item='it'>
<div key={it.mid}>
<c-grand-child></c-grand-child>
</div>
</template>
Child JS:
export default class Child extends LightningElement {
iteration = [{ mid: '1' }, { mid: '2' }, { mid: '3' }];
}
Parent HTML
<c-child></c-child>
Main component HTML:
<lightning-button label="Add Styles" onclick={addStyles}></lightning-button>
<lightning-button label="Add Data" onclick={addData}></lightning-button>
<div onitemregister={registerItem}>
<c-parent></c-parent>
</div>
Main component JS:
privateChildren = {};
registerItem(event) {
event.stopPropagation();
const item = event.detail;
// create key for each child against its name
if (!this.privateChildren.hasOwnProperty(item.name)) this.privateChildren[item.name] = {};
// store each item against its guid
this.privateChildren[item.name][item.guid] = item;
}
addStyles() {
Object.values(this.privateChildren['c-grand-child']).forEach((element) => {
element.template.querySelector('.my-class').style.color = 'white';
element.template.querySelector('.my-class').style.backgroundColor = 'blue';
});
}
addData() {
Object.values(this.privateChildren['c-grand-child']).forEach((element, index) => {
element.callbacks.dynamicData('Changing dynamic data => ' + index);
});
}
Here, you are using registerItem generic method to register all needed child elements with key as its names. Later you can use the names to access the callbacks and template.
Screenshot before:

Screenshot After:

Hi Sasank,
We have a parent (search page) and a child component (combobox result). We are trying to get the focused div of the child component to close the result based on the focus. Can you please provide your inputs? Thanks.
Shankar
LikeLike