Angular 2 - formControlName inside component

后端 未结 7 1538
闹比i
闹比i 2020-11-30 00:21

I want to create a custom input component that I can use with the FormBuilder API. How do I add formControlName inside a component?

Template:

7条回答
  •  日久生厌
    2020-11-30 00:59

    I'm solving this in a similar way like web-master-now. But instead of writing a complete own ControlValueAccessor I'm delegating everything to an inner ControlValueAccessor. The result is a much shorter code and I don't have to handle the interaction with the element on my own.

    Here is my code

    @Component({
      selector: 'form-field',
      template: `    
        
        `,
      providers: [{
        provide: NG_VALUE_ACCESSOR,
        useExisting: forwardRef(() => FormFieldComponent),
        multi: true
      }]
    })
    export class FormFieldComponent implements ControlValueAccessor, AfterViewInit {
      @Input() label: String;
      @Input() formControlName: String;
      @ViewChild(DefaultValueAccessor) valueAccessor: DefaultValueAccessor;
    
      delegatedMethodCalls = new ReplaySubject<(_: ControlValueAccessor) => void>();
    
      ngAfterViewInit(): void {
        this.delegatedMethodCalls.subscribe(fn => fn(this.valueAccessor));
      }
    
      registerOnChange(fn: (_: any) => void): void {
        this.delegatedMethodCalls.next(valueAccessor => valueAccessor.registerOnChange(fn));
      }
      registerOnTouched(fn: () => void): void {
        this.delegatedMethodCalls.next(valueAccessor => valueAccessor.registerOnTouched(fn));
      }
    
      setDisabledState(isDisabled: boolean): void {
        this.delegatedMethodCalls.next(valueAccessor => valueAccessor.setDisabledState(isDisabled));
      }
    
      writeValue(obj: any): void {
        this.delegatedMethodCalls.next(valueAccessor => valueAccessor.writeValue(obj));
      }
    }
    

    How does it work?

    Generally this won't work, as a simpel won't be a ControlValueAccessor without formControlName-directive, which is not allowed in the component due to missing [formGroup], as others already pointed out. However if we look at Angular's code for the DefaultValueAccessor implementation

    @Directive({
        selector:
            'input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]',
    
        //...
    })
    export class DefaultValueAccessor implements ControlValueAccessor {
    

    ... we can see that there is another attribute selector ngDefaultControl. It's available for a different purpose, but it seems to be supported officially.

    A little disadvantage is that the @ViewChild query result with the value accessor will be available not before the ngAfterViewInit handler is called. (It will be available earlier depending on your template, but that's not supported officially .)

    That's why I'm buffering all calls we want to delegate to our inner DefaultValueAccessor using a ReplaySubject. A ReplaySubject is an Observable, which buffers all events and emits them on subscription. A normal Subject would throw them away until subscription.

    We emit lambda expressions representing the actual call that can be executed later. On ngAfterViewInit we subscribe to our ReplaySubject and simply call the received lambda functions.

    I'm sharing two other ideas here, as they are very important for my own projects and it took me a while to work everything out. I see a lot of people having similar problems and use cases, so I hope this is useful for you:

    Idea for improvement 1: Provide the FormControl for the view

    I replaced the ngDefaultControl by formControl in my project, so we can pass the FormControl instance to the inner . This is not useful by itself, however it is if you are using other directives which interact with FormControls, such as Angular Material's MatInput. E.g. if we replace our form-field template by...

    
        
          {{label}}
          
          
        
        `,
        providers: [{
            provide: NG_VALUE_ACCESSOR,
            useExisting: forwardRef(() => FormFieldComponent),
            multi: true
        }]
    })
    export class FormFieldComponent implements ControlValueAccessor {
        // ... see above
    
        @Input() controlType: String = 'text';
        @ViewChild('valueAccessor', {read: NG_VALUE_ACCESSOR}) valueAccessor: ControlValueAccessor;
    
        // ... see above
    }
    

    Usage example:

    Hello "{{form.get('firstName').value}} {{form.get('lastName').value}}"

    A problem with the select above is that ngModel is already deprecated together with reactive forms. Unfortunately there is nothing like ngDefaultControl for Angular's

    提交评论

提交回复
热议问题