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