Angular Syncfusion Custom Validation for ejs child grid column Not Mapping Properly After Save

I am using Syncfusion EJS Grid in Angular with custom validation for add/update operations . Before clicking the Save button, validation works correctly, highlighting errors as expected. However, after clicking Save, the validation does not map properly—either it resets or does not trigger correctly on certain fields

After clicking save button and edit any row it is showing older error msg.Even after deleteing cache it is displaying the older validation msg,even after removing the line from the code.

here is the code provided ,please let us know the changes required.

HTML


<ejs-grid #VariationQSORgrid id="VariationQSORgrid" [dataSource]='VariationQSORDatasource'

                                    [editSettings]='VariationQSOReditSettings' [toolbar]="VariationQSORtoolbar"

                                    height="220" [allowResizing]="true" [allowSorting]="true" gridLines='Both'

                                    [allowReordering]="true" showColumnMenu='true' [groupSettings]='groupOptions'

                                    allowFiltering='true' [enableStickyHeader]="true" [filterSettings]='filterSettings'

                                    [allowExcelExport]="true" [allowPdfExport]="true"

                                    [selectionSettings]="{ type: 'Multiple' }" [showColumnChooser]="true"

                                    (created)="onCreatedVariationQSOR()"

                                    (headerCellInfo)="headerCellInfoVariation($event)"

                                    (queryCellInfo)="queryCellInfoVariation($event)"

                                    (actionBegin)="onActionBeginVariationQSOR($event)"

                                    (actionComplete)="onActionCompleteVariationQSOR($event)"

                                    (columnMenuOpen)="columnMenuOpen($event)"

                                    (recordDoubleClick)="onDoubleClick($event)"

                                    (resizeStop)="columnResize(VariationQSORgrid,gridNames.VariationQSORgrid)"

                                    (columnMenuClick)="columnMenuClick(VariationQSORgrid,gridNames.VariationQSORgrid)">

                                    <e-columns>

                                        <e-column field='id' headerText='Id' width="50" [visible]="false"

                                            [showInColumnChooser]="false" textAlign="center"></e-column>

                                        <e-column field='commercialSuiteProjectId' headerText='CommercialSuiteProjectId'

                                            width="50" [visible]="false" [showInColumnChooser]="false"

                                            textAlign="center"></e-column>

                                        <e-column field='sorReference' headerTextAlign="center"

                                            [headerText]="l('Reference')" width='140' textAlign="left"

                                            [allowEditing]="isAddHoc" [validationRules]="SorReferenceQSORRule" >

                                            <ng-template #template let-record>

                                                <span tooltip="{{record.sorReference}}"

                                                    placement="left">{{record.sorReference}}</span>

                                            </ng-template>

                                        </e-column>


