Angular 2 - Inject ngModel in custom directive whe

2019-09-14 09:14发布

问题:

I have tried using a custom attribute in a field provided by a directive (class with @Directive) that I created, and this directive has NgForm injected in the constructor, but it doesn't work if the field has the formControlName attribute.

Here is the plunker of a demo: http://plnkr.co/edit/gdm3Xb?p=preview

Situation

The field is bound with ngModel with two-way bind, because I use the model for updating directly the fields when I want (or need) or sending data to the server. In processes like submiting the form I don't use the fields from the form itself (with controls) because the model class has several methods and isn't just an anemic model, but a rich one with several functionalities, and I don't have to care with low-level stuff like controls. The model also has all the fields (even calculated fields, if needed, not in the demo). So I have included a [(ngModel)] attribute.

But on the other hand, I use FormControls to use the angular validations, so I included a formControlName attribute, the validation works fine and the field behaves as expected.

Until now it is ok, but when I created a directive (class with @Directive) to use as an attribute (in this case, the attribute myDirective) in the field that has the formControlName attribute and the directive has a NgModel injected, I receive the following error:

Error: Uncaught (in promise): EXCEPTION: Error in ./AppComponent class AppComponent - inline template:4:3
ORIGINAL EXCEPTION: No provider for NgModel!

In the demo I use @Optional for NgModel so as to not receive the error and the page load, but I log ngModel and it shows that it is null.

If I provide NgModel explicitly in the providers it doesn't show an error, but it won't be the ngModel that I want (not related to the control/field in question), so if I try to apply changes to that ngModel it won't reflect in the field.

If, instead, I remove the formControlName, the directive works fine (you can see in the logs where I log 'ngModel:' in one line and the object in the next: ngModel was was null before and now is an object of type NgModel). The validation also happens because of the name attribute (the formBuilder binds based on the 'name' attribute of the field). The problem is that the initial value in the model doesn't show in the field, only shows after that value changes (you can see below the field where I show the model object as JSON), and also the field doesn't stay in an invalid state even if the value in it is invalid, like when I erase the content (the border doesn't change to red and inspecting in the browser the class is ng-valid, and it is also ng-untouched, even after changing, kinda like the field and the control are not connected).


Files

my.directive.ts

import { Directive, Optional } from '@angular/core';
import { NgModel } from '@angular/forms';

@Directive({
    selector: '[myDirective]'
})
export class MyDirective {
    constructor(@Optional() ngModel: NgModel) {
        console.log('ngModel:');
        console.log(ngModel);       
    }
}

app.component.ts

import { Component } from '@angular/core';
import { REACTIVE_FORM_DIRECTIVES, FormBuilder, FormGroup, Validators } from '@angular/forms';

import { MyDirective } from './my.directive';

@Component({
    selector: 'my-app',
    template: `
        <h1>My Angular 2 App</h1>

        <form [formGroup]="formGroup" [class.error]="!myInput.valid && myInput.touched">
            <input 
                name="myInput" 
                type="text" 
                [(ngModel)]="model.myInput" 
                formControlName="myInput" 
                #myInput
                myDirective="123"
            >
            <div *ngIf="formGroup.controls.myInput.dirty && !formGroup.controls.myInput.valid">
                myInput is required
            </div>
        </form>
    `,
    directives: [REACTIVE_FORM_DIRECTIVES, MyDirective]
})
export class AppComponent {
    public model: { myInput: number } = { myInput: 456 };
    public formGroup: FormGroup;

    constructor(private formBuilder: FormBuilder) { }

    ngOnInit() {
        this.formGroup = this.formBuilder.group({ myInput: ['', [Validators.required], []] });
    }
}

main.ts

import { bootstrap } from '@angular/platform-browser-dynamic';
import { disableDeprecatedForms, provideForms } from '@angular/forms';
import { AppComponent } from './app.component';

bootstrap(AppComponent, [
    disableDeprecatedForms(),
    provideForms()
]);

TL;DR

I would like to know if there is a way to make the directive receive NgModel, but at the same time I want that the form also works the way that is doing now (with two-way data binding so I can use the model object, but also use the FormBuilder to define the validators for the fields).

Updated

Just to clarify, what I want is the angular2 object NgModel related to the control, with methods like viewToModelUpdate and properties like valueAccessor. You can see the NgModel object logged in my plunker if you remove the formControlName attribute.

回答1:

I couldn't find the problem besides how the @Optional()arg works, so I tried to find another way to do it, see if it solves the problem for you!


Files

my.directive.ts

import { Directive, Input,Optional , HostListener} from '@angular/core';
import { NgModel } from '@angular/forms';

@Directive({
    selector: '[myDirective]'
})
export class MyDirective {
    @Input('myDirective') ngModel:NgModel;
    constructor() {
    }
    /**Added to check if the Input is being updated**/
    @HostListener('mouseenter') onMouseEnter() {
      console.log('ngModel:');
      console.log(this.ngModel)
    }
    /**Inputs are only received OnInit**/
    ngOnInit()
    {
        console.log('ngModel:');
        console.log(this.ngModel)
    }
}

app.component.ts

import { Component } from '@angular/core';
import { REACTIVE_FORM_DIRECTIVES, FormBuilder, FormGroup, Validators } from '@angular/forms';

import { MyDirective } from './my.directive';

@Component({
    selector: 'my-app',
    template: `
        <h1>My Angular 2 App</h1>

        <form [formGroup]="formGroup">
            <input 
                name="myInput" 
                type="text" 
                [(ngModel)]="model.myInput" 
                formControlName="myInput"
                [myDirective]="model"
                #myInput
            >
            <div *ngIf="formGroup.controls.myInput.dirty && !formGroup.controls.myInput.valid">
                myInput is required
            </div>

            <br><br>

            {{ model | json }}
        </form>
    `,
    directives: [REACTIVE_FORM_DIRECTIVES, MyDirective]
})
export class AppComponent {
    public model: { myInput: number } = { myInput: 456 };
    public formGroup: FormGroup;

    constructor(private formBuilder: FormBuilder) { }

    ngOnInit() {
        this.formGroup = this.formBuilder.group({ myInput: ['789', [Validators.required], []] });
    }
}

Working plunker: http://plnkr.co/edit/nYtsXH?p=preview