Angular Grid Editing and Validation

    The Grid's editing exposes a built-in validation mechanism of user input when editing cells/rows. It extends the Angular Form validation functionality to allow easier integration with a well known functionality. When the state of the editor changes, visual indicators are applied to the edited cell.

    Configuration

    Configure via template-driven configuration

    We extend some of the Angular Forms validator directives to directly work with the IgxColumn. The same validators are available as attributes to be set declaratively in igx-column. The following validators are supported out-of-the-box:

    • required
    • min
    • max
    • email
    • minlength
    • maxlength
    • pattern

    To validate that a column input would be set and the value is going to be formatted as an email, you can use the related directives:

    <igx-column [field]="email" [header]="User E-mail" required email></igx-column>
    

    The following sample demonstrates how to use the prebuilt required, email and min validator directives in a Grid.

    Configure via reactive forms

    We expose the FormGroup that will be used for validation when editing starts on a row/cell via a formGroupCreated event. You can modify it by adding your own validators for the related fields:

    <igx-grid (formGroupCreated)='formCreateHandler($event)' ...>
    
        public formCreateHandler(args: IGridFormGroupCreatedEventArgs) {
            const formGroup = args.formGroup;
            const orderDateRecord = formGroup.get('OrderDate');
            const requiredDateRecord = formGroup.get('RequiredDate');
            const shippedDateRecord = formGroup.get('ShippedDate');
    
            orderDateRecord.addValidators(this.futureDateValidator());
            requiredDateRecord.addValidators(this.pastDateValidator());
            shippedDateRecord.addValidators(this.pastDateValidator());
        }
    

    You can decide to write your own validator function, or use one of the built-in Angular validator functions.

    Validation service API

    The grid exposes a validation service via the validation property. That service has the following public APIs:

    • valid - returns if the grid validation state is valid.
    • getInvalid - returns records with invalid states.
    • clear - clears state for record by id or clears all state if no id is provided.
    • markAsTouched - marks the related record/field as touched.

    Invalid states will persis until the validation errors in them are fixed according to the validation rule or they are cleared.

    Validation triggers

    Validation will be triggered in the following scenarios:

    • While editing via the cell editor based on the grid's validationTrigger. Either on change while typing in the editor, or on blur when the editor loses focus or closes.
    • When updating cells/rows via the API - updateRow, updateCell etc..
    • When using batch editing and the undo/redo API of the transaction service.

    Note: Validation will not trigger for records that have not been edited via user input or via the editing API. Visual indicators on the cell will only shown if the related input is considered touched - either via user interaction or via the markAsTouched API of the validation service.

    Angular Grid Validation Customization Options

    Set a custom validator

    You can define your own validation directive to use on a <igx-column> in the template.

    @Directive({
        selector: '[phoneFormat]',
        providers: [{ provide: NG_VALIDATORS, useExisting: PhoneFormatDirective, multi: true }]
    })
    export class PhoneFormatDirective extends Validators {
        @Input('phoneFormat')
        public phoneFormatString = '';
    
        public validate(control: AbstractControl): ValidationErrors | null {
            return this.phoneFormatString ? phoneFormatValidator(new RegExp(this.phoneFormatString, 'i'))(control)
                : null;
        }
    }
    

    Once it is defined and added in your app module you can set it declaratively to a given column in the grid:

    <igx-column phoneFormat="\+\d{1}\-(?!0)(\d{3})\-(\d{3})\-(\d{4})\b" ...>
    

    Change default error template

    You can define your own custom error template that will be displayed in the error tooltip when the cell enters invalid state. This is useful in scenarios where you want to add your own custom error message or otherwise change the look or content of the message.

    <igx-column ... >
      <ng-template igxCellValidationError let-cell='cell' let-defaultErr="defaultErrorTemplate">
          <ng-container *ngTemplateOutlet="defaultErr">
          </ng-container>
          <div *ngIf="cell.validation.errors?.['phoneFormat']">
            Please enter correct phone format
          </div>
      </ng-template>
    </igx-column>
    

    Prevent exiting edit mode on invalid state

    In some cases you may want to disallow submitting an invalid value in the data. In that scenarios you can use the cellEdit or rowEdit events and cancel the event in case the new value is invalid. Both events' arguments have a valid property and can be canceled accordingly. How it is used can be seen in the Cross-field Validation example

    <igx-grid (cellEdit)='cellEdit($event)' ...>
    
    public cellEdit(evt) {
      if (!evt.valid) {
        evt.cancel = true;
      }
    }
    

    Example

    The below example demonstrates the above-mentioned customization options.

    Cross-field validation

    In some scenarios validation of one field may depend on the value of another field in the record. In that case a custom validator can be used to compare the values in the record via their shared FormGroup.

    The below sample demonstrates a cross-field validation between different field of the same record. It checks the dates validity compared to the current date and between the active and created on date of the record as well as the deals won/lost ration for each employee. All errors are collected in a separate pinned column that shows that the record is invalid and displays the related errors.

    The next lines of code show the cross-field validator function, which contains the comparisons and sets the related errors relative to them.

    private rowValidator(): ValidatorFn {
        return (formGroup: FormGroup): ValidationErrors | null => {
            let returnObject = {};
            const createdOnRecord = formGroup.get('created_on');
            const lastActiveRecord = formGroup.get('last_activity');
            const winControl = formGroup.get('deals_won');
            const loseControl = formGroup.get('deals_lost');
            const actualSalesControl = formGroup.get('actual_sales');
    
            // Validate dates
            const curDate = new Date();
            if (new Date(createdOnRecord.value) > curDate) {
                // The created on date shouldn't be greater than current date.
                returnObject['createdInvalid'] =  true;
            }
            if (new Date(lastActiveRecord.value) > curDate) {
                // The last active date shouldn't be greater than current date.
                returnObject['lastActiveInvalid'] = true;
            }
            if (new Date(createdOnRecord.value) > new Date(lastActiveRecord.value)) {
                // The created on date shouldn't be greater than last active date.
                returnObject['createdLastActiveInvalid'] = true;
            }
    
            // Validate deals
            const dealsRatio = this.calculateDealsRatio(winControl.value, loseControl.value);
            if (actualSalesControl.value === 0 && dealsRatio > 0) {
                // If the actual sales value is 0 but there are deals made.
                returnObject['salesZero'] = true;
            }
            if (actualSalesControl.value > 0 && dealsRatio === 0) {
                // If the deals ratio based on deals won is 0 but the actual sales is bigger than 0.
                returnObject['salesNotZero'] = true;
            }
    
            return returnObject;
        };
    }
    
    public calculateDealsRatio(dealsWon, dealsLost) {
        if (dealsLost === 0) return dealsWon + 1;
        return Math.round(dealsWon / dealsLost * 100) / 100;
    }
    

    The cross-field validator can be added to the formGroup of the row from formGroupCreated event, which returns the new formGroup for each row when entering edit mode:

    <igx-grid #grid1 [data]="transactionData" [width]="'100%'" [height]="'480px'" [autoGenerate]="false" 
            [batchEditing]="true" [rowEditable]="true" [primaryKey]="'id'"
            (formGroupCreated)='formCreateHandler($event)'>
        <!-- ... -->
    </igx-grid>
    
    public formCreateHandler(evt: IGridFormGroupCreatedEventArgs) {
        evt.formGroup.addValidators(this.rowValidator());
    }
    

    The different errors are displayed in a templated cell that combines all errors in a single tooltip. Depending on the row valid state different icon is displayed:

    <igx-column field="row_valid" header=" " [editable]="false" [pinned]="true" [width]="'50px'">
        <ng-template igxCell let-cell="cell">
            <div *ngIf="isRowValid(cell)" [igxTooltipTarget]="tooltipRef"  style="margin-right: '-10px';">
                <img width="18" src="assets/images/grid/active.png"/>
            </div>
            <div *ngIf="!isRowValid(cell)" [igxTooltipTarget]="tooltipRef" style="margin-right: '-10px';">
                <img width="18" src="assets/images/grid/expired.png"/>
            </div>
            <div #tooltipRef="tooltip" igxTooltip [style.width]="'max-content'">
                <div *ngFor="let message of stateMessage(cell)">
                    {{message}}
                </div>
            </div>
        </ng-template>
    </igx-column>
    

    The error messages are gathered in the stateMessage function, which gathers the errors for each cell, because each column could have templated form validations and then checks the errors for the row itself, which come from the custom rowValidator.

    public stateMessage(cell: CellType) {
        const messages = [];
        const row = cell.row;
        const cellValidationErrors = row.cells.filter(x => !!x.validation.errors);
        cellValidationErrors.forEach(cell => {
            if (cell.validation.errors) {
                if (cell.validation.errors.required) {
                    messages.push(`The \`${cell.column.header}\` column is required.`);
                }
                // Other cell errors ...
            }
        });
    
        if (row.validation.errors?.createdInvalid) {
            messages.push(`The \`Date of Registration\` date cannot be in the future.`);
        }
        // Other cross-field errors...
    
        return messages;
    }
    

    Cross-field example

    The below sample demonstrates the cross-field validation in action.

    Styling

    Using the Ignite UI for Angular Theme Library, we can alter the default validation styles while editing.

    In the example below, we will make use of the exposed template for validation message, which pops out in a tooltip and overriding the error color to modify the default looks of the validation. We will also style the background of the invalid rows to make them more distinct.

    Import theme

    The easiest way to style and access css variables is to define styles in our app's global style file (typically styles.scss). The first thing we need to do is import the themes/index file - this gives us access to all the powerful tools of the Ignite UI for Angular Sass framework:

    @use "igniteui-angular/theming" as *;
    
    // IMPORTANT: Prior to Ignite UI for Angular version 13 use:
    // @import '~igniteui-angular/lib/core/styles/themes/index';
    

    Include the styles

    In order to change the error color you can use the css variable --igx-error-500:

    --igx-error-500: 34, 80%, 63%;
    

    Custom Templates

    Changing the default error template allows setting custom classes and styles:

    <ng-template igxCellValidationError let-cell='cell' let-defaultErr='defaultErrorTemplate'>
        <div class="validator-container">
            <ng-container *ngTemplateOutlet="defaultErr">
            </ng-container>
        </div>
    </ng-template>
    

    Invalid row and cell styles

    Rows and cells provide API for the developers to know if a row or cell is invalid and what kind of errors are active.

    public rowStyles = {
        background: (row: RowType) => row.validation.status === 'INVALID' ? '#FF000033' : '#00000000'
    };
    public cellStyles = {
        'invalid-cell': (rowData, columnKey) => {
            const pKey = this.grid.primaryKey;
            const cell = this.grid.getCellByKey(rowData[pKey], columnKey);
            return cell && cell.validation.status === 'INVALID';
        }
    }
    
    <igx-grid [rowStyles]="rowStyles">
        <igx-column field="ReorderLevel" header="ReorderLever" required [cellClasses]="cellStyles">
    

    Demo

    API References

    Known Issues and Limitations

    Limitation Description
    When validationTrigger is blur, editValue and validation will trigger only after editor is blurred.

    Reason is that this utilizes the formControl's updateOn property. This determines the event on which the formControl will update and trigger related validators.

    Additional Resources

    Our community is active and always welcoming to new ideas.