I'm working on a Lightning Web Component form where child components contain input fields. These components can be expanded to see all the fields, or collapsed to save space. So far, so good.
The challenge comes when I want to validate the inputs. I want to validate all the fields, whether they're displayed or collapsed at the time of validation. But they technically aren't rendered if they're collapsed -- so they aren't returned when using this.template.querySelectorAll('lightning-input')
.
My first thought was to force the components to expand temporarily during validation, but there seems to be a timing issue where the change to the expandAll
property doesn't propagate down to the children in time for querySelectorAll
to be able to register them.
What would be the best way to defer execution of querySelectorAll
until propagation is complete? Or is there another solution to this that I haven't thought of?
<!-- testParent.html -->
<template>
<div class="slds-box slds-theme_default slds-p-around_small">
<lightning-button
label={expandCollapseButtonLabel}
onclick={handleExpandCollapse}>
</lightning-button>
<lightning-button
label="Validate All"
onclick={validateAll}>
</lightning-button>
<div>
All valid? {valid}
</div>
<template for:each={contacts} for:item="contact" for:index="idx">
<c-test-child
key={contact.id}
index={idx}
first-name={contact.firstName}
last-name={contact.lastName}
expanded={expandAll}
oninputchange={handleInputChange}>
</c-test-child>
</template>
</div>
</template>
// testParent.js
import { LightningElement, track } from 'lwc';
export default class testParent extends LightningElement {
@track contacts = [{id: 1, firstName: 'John', lastName: 'Doe'},
{id: 2, firstName: null, lastName: null}];
@track expandAll = true;
@track valid;
handleInputChange(e) {
this.contacts[e.detail.index][e.detail.variable] = e.detail.value;
}
handleExpandCollapse() {
this.expandAll = !this.expandAll;
}
validateAll() {
// Capture existing state to restore later
let tempExpandAll = this.expandAll;
// Temporarily expand all the child components (doesn't propagate in time)
this.expandAll = true;
this.valid = [...this.template.querySelectorAll('c-test-child')]
.reduce((allValid, childCmp) => {
if (childCmp.validateInputs()) {
return allValid;
}
return 'no';
}, 'yes');
this.expandAll = tempExpandAll; // Put the state back where you found it
}
get expandCollapseButtonLabel() {
return this.expandAll ? 'Collapse All' : 'Expand All';
}
}
<!-- testChild.html -->
<template>
<div class="slds-box slds-p-around_small">
<div>
Contact: {firstName} {lastName}
</div>
<template if:true={expanded}>
<lightning-input
data-field="firstName"
type="text"
label="First Name"
value={firstName}
required max-length="15"
onchange={handleChange}>
</lightning-input>
<lightning-input
data-field="lastName"
type="text"
label="Last Name"
value={lastName}
required
max-length="15"
onchange={handleChange}>
</lightning-input>
</template>
</div>
</template>
import { LightningElement, api } from 'lwc';
export default class testChild extends LightningElement {
@api index;
@api firstName;
@api lastName;
@api expanded;
@api
validateInputs() {
return [...this.template.querySelectorAll('lightning-input')]
.reduce((allValid, inputCmp) => {
inputCmp.reportValidity();
if (inputCmp.checkValidity()) {
return allValid;
}
return false;
}, true);
}
handleChange(e) {
let changeDetail = {index: this.index,
variable: e.target.dataset.field,
value: e.target.value};
this.dispatchEvent(new CustomEvent('inputchange', {detail: changeDetail}));
}
}
@api
methodsshow/hide
and maintaining this hidden state in them (e.g. wrap all html in a<div if:true={show}></div>
). Then the components will always be returned by parent'squerySelectorAll
but will visually not be there.querySelectorAll('c-test-child')
is actually working fine in my example. What's not working is the child'squerySelectorAll('lightning-input')
, which is what you ultimately need to be able to access the inputs' validity properties.