Skip to content

Instantly share code, notes, and snippets.

@cyrillbrito
Last active May 15, 2025 08:53
Show Gist options
  • Save cyrillbrito/f387212029bcc97287088297492c54d8 to your computer and use it in GitHub Desktop.
Save cyrillbrito/f387212029bcc97287088297492c54d8 to your computer and use it in GitHub Desktop.
@Directive({
standalone: true,
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: HostControlDirective,
},
],
})
export class HostControlDirective implements ControlValueAccessor {
control!: FormControl;
private injector = inject(Injector);
private subscription?: Subscription;
ngOnInit(): void {
const ngControl = this.injector.get(NgControl, null, { self: true, optional: true });
if (ngControl instanceof FormControlName) {
const group = this.injector.get(ControlContainer).control as UntypedFormGroup;
this.control = group.controls[ngControl.name!] as FormControl;
return;
}
if (ngControl instanceof FormControlDirective) {
this.control = ngControl.control;
return;
}
if (ngControl instanceof NgModel) {
this.subscription = ngControl.control.valueChanges.subscribe(newValue => {
// The viewToModelUpdate updates the directive and triggers the ngModelChange.
// So we want to called it when the value changes except when it comes from the parent (ngModel input).
// The `if` checks if the newValue is different from the value on the ngModel input or from the current value.
if (ngControl.model !== newValue || ngControl.viewModel !== newValue) {
ngControl.viewToModelUpdate(newValue);
}
});
this.control = ngControl.control;
return;
}
// Fallback
this.control = new FormControl();
}
writeValue(): void { }
registerOnChange(): void { }
registerOnTouched(): void { }
ngOnDestroy(): void {
this.subscription?.unsubscribe();
}
}
// Usage example
@Component({
selector: 'app-custom-input',
template: `<input [formControl]="hcd.control" />`,
standalone: true,
imports: [ReactiveFormsModule],
hostDirectives: [HostControlDirective],
})
export class CustomInputComponent {
hcd = inject(HostControlDirective);
}
@Dimonina
Copy link

you can test the unsubscription by

ngControl.control.valueChanges.pipe(
finalize(() => console.log('will never be called'))
).subscribe(....)

As for the host control, I finally came to this approach: https://netbasal.com/forwarding-form-controls-to-custom-control-components-in-angular-701e8406cc55

@pookdeveloper
Copy link

pookdeveloper commented Apr 19, 2023

@cyrillbrito what do you think about this approach: I use NoopValueAccessorDirective by directive:
pd/ if there is any better approach feel free to tell me :)

import { Component, Input, OnDestroy } from '@angular/core';
import { injectNgControl, NoopValueAccessorDirective } from '../noop-value-accessor/noop-value-accessor.directive';
import { Subject } from 'rxjs';

@Component({
  selector: 'app-new-wrapper-form-control',
  templateUrl: './new-wrapper-form-control.component.html',
  styleUrls: ['./new-wrapper-form-control.component.scss'],
  hostDirectives: [NoopValueAccessorDirective]
})
export class NewWrapperFormControlComponent implements OnDestroy {
  //
  _control;
  _destroyed: Subject<any>;

  constructor() {
    this._control = injectNgControl(this._destroyed);
  }

  ngOnDestroy() {
    this._destroyed.next({});
    this._destroyed.complete();
  }
}
import {
  ControlValueAccessor, FormControlDirective, FormControlName, NG_VALUE_ACCESSOR, NgControl, NgModel
} from '@angular/forms';
import { Directive, inject } from '@angular/core';
import { Subject, takeUntil } from 'rxjs';

export function injectNgControl(_destroyed: Subject<any>): any {
  const ngControl = inject(NgControl, {self: true, optional: true});
  // if (!ngControl) throw new Error('...');
  if (
    ngControl instanceof FormControlDirective ||
    ngControl instanceof FormControlName ||
    ngControl instanceof NgModel
  ) {
    // The viewToModelUpdate updates the directive and triggers the ngModelChange.
    // So we want to called it when the value changes except when it comes from the parent (ngModel input).
    // The `if` checks if the newValue is different from the value on the ngModel input or from the current value.
    if (ngControl instanceof NgModel) {
      ngControl.control.valueChanges
        .pipe(takeUntil(_destroyed))
        .subscribe(newValue => {
          if (ngControl.model !== newValue || ngControl.viewModel !== newValue) {
            ngControl.viewToModelUpdate(newValue);
          }
        });
    }
    return ngControl;
  }
  // throw new Error('...');
}

