-
-
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 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 {
}
}@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
Nice @cyrillbrito :)
@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});
@cyrillbrito I get and error with your example:

@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
@cyrillbrito your HostControlDirective is my NoopValueAccessorDirective i only use this name , but i hace the same code
The problem is that I have move the code in NgOninit to constructor.
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
@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.
@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.
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();
}));
Hi @cyrillbrito when I change the reference of a form group the formcontrols dosent update the reference
Do you have the working example? With an example usage? Thanks in advance!
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:
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...


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