.ts



    public customQSORValidator: (args: { [key: string]: string }) => boolean = (args: { [key: string]: string }) => {


        const gridName = ((args.element as any).id).split((args.element as any).name)[0];

        let grid: GridComponent | undefined;


        if (gridName === 'VariationQSORgrid') {

            this.VariationQSORgrid.forEach((gridInstance: GridComponent) => {

                grid = gridInstance;

            });

        }

        const newValue = args.value;

        const isDuplicate = (grid?.dataSource as Object[]).some(item =>

            item['sorReference'] === newValue &&

            (!this.isEditMode|| item['sorReference'] !== this.editingOldSorRefVal)


        );


        if (isDuplicate) {


            return false;

        }

        return true;

    };


    SorReferenceQSORRule = { required: true, min: [this.customQSORValidator.bind(this),'Please Enter a Unique Reference Value'] };


  getAllChildGridItems(voId) {

        return new Promise(async (resolve, reject) => {

            const processChildItems = async (result) => {

                result = result && result.length > 0 ? result : [];

                this.VariationQSORDatasource = result.filter(f => f.childType == (this.isAddHoc ? "addHoc" : "qsor"));

                this.calculateTotalsVariationQSOR();

                await this.getAllVariationSummaryItems();

                if (this.isPageInitialized == false) {

                    this.StaticQSORArray = _.cloneDeep(this.VariationQSORDatasource);

                    this.isPageInitialized = true;

                }

                resolve(true);

            };

            var currentVariation = this.AllVariations.find(x => x.id == voId);

            if (currentVariation && currentVariation.id) {

                const currVariationVal = _.cloneDeep(currentVariation.value);

                this.VariationQSORDatasource = [];

                this.isAddHoc = currentVariation.isAddHocEnabled;

                if (!currentVariation.isStandaloneVoData && currentVariation.isAddHocEnabled) {

                    var _grossMargin = currVariationVal - currentVariation.cost;

                    this.VariationQSORDatasource.push({

                        id: 0,

                        sorReference: "",

                        description: "Un Categorized",

                        quantity: 1,

                        unitSell: currVariationVal || 0,

                        totalSell: currVariationVal || 0,

                        unitCost: currentVariation.cost || 0,

                        totalCost: currentVariation.cost || 0,

                        grossMargin: _grossMargin,

                        grossMarginPercentage: !currVariationVal ? 0 : (_grossMargin / currVariationVal) * 100,

                        costCodeName: "",

                        variationId: this.VariationForm.get('id').value || 0,

                        commercialSuiteProjectId: this.projectId,

                        isVariationsData: true,

                        isAddHocData: true,

                    });

                    this.calculateTotalsVariationQSOR();

                    await this.getAllVariationSummaryItems();

                    this.StaticQSORArray = _.cloneDeep(this.VariationQSORDatasource);


                    resolve(true);

                }

                else {

                    this._aFPVariationSummaryServiceProxy.getAllVariationsChildItems(this.projectId, this.isAddHoc, voId ?? 0).subscribe(processChildItems);

                }

            }

            else {

                this._aFPVariationSummaryServiceProxy.getAllVariationsChildItems(this.projectId, this.isAddHoc, 0).subscribe(processChildItems);

            }

        });

    }


 async saveDropDown(e) {

        await this.updateOpenEditRow();


        if (!this.ValidateObject(this.VariationForm.value)) {

            this.message.warn(this.l('ErrorMsg'));

            this.activeStepIndex = 0;

            return;

        }


        const voExists = this.AllVariations.some(x =>

            x.id !== this.VariationForm.get('id').value && x.voNumber?.toString()?.trim() === this.VariationForm.get('voNumber').value?.toString()?.trim()

        );


        if (voExists) {

            this.message.warn(this.l('Entered VO Number already exists for this Job'));

            return;

        }


        this.spinnerService.show();

        if (e.item.id === "save") {

            this.save(true).then(() => {

                this.spinnerService.hide();

                this.VariationQSORgrid.first?.refresh();

            });

        }

        else if (e.item.id === "saveAndClose") {

            this.save(true).then(() => {

                this.spinnerService.hide();

                this.close();

            });

        }

    }


  save(showSpinner = false) {

        return new Promise((resolve, reject) => {

            var _data: CreateOrEditAFPVariationSummaryDto = new CreateOrEditAFPVariationSummaryDto();

            _data = _.cloneDeep(this.VariationForm.value);

            _data.isAddHocEnabled = this.isAddHoc;

            _data.isStandaloneVoData = true;

            _data.commercialSuiteProjectId = this.projectId;

            _data.value = this.TotalPriceVS;

            _data.cost = this.TotalCostVS;

            _data.best = this.TotalPriceVS;

            _data.pm = this.AllVariations.find(f => f.id == this.VariationForm.get('id').value)?.pm || 0;

            const foundItem = this.CASdropdown.find(c => c.id == _data.customerAgreementStatusId);

            _data.ptp = foundItem ? !_data.value ? 0 : (foundItem.percentageOfBest * _data.value) / 100 : 0;


            _data.qsoRs = [];

            if (this.VariationQSORDatasource && this.VariationQSORDatasource.length > 0) {

                this.VariationQSORDatasource.forEach((element, index) => {

                    let dobj: QSORDto = new QSORDto();

                    Object.assign(dobj, element);

                    dobj.costCodeId = this.costCodes.find((costCode) => costCode.costCodeName === element.costCodeName)?.id;

                    _data.qsoRs[index] = dobj;

                });

            }

            this.spinnerService.show();

            this._aFPVariationSummaryServiceProxy.createOrEdit(_data).subscribe(async result => {

                this.spinnerService.hide();

                if (!_data.id && result && result.id > 0) {

                    _data.id = result.id;

                    this.VariationForm.controls['id'].setValue(result.id);

                }


                var idx = this.AllVariations.findIndex(x => x.id == result.id);

                if (idx == -1 && !this.AllVariations[0].id) this.AllVariations[0].id = result.id;

                idx = this.AllVariations.findIndex(x => x.id == result.id);

                if (idx != -1) Object.assign(this.AllVariations[idx], result);


                await this.getAllChildGridItems(result.id);

                this.StaticQSORArray = _.cloneDeep(this.VariationQSORDatasource);

                this.saveEmitter.emit(_data);

                if (showSpinner) this.message.info(this.l('SavedSuccessfully'));


                this.VariationQSORgrid.first?.refresh();

                resolve(true);

            },

                () => {

                    this.spinnerService.hide();

                    this.message.warn(this.l('FailedToSave'));

                }

            );

        });

    }


 onActionBeginVariationQSOR(args) {

        if (args.requestType === 'beginEdit') {

            if (!args.rowData.grossMarginPercentage) args.rowData.grossMarginPercentage = 0;


            this.isEditMode = true;

            this.editingOldSorRefVal = (args.rowData as any).sorReference;


        }

        else if (args.requestType === 'add') {

            this.isEditMode = false;

            this.editingOldSorRefVal = null;

        }



        if (args.requestType === "save") {

            args.data.totalSell = args.data.unitSell * args.data.quantity;

            args.data.totalCost = args.data.unitCost * args.data.quantity;

            args.data.grossMargin = args.data.totalSell - args.data.totalCost;

            args.data.grossMarginPercentage = (args.data.grossMargin / args.data.totalSell) * 100;

            args.data.commercialSuiteProjectId = this.projectId;


            const forbiddenValues = ['id', 'variationId', 'isVariationsData', 'isAddHocData', 'costCodeId'];

            for (const [key, value] of Object.entries(args.data)) {

                if (!forbiddenValues.includes(key) && (value == null || value === undefined || (typeof value === 'string' && value.trim().length === 0))) {

                    this.message.warn(this.l('Enter All Fields'));

                    return args.cancel = true;

                }

            }

        }

    }



 async onActionCompleteVariationQSOR(args) {

        var Grid: GridComponent;

        this.VariationQSORgrid.forEach((gridInstance: GridComponent) => {

            Grid = gridInstance;

        });

         console.log('request tyepe:', args.requestType);

        if (args.requestType === 'beginEdit') {

            args.form.querySelector('#' + Grid.element.id + this.setFocus.field).focus();

        }


        if (args.requestType === 'beginEdit' || args.requestType === 'add') {



            var _tempData: CreateOrEditQSORDto = new CreateOrEditQSORDto();

            for (var i = 0; i < args.form.elements.length; i++) {

                args.form.elements[i].oninput = () => {

                    var x = args.form.elements;

                    for (var j = 0; j < x.length; j++) {

                        const name = x[j].nextSibling?.getAttribute('name') || "";

                        const value = this.convertToNumber(x[j].value);

                        if (name) _tempData[name] = value;


                        switch (name) {

                            case "totalSell":

                                _tempData.totalSell = (_tempData.unitSell || 0) * (_tempData.quantity || 0);

                                x[j].value = _tempData.totalSell.toLocaleString('en-GB', { minimumFractionDigits: 2, maximumFractionDigits: 2 });

                                break;

                            case "totalCost":

                                _tempData.totalCost = (_tempData.unitCost || 0) * (_tempData.quantity || 0);

                                x[j].value = _tempData.totalCost.toLocaleString('en-GB', { minimumFractionDigits: 2, maximumFractionDigits: 2 });

                                break;

                            case "grossMargin":

                                _tempData.grossMargin = (_tempData.totalSell || 0) - (_tempData.totalCost || 0);

                                x[j].value = _tempData.grossMargin.toLocaleString('en-GB', { minimumFractionDigits: 2, maximumFractionDigits: 2 });

                                break;

                            case "grossMarginPercentage":

                                _tempData.grossMarginPercentage = ((_tempData.grossMargin / _tempData.totalSell) * 100) || 0;

                                x[j].value = _tempData.grossMarginPercentage.toLocaleString('en-GB', { minimumFractionDigits: 2, maximumFractionDigits: 2 });

                                break;

                            default:

                                break;

                        }

                    }

                }

            }


        }


        if (args.requestType === 'save') {



            if (this.VariationQSORDatasource && args.data) {

                const newReference = args.data.sorReference;

                const oldReference = args.previousData?.sorReference;

                console.log("old,new:",newReference,oldReference);

                const existingIndex = this.VariationQSORDatasource.findIndex(

                    (item) => item.sorReference === oldReference

                );


                if (existingIndex !== -1) {

                    //updating row

                    this.VariationQSORDatasource[existingIndex] = { ...args.data };

                } else {

                    //adding new row

                    const alreadyExists = this.VariationQSORDatasource.some(

                        (item) => item.sorReference === newReference

                    );

                    if (!alreadyExists) {

                        this.VariationQSORDatasource.push({ ...args.data });

                        this.VariationQSORDatasource.refresh();

                    }

                }


                this.VariationQSORDatasource = [...this.VariationQSORDatasource];


                await this.updateVariationSummaryDs();

                this.calculateTotalsVariationSummary();

                this.calculateTotalsVariationQSOR();

                this.VariationQSORgrid.first?.endEdit();

                this.VariationQSORgrid.first?.refresh();




            }

        }


        if (["columnstate", "reorder", "filtering", "sorting", "searching"].includes(args.requestType)) {

            var Grid: GridComponent;

            this.VariationQSORgrid.forEach((gridInstance: GridComponent) => {

                Grid = gridInstance;

            });


            Grid.refresh();

             this._utilsServiceComponent.createOrEditUserPersistance(Grid, this.gridNames.VariationQSORgrid, this.projectId);

        }

    }


