Skip to content

Instantly share code, notes, and snippets.

@cyrillbrito
Last active October 2, 2025 15:43
Show Gist options
  • Select an option

  • Save cyrillbrito/f387212029bcc97287088297492c54d8 to your computer and use it in GitHub Desktop.

Select an option

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);
}
@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

@tonysamperi
Copy link

Thanks for this. I needed to use it both with formControlName and ngModel...
Lost 1 hour wondering why some logic didn't work, before realizing the ngModel wasn't working with the original hack...

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