Search code examples
angularangular-directiveivyangular-ngmodel

Angular 9 - NgModel in custom directive changed behaviour from Angular 7


I'm part way through upgrading a large application to Angular 9 with Ivy compilation enabled, and have come across what seems to be a change in behaviour with NgModel injected into a custom directive

When using this directive, the NgModel that is injected is different between version 7 and version 8/9 when IVY compilation is used.

This does NOT happen when Ivy compilation is disabled.

The Directive (tabSelect)

import { Directive, AfterViewInit, OnDestroy, Optional } from '@angular/core';
import { NgModel } from "@angular/forms";
import { MatAutocompleteTrigger } from '@angular/material/autocomplete';


@Directive({
    selector: '[tabSelect]',
    providers: [NgModel]
})

export class TabSelectDirective implements AfterViewInit, OnDestroy {

    observable: any;
    onChange: any;      

    constructor(@Optional()        
        private autoTrigger: MatAutocompleteTrigger,            
        private ngModel: NgModel) { }
    ngAfterViewInit() {

        this.observable = this.autoTrigger.panelClosingActions.subscribe(x => {
            if (this.autoTrigger.activeOption && this.autoTrigger.activeOption.value) {
                if (this.ngModel.model !== this.autoTrigger.activeOption.value) {
                    this.ngModel.update.emit(this.autoTrigger.activeOption.value);
                    this.autoTrigger.autocomplete.optionSelected.emit();
                }
            } else {
                console.log(this.ngModel);
                console.log(this.autoTrigger);

                if (!this.ngModel.viewModel || this.ngModel.viewModel === "")
                    this.ngModel.update.emit(null);
            };
        });
    }
    ngOnDestroy() {
        this.observable.unsubscribe();
    }
};

Using the Directive in the HTML

<mat-form-field appearance="outline" color="primary" class="mat-input-no-validation pull-right"
                                style="width: 130px; margin-left: 10px">
                    <mat-label>Document Group</mat-label>
                    <input matInput
                           placeholder="Search.."
                           name="documentGroupInput"
                           spellcheck="false"
                           [(ngModel)]="documentNode.documentGroup"
                           [matAutocomplete]="documentGroupAuto"
                           #documentGroupCtrl="ngModel"
                           tabSelect />
                    <div class="small-progress-spinner-container" [hidden]="!documentGroupSearching">
                        <mat-progress-spinner [diameter]="15" [mode]="'indeterminate'"></mat-progress-spinner>
                    </div>
                    <mat-autocomplete #documentGroupAuto="matAutocomplete">
                        <mat-option *ngFor="let documentGroup of documentGroupResults; trackBy:formatters.trackIndex" [value]="documentGroup">
                            <div class="autocomplete-option">
                                {{ documentGroup }}
                            </div>
                        </mat-option>
                    </mat-autocomplete>
                </mat-form-field>

When inspecting the NgModel that is injected into the Directive, in Angular 7 we see the Control name

NgModel {_parent: NgForm, name: "documentGroupInput", valueAccessor: MatAutocompleteTrigger, _rawValidators: Array(0), _rawAsyncValidators: Array(0), …}

Wheras in Angular 9 we do not

NgModel {_parent: NgForm, name: null, valueAccessor: MatAutocompleteTrigger, _rawValidators: Array(0), _rawAsyncValidators: Array(0), …}

This leads me to believe that the injected NgModel is not actually the model of the control itself in Ivy, and this is supported by the method of assigning the value and emitting the changed event no longer working

Any ideas what has changed and how I can resolve this?

Thanks

EDIT: - Fixed code

import { Directive, AfterViewInit, OnDestroy, Optional } from '@angular/core';
import { NgModel } from "@angular/forms";
import { MatAutocompleteTrigger } from '@angular/material/autocomplete';


@Directive({
    selector: '[tabSelect][ngModel]',
    providers: []
})

export class TabSelectDirective implements AfterViewInit, OnDestroy {

    observable: any;
    onChange: any;      

    constructor(@Optional()        
        private autoTrigger: MatAutocompleteTrigger,            
        private ngModel: NgModel) { }

    ngAfterViewInit() {

        console.log(this.ngModel);
        this.observable = this.autoTrigger.panelClosingActions.subscribe(x => {
            if (this.autoTrigger.activeOption && this.autoTrigger.activeOption.value) {
                if (this.ngModel.model !== this.autoTrigger.activeOption.value) {
                    this.ngModel.update.emit(this.autoTrigger.activeOption.value);
                    this.autoTrigger.autocomplete.optionSelected.emit();
                }
            } else {
                console.log(this.ngModel);
                console.log(this.autoTrigger);

                if (!this.ngModel.viewModel || this.ngModel.viewModel === "")
                    this.ngModel.update.emit(null);
            };
        });
    }
    ngOnDestroy() {
        this.observable.unsubscribe();
    }
};

Solution

  • Removing providers: [NgModel] from the @Directive attribute should fix the problem.

    See also github issue 35594