3

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

See live demo in the Lightning Web Components Playground

  • The way i'd solve it is by making child components have an @api methods show/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's querySelectorAll but will visually not be there. – zaitsman Nov 22 '19 at 1:00
  • As I understand it, the parent's querySelectorAll('c-test-child') is actually working fine in my example. What's not working is the child's querySelectorAll('lightning-input'), which is what you ultimately need to be able to access the inputs' validity properties. – Matthew Souther Nov 22 '19 at 1:22
3

As you are using <template if:true={expanded}>, the body of this template will be removed from DOM when the condition becomes false. So, you can use a div with hide or show as below:

<div class={wrapClasses}>
    <lightning-input>....</lightning-input>
    <lightning-input>....</lightning-input>
</div>

JS:

get wrapClasses() {
    return this.expanded ? 'slds-show' : 'slds-hide';
}

playground link for this.

Note: you can also validate by using <template if:true={expanded}>, but you should get and store all the input values in a variable. The biggest disadvantage with this approach is you cannot use inputCmp.reportValidity(); as the elements are not there in DOM and so you should be writing the validations manually in JS.

3

I have made some code changes.
Now On click of 'Validate All', box also expands as well as validation gets performed.

Check this Playground link - Link

Hope this is useful & your problem is solved.

  • This is very interesting, and I can see that the timing issue appears to be resolved in your playground link. Putting this.expandAll = true; inside the reduce function seems a little odd, since that action will be repeated for every iteration (and should only need to be done once). For the sake of understanding, can you elaborate on why this makes a difference? – Matthew Souther Nov 22 '19 at 17:37
  • @MatthewSouther Please do upvote/select as best answer if this has helped you. Thank you. – Mohit Kulkarni Nov 22 '19 at 17:39
  • @MatthewSouther Timing issue is resolved here because hiding or showing is now applied to div tag and not on template tag like in your original example. This is just an extension of the answer you already accepted. But this is still a good solution and I upvoted it. – javanoob Nov 22 '19 at 21:29
  • Thank you @javanoob – Mohit Kulkarni Nov 23 '19 at 12:35

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.