-
-
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); | |
} |
Any solution for work with ngmodel?
@Dimonina why is memory leak? can you explain?
the subscription is created, but there's no unsubscription, so it will live forever even after your components are destroyed
But since it is associated with the form, it won't die automatically? How can you check to see if the variable stays alive? Thanks @Dimonina
@pookdeveloper this solution does work with ngModel. As for the leak I am not sure how we would test it. But if indeed it is a leak we need to return the subscription to the caller and the caller most run the unsubscribe onDestroy. In the v16 we might be able to avoid this with the destroyRef
@cyrillbrito Ok thanks, this ca be done with directive ? Can you share an example ??
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
@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
Line 17 - memory leak, no unsubscription