-
-
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
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