@Directive({
  standalone: true,
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      multi: true,
      useExisting: NoopValueAccessorDirective,
    },
  ],
  selector: '[appNoopValueAccessor]'
})
export class NoopValueAccessorDirective implements ControlValueAccessor {
  writeValue(obj: any): void {
  }

  registerOnChange(fn: any): void {
  }

  registerOnTouched(fn: any): void {
  }

}

@cyrillbrito
Copy link
Author

@pookdeveloper I updated the code to take advantage of the hostDirectives and fixed the memory leak. I think it looks really clean now, and it works with ngModel, formControl and formControlName

@pookdeveloper
Copy link

Nice @cyrillbrito :)

@pookdeveloper
Copy link

@cyrillbrito nice work, only .. Inject flags are deprecated:
const ngControl = this.injector.get(NgControl, null, InjectFlags.Self + InjectFlags.Optional);
Now:
const ngControl = this.injector.get(NgControl, null, {self: true, optional: true});

@pookdeveloper
Copy link

@cyrillbrito I get and error with your example:
image

@cyrillbrito
Copy link
Author

@pookdeveloper Fixed the depredated flags, thanks. Maybe use are using NoopValueAccessorDirective together with my HostControlDirective and since both provide NG_VALUE_ACCESSOR it is causing problems. With my solution you don't need the NoopValueAccessorDirective since it is already incorporated

@pookdeveloper
Copy link

pookdeveloper commented Apr 28, 2023

@cyrillbrito your HostControlDirective is my NoopValueAccessorDirective i only use this name , but i hace the same code

@pookdeveloper
Copy link

The problem is that I have move the code in NgOninit to constructor.

@Xriuk
Copy link

Xriuk commented Oct 27, 2023

I had some problems with array values, the code above caused the changes to be triggered twice, so I changed the condition to this:

this.subscription = ngControl.control.valueChanges.subscribe(newValue => {
  // The viewToModelUpdate updates the directive and triggers the ngModelChange.
  // So we want to called it when the value changes except when it comes from the parent (ngModel input).
  // The `if` checks if the newValue is different from the value on the ngModel input or from the current value.
  if (ngControl.model !== newValue && ngControl.viewModel !== newValue) {
    ngControl.viewToModelUpdate(newValue);
  }
});

So that only when both models are different it triggers the update. Now it seems to work

@jacobfederer
Copy link

@cyrillbrito I like your solution! But how do you test it? I tried to provide a custom version of NgControl, but the self option of the Injector.get seems to prevent this. In my dependent component I'm unable to inject a working version of NgControl.

@cyrillbrito
Copy link
Author

@jacobfederer Did not really understand your question. What do you mean by "custom version of NgControl" ?
There is an example of how to use in the snippet.

@jacobfederer
Copy link

jacobfederer commented Apr 24, 2025

Yes, I just wanted to write a unit test for my component and wondered on how to inject an instance of NgControl in this case. But I found a solution for it. This seems to work:

let testFormControl: FormControl


 beforeEach(waitForAsync(() => {

   testFormControl = new FormControl(null);

   const formControlDirective = new FormControlDirective([], [], [
// At least one control value accessor is required
     {writeValue: () => null, registerOnChange: () => null, registerOnTouched: () => null}
   ], null, null);

   formControlDirective.form = testFormControl
   formControlDirective.name = "test"

   TestBed.configureTestingModule({
     imports: [ParentFormControlDirective, TextAreaComponent, NoopAnimationsModule],
   }).overrideComponent(NextTextAreaComponent, {
     
 // A regular provider does not work with the injector self-flag, you need to override it like this
     set: {
       providers: [
         {
           provide: NgControl,
           useValue: formControlDirective
         }
       ]
     }
   })
     .compileComponents();

 }));

@pookdeveloper
Copy link

Hi @cyrillbrito when I change the reference of a form group the formcontrols dosent update the reference

@vivekraj-kr
Copy link

Do you have the working example? With an example usage? Thanks in advance!

@pookdeveloper
Copy link

of course @vivekraj-kr as you can see in the image when I use the control with Control, it always binds me to the reference of the first form:

https://stackblitz.com/edit/stackblitz-starters-syppdkqc?file=src%2Fapp%2Fcustom-input-hostcontrol.component.ts

image

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment