1

In my LWC, I need an input to be required when a specific option in a radio-group is selected.

When the user clicks on the into the input directly below the Another Amount radio button, that option is automatically selected and the input is now required:

enter image description here

When I select a different option, say Amount Due Today, the input still displays the validation message, even though my code clears it:

enter image description here

When I click the Remaining Balance option, the validation message finally disappears:

enter image description here

There seems to be a lag when that validation message gets removed. Am I clearing the custom validation message incorrectly? Is there some other way to immediately clear the custom validation?

My code is in the following repo: https://github.com/mikesobczak/LWC-Custom-Validation-Issue

And here:

payment.html

<template>
    
    <div class="paymentArea">
    
        <div class="slds-var-m-bottom_medium slds-card__header-title">{labels.component.paymentHeader}</div>

            <div class="slds-grid slds-gutters slds-var-m-bottom_small">
                <div class="slds-col slds-size_1-of-3 slds-nowrap">
                    {labels.component.amountToPay}
                </div>

                <div class="slds-col">
                    <lightning-radio-group
                        name="amountToPay"
                        label={labels.component.amountToPay}
                        variant="label-hidden"
                        options={amountToPayOptions}
                        value={amountToPaySelection}
                        type="radio"
                        onchange={handleAmountToPayChange}
                        class="slds-var-m-bottom_small requiredInput"
                        required>

                    </lightning-radio-group>

                    <lightning-input 
                        type="number" 
                        name="anotherAmount" 
                        label="Another Amount" 
                        variant="label-hidden" 
                        value={anotherAmount} 
                        min="1"
                        onfocus={handleAnotherAmountFocus}
                        onchange={handleAnotherAmountChange}
                        formatter="currency"
                        class="inputAmount anotherAmount requiredInput"
                        required={isAnotherAmountRequired}
                        >
                    </lightning-input>

                </div>

            </div>

    </div>

   </template>

payment.js

    import { LightningElement } from 'lwc';

    const ANOTHER_AMOUNT_OPTION = 'anotherAmount';
    const AMOUNT_DUE_TODAY_OPTION = 'amountDueToday';

    export default class Payment extends LightningElement {

    labels = {
        toast: {
            processingErrorTitle: 'Processing Error'
            , validationErrorTitle: 'Input Validation Error'
            , validationErrorMessage: 'Please provide values for all required fields'
        },
        component: {
            paymentHeader: 'Payment'
            , amountToPay: 'Amount to Pay'
            , amountDueToday: 'Amount Due Today '
            , remainingBalance: 'Remaining Balance '
            , anotherAmount: 'Another Amount'
        }
    }

    amountToPaySelection;
    anotherAmount;

    connectedCallback() {
        this.amountToPaySelection = AMOUNT_DUE_TODAY_OPTION
    }

    handleAmountToPayChange(event) {

        console.log('payment', 'handleAmountToPayChange()', event.detail.value);

        this.amountToPaySelection = event.detail.value;

        if(this.amountToPaySelection !== ANOTHER_AMOUNT_OPTION) {
            this.anotherAmount = undefined;

            let anotherAmountCmp = this.template.querySelector(".anotherAmount");
            anotherAmountCmp.setCustomValidity("");
            anotherAmountCmp.reportValidity();
            anotherAmountCmp.checkValidity();
        }
    }

    handleAnotherAmountChange(event) {
        const value = event.detail.value;
        this.anotherAmount = value;
    }

    handleAnotherAmountFocus(event) {
        this.amountToPaySelection = ANOTHER_AMOUNT_OPTION;
    }

    get isAnotherAmountRequired() {
        console.log('isAnotherAmountRequired()', this.amountToPaySelection);
        if(this.amountToPaySelection === ANOTHER_AMOUNT_OPTION) {
            console.log('isAnotherAmountRequired() = true');
            return true;
        }
        console.log('isAnotherAmountRequired() = false');

        return false;
    }

    get amountToPayOptions() {
        const theOptions = [];
        theOptions.push({ label: this.labels.component.amountDueToday, value: AMOUNT_DUE_TODAY_OPTION});

        let balanceLabel = this.labels.component.remainingBalance;

        theOptions.push({ label: balanceLabel, value: 'remainingBalance'});
        
        theOptions.push({ label: this.labels.component.anotherAmount, value: ANOTHER_AMOUNT_OPTION });

        return theOptions;
    }

}

2 Answers 2

1

The DOM lags behind the VDOM (Virtual DOM), so you do need to wait a tick before you reset the validity. No need to go crazy here, though, just do the following:

async handleAmountToPayChange(event) {
    this.amountToPaySelection = event.detail.value;
    const anotherAmountField = this.template.querySelector('.anotherAmount');
    if(this.amountToPaySelection !== ANOTHER_AMOUNT_OPTION) {
        this.anotherAmount = undefined;
        await Promise.resolve();
        anotherAmountField.reportValidity();
    } else {
      anotherAmountField.focus();
    }
}

Demo.

Also, I'd advise against the getter, just set the property directly. That's what properties are meant for.

4
  • 1
    Excellent answer, this indeed looks cleaner! Thanks for sharing the async await way of handling this situation. :)
    – Raul
    Apr 14 at 22:51
  • I have an issue with your example. When I put the cursor into the anotherAmount field and click elsewhere, I don't see an error message appear. Apr 15 at 13:22
  • My bad, I thought you had removed the required property from the input. When I add it back, it works. Apr 15 at 13:36
  • 1
    @MichaelSobczak I did edit it, it was missing a line of code on focus. It should befixed now.
    – sfdcfox
    Apr 15 at 14:15
1

This looks like the issue with conditional rendering because the report validity finishes before the template renders.

You may use a timeout with 0 milliseconds to execute your code after template renders.

Also you can set your message conditionally to setCustomValidity function, modifying the function as below seems to be working fine:

handleAmountToPayChange(event) {
    console.log("payment", "handleAmountToPayChange()", event.detail.value);

    this.amountToPaySelection = event.detail.value;

    this.anotherAmount = undefined;
    let message = "";
    if (this.amountToPaySelection === ANOTHER_AMOUNT_OPTION) {
        message = "Complete this field.";
    }

    // eslint-disable-next-line @lwc/lwc/no-async-operation
    setTimeout(() => {
        const anotherAmountCmp =
            this.template.querySelector(".anotherAmount");
        anotherAmountCmp.setCustomValidity(message);
        anotherAmountCmp.reportValidity();
    }, 0);
}

Code in action: webcomponents.dev.

Note: webcomponents.dev/create/lwc is handy for sharing LWC code. :)

1

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.