1 Reply

SR Sivaranjani Rajasekaran Syncfusion Team April 4, 2025 10:24 AM UTC

Hi Almas Fathima,

Greetings from Syncfusion support!

We understand from your query that you would like to apply custom validation to a column in the child grid, based on the code snippet you shared, it appears that you may be using multiple Grid instances with the same reference. If so, please ensure that each Grid has a unique reference to avoid unexpected behavior.

We noticed that you're aiming to prevent duplicate values from being entered. To achieve this, you can bind custom validation rules to the desired column and define a custom validation method that checks whether the entered value already exists in the data source.

We have prepared a sample demonstrating how to implement this functionality. In the sample, we apply custom validation rules to the CustomerID column of the child grid to check for duplicate entries and enforce unique values during editing.


Code Example : 

  customFn: (args: { [key: string]: string }) => boolean = (args: {
    [key: string]: string;
  }) => {
    const inputValue = args['value']?.trim().toLowerCase(); // normalize input
    const isDuplicate = orderDatas.some(
      (item: any) => item.CustomerID?.trim().toLowerCase() === inputValue
    );
    return !isDuplicate; // return true if valid (not duplicate)
  };


      {
          field: 'CustomerID',
          headerText: 'Customer ID',
          width: 120,
          validationRules: {
            required: true,
            custom: [
              this.customFn,
              'Duplicate Value, please enter unique value',
            ],
          },
        },





To assist you further and provide the most accurate solution, we kindly request the following additional details:
  1. You are currently using local data to validate duplicate values. Additionally, we noticed that you are using both the actionBegin and actionComplete events. Could you please elaborate on the custom logic or operations you're performing within these event handlers?
  2. Could you confirm the edit type you are using? For example, are you using Dialog, or Template editing? If you're using a custom template for the form, kindly share those details as well.
  3. Please let us know the exact Syncfusion Grid package version and Angular version you are using in your application.
  4. Please share a screenshot or short video demonstrating the issue to help us visualize the problem.
  5. Share a minimal reproducible sample or try to replicate the issue using the sample we have provided. This will help us debug the scenario efficiently

Once we have this information we provide the prompt solution.

Loader.
Up arrow icon