[Angular 5] Odd FormArray behavior when using reactive form in a FlexGrid

Posted by: pedro-antonio.vicente on 26 March 2018, 10:53 am EST

    • Post Options:
    • Link

    Posted 26 March 2018, 10:53 am EST

    Hi,

    We’ve detected two issues tied to the sorting and filtering features and the way the wijmo FlexGrid interacts with the FormArray in a reactive form. I’ve attached a project which demonstrates both of them.

    Angular v5.2.3

    Wijmo v3.1.0

    wijmo-app.zip

    Let’s say we want to implement a custom validation that prevents a value to appear more than once within a column, i.e., values inserted in a certain column have to be unique.

    Using wj-flex-grid with a wj-flex-grid-filter and implementing validation through reactive form:

    <form [formGroup]="formGroup">
        <wj-flex-grid 
            #flexGrid 
            [selectionMode]="'None'" 
            formArrayName="formArray">
            <wj-flex-grid-filter></wj-flex-grid-filter>
            <wj-flex-grid-column
                [width]="'*'"
                [header]="'Header1'"
                [binding]="'column1'">
                <ng-template
                    wjFlexGridCellTemplate
                    [cellType]="'Cell'"
                    let-item="item"
                    let-cell="cell">
                    <ng-container [formGroupName]="cell.row.index">
                        <input type="text" [(ngModel)]="item.column1" formControlName="column1">
                        <ng-container *ngIf="formArray.get([(cell.row.index).toString(), 'column1']).invalid">
                            {{formArray.get([(cell.row.index).toString(), 'column1']).errors | json}}
                        </ng-container>
                    </ng-container>
                </ng-template>
            </wj-flex-grid-column>
            <wj-flex-grid-column
                [width]="'*'"
                [header]="'Header2'"
                [binding]="'column2'">
                <ng-template
                    wjFlexGridCellTemplate
                    [cellType]="'Cell'"
                    let-item="item"
                    let-cell="cell">
                    <ng-container [formGroupName]="cell.row.index">
                        <input type="text" [(ngModel)]="item.column2" formControlName="column2">
                        <ng-container *ngIf="formArray.get([(cell.row.index).toString(), 'column2']).invalid">
                            {{formArray.get([(cell.row.index).toString(), 'column2']).errors | json}}
                        </ng-container>
                    </ng-container>
                </ng-template>
            </wj-flex-grid-column>
        </wj-flex-grid>
    </form>
    

    Relevant component code:

    @ViewChild('flexGrid') public flexGrid: FlexGrid;
    
        public dataList: {column1: string, column2: string}[] = [
            { column1: 'value1', column2: 'w'},
            { column1: 'value2', column2: 'x'},
            { column1: 'value3', column2: 'y'},
            { column1: 'value4', column2: 'z'},
        ];
    
        public formGroup: FormGroup;
    
        public formArray: FormArray;
    
        public constructor(private formBuilder: FormBuilder) {
            this.createForm();
        }
    
        ngOnInit() {
            this.flexGrid.headersVisibility = HeadersVisibility.Column;
            this.flexGrid.allowSorting = true;
            this.flexGrid.allowDragging = AllowDragging.None;
            this.flexGrid.allowResizing = AllowResizing.None;
            this.flexGrid.itemsSource = this.dataList;
        }
    
        private createForm() {
            this.formGroup = this.formBuilder.group({
                formArray : this.formBuilder.array([])
            });
    
            this.formArray = this.formGroup.controls['formArray'] as FormArray;
    
            this.dataList.forEach(() => {
                const newFormGroup: FormGroup = this.formBuilder.group(
                    {
                        column1: [''],
                        column2: [''],
                    }
                );
                this.formArray.push(newFormGroup);
            });
        }
    

    Using the following custom validator:

    
        public static uniqueValueValidator(formControl: FormControl): ValidationErrors {
            if (!formControl.parent || !formControl.parent.parent) {
                return null;
            }
            // Retrieve all values in the first column
            const formArrayList: FormArray = formControl.parent.parent as FormArray;
            const valueList: string[] = [];
            for (const formGroup of formArrayList.controls) {
                valueList.push(formGroup.get('column1').value);
            }
    
            // Check if unique
            const isUnique = valueList.filter((value: string) => value === formControl.value).length === 1;
    
            return isUnique ? null : { uniqueValue: true };
        }
    

    1) Sorting

    Say we start with four rows and the following values:

    1. value1
    2. value2
    3. value3
    4. value4

    The way sorting works, it starts by replacing the first value with the last (fourth) one. This triggers the validation… but since the fourth value hasn’t changed yet, the first one is considered invalid, since the values match. The same issue arises with the following line.

    Here is a step by step of the problem which can be observed while debugging:

    1. value1 ----> value4 {error} (because of row 4)

    2. value2 value2

    3. value3 value3

    4. value4 value4

    5. value4 value4 {error}

    6. value2 ----> value3 {error} (because of row 3)

    7. value3 value3

    8. value4 value4

    9. value4 value4 {error}

    10. value3 value3 {error}

    11. value3 ----> value2 (no error)

    12. value4 value4

    13. value4 value4 {error}

    14. value3 value3 {error}

    15. value2 value2

    16. value4 ----> value1 (no error)

    Final result:

    1. value4 {error}
    2. value3 {error}
    3. value2
    4. value1

    The obvious workaround would be to trigger an update of all validations using AbstractControl.updateValueAndValidity() once sorting ends. However, FlexGrid’s (sortedColumn) event triggers before any (ngModelChange), rendering it useless for this purpose.

    Is this the intended behavior of (sortedColumn)?

    Is there some other way to detect when the sorting has finished so that the validations can be refreshed? Or any other possible workarounds?

    2) Filter

    This one is a bit trickier to explain but should be obvious if you try to filter one of the values out while the validation is active in the attached example.

    There are two issues at play here:

    The first one is the one described in the sorting example above. The order in which the values are replaced causes some ghost errors to pop up.

    The second issue becomes apparent if you check the FormArray values as shown below the table. As I understand it, activating a filter doesn’t hide the affected rows but instead modifies the value of the existing rows as it sees fit and then hides the unnecessary rows starting from the end of the table.

    For example:

    1. value1
    2. value2
    3. value3
    4. value4

    If we filter value3 out, although the table shows the correct result…

    1. value1
    2. value2
    3. value4

    …the values in the FormArray reveal what happened under the hood:

    { “column1”: “value1”, … }

    { “column1”: “value2”, … }

    { “column1”: “value4”, … }

    { “column1”: “value4”, … }

    “value3” was replaced by “value4” in the third row but the old value in row 4 is not modified and thus any validation becomes incorrect, as they are always done against the values currently in the FormArray and those are not correct.

    Is this the intended behavior of the flex-grid-filter?

    Are there any workarounds we could use other than completely changing the validation as it is currently implemented?

    Thank you.

  • Posted 27 March 2018, 7:14 am EST

    Hi,

    The problem you are facing here is because of the way value is assigned to the “formArray.control”. It gets assigned after an input field is assigned avalue.

    When you sort/filter, first the grid updates its source then refreshes the view to reflect changes.As the grid is updating its view , concurrently validation is checked. Since the view is yet not updated completely there exists a discrepancy between the grid’s source and formArray.control values which causes the validation to fail.

    To avoid this you may use the grid’s source values to check for duplicates.

    Refer to following code snippet:-

    //in AppValidator class change uniqueValueValidator as
    
    
    public static uniqueValueValidator(formControl:FormControl,listItems:any[]):ValidationErrors{
    
    ......unchanged code
    
    
    //replace this code with highlighted code
    
    // for(const formGroup of formArrayList.controls){
    
    //    valueList.push(formGroup.get('column1').value);
    
    // }
    
    
    listItems.forEach(item=>valueList.push(item['column1']));
    
    
    
    .....rest of unchanged code
    
    
    }
    
    
    
    //in addUniqueValidation() method
    
    
    public addUniqueValidation(){
    
    for(const control of this.formArray.controls){
    
    control.get('column1').setValidators(fControl=>AppValidator.uniqueValueValidator(fControl as FormControl,this.flexGrid.collectionView.items));
    
    control.get('column1').updateValueAndValidity();
    
    }
    
    }
    
    
    

    You can also refer to the attached updated sample.

  • Posted 28 March 2018, 7:33 am EST

    Thank you, that does the trick as a workaround.

    Small correction however: it’s probably better to use this.flexGrid.collectionView.sourceCollection instead of this.flexGrid.collectionView.items in the validation check as ‘items’ does not contain filtered data.

  • Posted 28 March 2018, 11:03 pm EST

    Nice catch, I agree that using this.flexGrid.collectionView.sourceCollection would be better.

Need extra support?

Upgrade your support plan and get personal unlimited phone support with our customer engagement team

Learn More

Forum Channels