ronif
ronif

Reputation: 815

Attempting to extend FormControlDirective to implement my own FormControl directive results in faulty binding

I'm trying to inverse the way forms controls are registering themselves onto a FormGroup, so that instead of having to

@Component({..., template: `<input [formControl]="myControl"`}    
...

Or

@Component({..., template: `<input [formControName]="myControlName"`}    
...

I could

@Component({..., template: `<input myFormControl`}    
...

And have my directive create and add the FormControl for me.

It is best explained with this Plunker.

What doesn't seem to work is the binding the view to the form model, as you can see, changing the the input does not change the form model value.

Debugging it shows that there are no valueAccessor injected to the constructor (unlike when using the base FormControlDirective class directly).

If you're wondering, my end goal would be that I would have a parent customized group component that would @ViewChildren(MyFormDirective), and dynamically add them all to it's created form.

Upvotes: 3

Views: 2706

Answers (2)

g0rb
g0rb

Reputation: 2389

I ran into the same issue. Odd that there aren't more Stackoverflow posts about this. The above answer did not work for me, but this is how I solved it in the case that there are more out there.

@Directive({
  selector: '[hybridFormControl]'
})
class HybridFormControl Directive extends FormControlName implements ControlValueAccessor, OnChanges {
  @Input('hybridFormControl') name: string;

  onChange;
  onTouched;

  constructor(
      @Optional() protected formGroupDirective: FormGroupDirective,
      @Optional() @Self() @Inject(NG_VALUE_ACCESSOR) valueAccessors: ControlValueAccessor[],
      private fb: FormBuilder,
      private renderer: Renderer2,
      private element: ElementRef
  ) {
    super(formGroupDirective, [], [], valueAccessors, null);
    this.valueAccessor = this;
  }

  ngOnChanges(changes: SimpleChanges): void {
    if (!this._registered) {
      // dynamically create the form control model on the form group model.
      this.formGroup = this.formGroupDirective.form;
      this.formGroup.registerControl(name, this.fb.control(''));
      this._registered = true;
    }

    // IMPORTANT - this ties your extended form control to the form control 
    // model on the form group model that we just created above. Take a look
    // at Angular github source code.
    super.ngOnChanges(changes); 
  }

  @HostListener('input', ['$event.target.value'])
  @HostListener('change', ['$event.target.value'])
  onInput(value): void {
    this.onChange(modelValue);
  }

  writeValue(value): void {
    const element = this.element.nativeElement;
    this.renderer.setProperty(element, 'value', value);
  }

  registerOnChange(fn): void {
    this.onChange = fn;
  }

  registerOnTouched(fn): void {
    this.onTouched = fn;
  }
}

And this hybrid component could be used like:

@Component({
  selector: 'app',
  template: `
    <form formGroup=[formGroup]>
      <input type="text" hybridFormControl="myName">
    </form>
  `
class AppComponent {
  formGroup: FormGroup

  constructor(fb: FormBuilder) {
    this.form = this.fb.group({});
  }
}

Sources: https://github.com/angular/angular/blob/master/packages/forms/src/directives/reactive_directives/form_control_name.ts#L212

Upvotes: 0

rusev
rusev

Reputation: 1920

You are almost there. There is one more trick though. There isn't DefaultValueAccessor for that input element, thus constructor arguments are populate with null value.

The formControl \ formControlName selectors appear in one more place - the value accessor. In order your directive to work you should implement all default value accessors for the hybridFormControl directive ( following the pattern for the built-in directives).

P.S I believe the provider of your directive should be corrected to

providers: [{
    provide: NgControl, //<-- NgControl is the key
    useExisting: forwardRef(() => HybridFormControlDirective)
}]

Upvotes: 3

Related Questions