There may be a case when we need to add some validation criteria to user input to keep our information database more efficient. Hence, to prevent our database from receiving invalid inputs. So, I will share how to add custom validation on lightning input in LWC. This can be done with the help of a third-party library, Numeral.js.
To understand how we can use third-party libraries in LWC. Go here.
Step 1: Create a Lightning web component with the name inputValidation.
Drag and drop the LWC component on the page.
To learn more about how to create the LWC component, click here.
Step 2: In inputvalidation.html, add an input box and a combo box. Display the input
response as shown in the image below:
Step 3: Validation Criteria:
Allow users to
Enter a negative number.
Enter numbers up to 16 digits.
Enter a special character as per the user locale to denote the decimal point. (eg “ . “, “, “ or “ ' “)
Prevent users to
Enter alphabets.
Enter more than 16 characters.
Enter special characters except (-) at the beginning to represent negative numbers.
Additional features:
Represent numeric values according to the selected locale.
Represent rounded values of large numbers.
On saving the number (without decimal part) by clicking out of the box, the system automatically adds the decimal part.
Step 4: Represent numeric values according to the selected locale.
For example, the number ‘1234567’ will appear as ‘12,34,567’ for an English(India) locale,
whereas ‘12.34.567’ for a French(Belgium) locale.
Step 5: Represent rounded values of large numbers.
For example, the number ‘123456789’ will appear as ‘123.46 M’.
Step 6: Input Testing
If we enter the following the number validity must turn false:
A hyphen (-) in between the number.
The number is greater than 16 digits.
Decimals more than allowed decimal places.
Step 7: Let's code the functionalities now :
inputValidation.html
<template>
<lightning-card>
<div class="slds-p-vertical_x-small slds-grid slds-grid_vertical-align-center">
<img src={image} style="max-width: 6%;">
<h1 style="font-size: 30px;margin-top: 15px;margin-left: 10px;">
<b>{label.PAGE_TITLE}</b>
</h1>
</div>
<div>
<header>
<table>
<thead>
<tr>
<th class= "head">
<div onkeydown={handleInput}>
<lightning-input label="Input" oncommit={saveResult}
style="margin-left: 17px; width: 90%;">
</lightning-input>
</div>
</th>
<th class="head">
<lightning-combobox label="Decimal Places" value=""
placeholder="Select an option" options={options} onchange=
{setDecimalPlaces} style="margin-right: 17px; width: 90%;">
</lightning-combobox>
</th>
</tr>
</thead>
</table>
</header>
</div>
<table>
<tbody>
<tr>
<td style="padding-left: 20px; padding-top: 10px;">
<table>
<tbody>
<tr>
<td width="20%"><b>{label.NUMBER_VALIDITY}: </b></td>
<td>{validity}</td>
</tr>
<tr>
<td width="20%"><b>{label.CHAR_CODE}: </b></td>
<td>{charCode}</td>
</tr>
<tr>
<td width="20%"><b>{label.CURSOR_POSITION}: </b></td>
<td>{cursorPosition}</td>
</tr>
<tr>
<td width="20%"><b>{label.DECIMAL_PLACES}: </b></td>
<td>{noOfDecimalPlaces}</td>
</tr>
<tr>
<td width="20%"><b>{label.LOCALE_LABEL}: </b></td>
<td>{locales}</td>
</tr>
<tr>
<td width="20%"><b>{label.INPUT_VALUE}: </b></td>
<td>{value}</td>
</tr>
<tr>
<td width="20%"><b>{label.FORMATTED_VALUE}: </b></td>
<td>{formattedNumber}</td>
</tr>
<tr>
<td width="20%"><b>{label.ROUND_OFF}: </b></td>
<td>{roundOff}</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</lightning-card>
</template>
inputValidation.js
import {LightningElement} from 'lwc';
import {loadScript} from 'lightning/platformResourceLoader';
import PackageResources from '@salesforce/resourceUrl/PackageResources';
import Image from '@salesforce/resourceUrl/Image';
import locale from '@salesforce/i18n/locale';
import {ShowToastEvent} from 'lightning/platformShowToastEvent';
import {
formatOutputNumber,
isArrowKey,
isNumberValid,
getValue,
} from 'c/numberUtil';
import PAGE_TITLE from '@salesforce/label/c.PAGE_TITLE';
import NUMBER_VALIDITY from '@salesforce/label/c.NUMBER_VALIDITY';
import CHAR_CODE from '@salesforce/label/c.CHAR_CODE';
import CURSOR_POSITION from '@salesforce/label/c.CURSOR_POSITION';
import DECIMAL_PLACES from '@salesforce/label/c.DECIMAL_PLACES';
import LOCALE_LABEL from '@salesforce/label/c.LOCALE_LABEL';
import INPUT_LABEL from '@salesforce/label/c.INPUT_VALUE';
import FORMATTED_VALUE from '@salesforce/label/c.FORMATTED_VALUE';
import ROUND_OFF from '@salesforce/label/c.ROUND_OFF';
const DECIMAL_CHAR_CODE = 46;
const COMMA_CHAR_CODE = 44;
export default class Test extends LightningElement {
scriptLoaded = false;
decimalDelimiter;
thousandDelimeter;
decimalDelimiterCharCode;
noOfDecimalPlaces = 0;
cursorPosition;
value;
charCode;
validity;
formattedNumber;
roundOff;
roundOffDigit = 2;
image = Image + '/logo.jpg';
locales = locale;
label = {
PAGE_TITLE,
NUMBER_VALIDITY,
CHAR_CODE,
CURSOR_POSITION,
DECIMAL_PLACES,
LOCALE_LABEL,
INPUT_LABEL
FORMATTED_VALUE
ROUND_OFF
}
renderedCallback() {
if (!this.scriptLoaded) {
this.loadScript();
}
}
loadScript() {
console.log(Promise.all([
loadScript(this, PackageResources + '/numeral/numeral206.js'),
loadScript(this, PackageResources + '/numeral/locales.js')
]));
Promise.all([
loadScript(this, PackageResources + '/numeral/numeral206.js'),
loadScript(this, PackageResources + '/numeral/locales.js')
])
.then((response) => {
this.initNumeralHelpers();
this.scriptLoaded = true;
})
.catch(error => {
const event = new ShowToastEvent({
title: 'error',
message:'LoadScrip is not loaded. '+error,
});
this.dispatchEvent(event);
});
}
get options() {
return [
{label:'1', value:'1'},
{label:'2', value:'2'},
{label:'3', value:'3'},
{label:'4', value:'4'},
];
}
initNumeralHelpers() {
console.log("initNumeralHelper");
if (numeral.locales[locale.replace("-","_").toLowerCase()]) {
numeral.locale(locale.replace("-","_").toLowerCase());
}
else {
numeral.locale('en_us');
}
this.decimalDelimiter = numeral.localeData(numeral.locale()).delimiters["decimal"];
this.thousandDelimeter =
numeral.localeData(numeral.locale()).delimiters["thousands"];
if (this.decimalDelimiter == '.') {
this.decimalDelimiterCharCode = DECIMAL_CHAR_CODE;
}
else if (this.decimalDelimiter == ',') {
this.decimalDelimiterCharCode = COMMA_CHAR_CODE;
}
}
setDecimalPlaces(event) {
this.noOfDecimalPlaces = event.target.value;
}
handleInput(event) {
this.charCode = event.which ? event.which : event.keyCode;
this.cursorPosition = event.target.selectionStart;
this.value = getValue(event.target.value, this.cursorPosition, event.key);
this.validity = isArrowKey(this.charCode)
? true
: isNumberValid(
this.charCode,
this.value,
this.cursorPosition,
event.target.selectionEnd,
event.key,
this.decimalDelimiter,
this.thousandDelimeter,
this.decimalDelimiterCharCode,
this.noOfDecimalPlaces
);
if(!this.validity) {
event.preventDefault();
}
}
saveResult(event) {
this.formattedNumber = this.handleFormat(event.target.value);
this.roundOff = formatOutputNumber(event.target.value, this.roundOffDigit);
}
handleFormat(value) {
let decimalValue = '';
if (value != '' && value != null) {
let decimalPlaces = parseInt(this.noOfDecimalPlaces);
decimalValue = this.decimalValue(value, decimalPlaces);
}
return decimalValue;
}
decimalValue(value, decimalPlaces) {
let decimalFormat = '0,0.';
for (let i = 1; i <= decimalPlaces; i++) {
decimalFormat = decimalFormat + '0';
}
return numeral(value).format(decimalFormat);
}
}
inputValidation.js-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>55.0</apiVersion>
<isExposed>true</isExposed>
<targets>
<target>lightning__AppPage</target>
<target>lightning__HomePage</target>
<target>lightning__RecordPage</target>
</targets>
</LightningComponentBundle>
numberUtil.js
const VALID_NO_DIGITS = 16;
const HYPHEN_CHAR_CODE = 45;
const BACKSPACE_CHAR_CODE = 8;
const MILLION = 1000000;
const BILLION = 1000000000;
const TRILLION = 1000000000000;
const isArrowKey = (charcode) => {
return (charcode >= 37 && charcode <= 40);
}
const isNumberValid = (
charCode,
value,
selectionStart,
selectionEnd,
key,
decimalDelimiter,
thousandDelimeter,
decimalDelimiterCharCode,
numberOfDecimals
) => {
let isValid = false;
let cursorPosition = charCode == HYPHEN_CHAR_CODE
? selectionStart
: getCursorPostion(
selectionStart,
value,
thousandDelimeter,
key
);
let decimalPlaces = parseInt(numberOfDecimals);
value = getValue(value, selectionStart, key);
if (parseInt(decimalPlaces) > 0) {
isValid =
isDecimal(
value,
charCode,
cursorPosition,
parseInt(decimalPlaces),
decimalDelimiter,
thousandDelimeter,
decimalDelimiterCharCode
);
} else {
isValid = isInteger(selectionStart, selectionEnd, value, charCode, cursorPosition);
}
return isValid;
}
const getCursorPostion = (selectionStart, value, thousandDelimeter, key) => {
value = getValue(value, selectionStart, key).substring(0, selectionStart+1);
let regex = thousandDelimeter.replace('///g', '');
value = value.replace(new RegExp(regex, 'g'), '');
console.log("Position: "+value.length+1);
return value.length + 1;
}
const getValue = (value, selectionStart, key) => {
return (
value.substring(0, selectionStart)
+ key
+ value.substring(selectionStart, value.length+1)
);
}
const isDecimal = (
value,
charCode,
cursorPosition,
decimalPlaces,
decimalDelimiter,
thousandDelimeter,
decimalDelimiterCharCode,
) => {
return (
validateNegativeSign(cursorPosition, value, charCode)
&& (
hasAllNumbers(charCode)
|| hasDecimal(value, charCode, decimalDelimiter, decimalDelimiterCharCode)
)
&& validateNumberOfDigitsForDecimal(
cursorPosition,
value,
charCode,
decimalPlaces,
decimalDelimiter,
thousandDelimeter,
decimalDelimiterCharCode
)
);
}
const hasDecimal = (value, charCode, decimalDelimiter, decimalDelimiterCharCode) => {
return (
value.indexOf(decimalDelimiter) >= 0
&& charCode == decimalDelimiterCharCode
&& (value.indexOf(decimalDelimiter) === value.lastIndexOf(decimalDelimiter))
);
}
const validateNumberOfDigitsForDecimal = (
cursorPosition,
value,
charCode,
noOfDecimalPlaces,
decimalDelimiter,
thousandDelimeter,
decimalDelimiterCharCode
) => {
let regex = '\\' + thousandDelimeter.replace('///g','');
let regexValue = value.replace(new RegExp(regex,'g'),'');
let beforeDecimal = regexValue.substring(0, regexValue.indexOf(decimalDelimiter)).length;
let afterDecimal = regexValue.substring(regexValue.indexOf(decimalDelimiter)+1).length;
let decimalIndex = regexValue.indexOf(decimalDelimiter);
let validDigits = true;
if(decimalIndex >= 0) {
if(beforeDecimal > VALID_NO_DIGITS && cursorPosition <= decimalIndex &&
charCode != BACKSPACE_CHAR_CODE
) {
validDigits = false;
}
else if(afterDecimal > noOfDecimalPlaces && cursorPosition >= decimalIndex &&
charCode != BACKSPACE_CHAR_CODE
) {
validDigits = false;
}
}
else if(decimalIndex < 0) {
if(value.length > VALID_NO_DIGITS && charCode != decimalDelimiterCharCode &&
charCode != BACKSPACE_CHAR_CODE
) {
validDigits = false;
}
}
//If whole value is selected
if (value.selectionStart == 0 && value.selectionEnd == value.length && !validDigits) {
value = '';
validDigits = true;
}
return validDigits;
}
const isInteger = (selectionStart, selectionEnd, value, charCode, cursorPosition) => {
return (
validateNegativeSign(cursorPosition, value, charCode)
&& hasAllNumbers(charCode)
&& hasValidNumberOfDigits(selectionStart, selectionEnd, value)
);
}
const validateNegativeSign = (cursorPosition, value, charCode) => {
return (
(
(
charCode == HYPHEN_CHAR_CODE && cursorPosition == 0 ) ||
charCode != HYPHEN_CHAR_CODE
)
&& value.indexOf('-') < 1
&& value.indexOf('-') === value.lastIndexOf('-')
);
}
const hasAllNumbers = (charCode) => {
return !(
(charCode > 31 || charCode == 13)
&& (charCode < 48 || charCode > 57)
&& charCode != HYPHEN_CHAR_CODE
);
}
const hasValidNumberOfDigits = (selectionStart, selectionEnd, value) => {
return !(
(value.replace(/\D/g, '').length + 1 > VALID_NO_DIGITS)
&&
!(
value.replace(/\D/g, '').length == VALID_NO_DIGITS
&& selectionStart == 0
&& selectionEnd == value.length
)
);
}
const formatOutputNumber = (value, roundOffDigits) => {
if (value && value != '') {
if (value >= TRILLION) {
value = formatTrillionValue(value, roundOffDigits);
}
else if (value >= BILLION) {
value = formatBillionValue(value, roundOffDigits);
}
else if (value >= MILLION) {
value = formatMillionValue(value, roundOffDigits);
}
}
return value;
}
const formatTrillionValue = (value, roundOffDigits) => {
value = parseFloat(value/TRILLION).toFixed(roundOffDigits);
value = value + 'T';
return value;
}
const formatBillionValue = (value, roundOffDigits) => {
value = parseFloat(value/BILLION).toFixed(roundOffDigits);
value = value + 'B';
return value;
}
const formatMillionValue = (value, roundOffDigits) => {
value = parseFloat(value/MILLION).toFixed(roundOffDigits);
value = value + 'M';
return value;
}
export {
isInteger,
validateNegativeSign,
isArrowKey,
getValue,
getCursorPostion,
isDecimal,
isNumberValid,
formatOutputNumber,
}
numberUtil.js-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>55.0</apiVersion>
<isExposed>false</isExposed>
</LightningComponentBundle>
Note:
For ASCII CODE-
HYPHEN_CODE : 45
DECIMAL_CHAR_CODE: 46
COMMA_CHAR_CODE: 44
For UNICODE -
HYPHEN_CODE : 189
DECIMAL_CHAR_CODE: 190
COMMA_CHAR_CODE: 188
To know how to import an external javascript library in LWC.
Click here.
Happy!! To help you add to your knowledge. You can leave a comment to help me understand how the blog helped you. If you need further assistance, please leave a comment or contact us. You can click on "Reach us" on the website and share the issue with me.
Blog Credit:
A. Kumar
Salesforce Developer
Avenoir Technologies Pvt. Ltd.
Reach us: team@avenoir.ai