I am building a directive for my app that creates dynamically component and inject it right after the element it decorates.
The directive is working perfectly except one issue: When the component is added to the view by the directive, it contains wrapping tag around my template:
The component template is: <mat-error>...</mat-error>
But when the component added to the view the html is: <ng-component><mat-error>...</mat-error></ng-component>
Is there anyway to void the wrapping <ng-component></ng-component> ?
Here is the source code of my directive:
import { Directive, Self, Inject, Optional, Host, ComponentRef, ViewContainerRef, ComponentFactoryResolver, OnDestroy } from '@angular/core';
import { NgControl } from '@angular/forms';
import { untilDestroyed } from 'ngx-take-until-destroy';
import { InjectionToken } from '@angular/core';
import { merge, Observable, EMPTY } from 'rxjs';
import { FormSubmitDirective } from './form-submit.directive';
import { ControlErrorComponent } from './control-error.component';
import { ControlErrorContainerDirective } from './control-error-container.directive';
export const defaultErrors = {
required: (error) => `This field is required`,
minlength: ({ requiredLength, actualLength }) => `Expect ${requiredLength} but got ${actualLength}`
}
export const FORM_ERRORS = new InjectionToken('FORM_ERRORS', {
providedIn: 'root',
factory: () => defaultErrors
});
@Directive({
selector: '[formControl], [formControlName]'
})
export class ControlErrorsDirective implements OnDestroy{
submit$: Observable<Event>;
ref: ComponentRef<ControlErrorComponent>;
container: ViewContainerRef;
constructor(
private vcr: ViewContainerRef,
@Optional() controlErrorContainer: ControlErrorContainerDirective,
@Self() private control: NgControl,
@Optional() @Host() private form: FormSubmitDirective,
@Inject(FORM_ERRORS) private errors,
private resolver: ComponentFactoryResolver) {
this.submit$ = this.form ? this.form.submit$ : EMPTY;
console.log("Form: ", this.form);
this.container = controlErrorContainer ? controlErrorContainer.vcr : vcr;
}
ngOnInit() {
merge(
this.submit$,
//this.control.valueChanges,
this.control.statusChanges,
).pipe(
untilDestroyed(this)).subscribe((v) => {
const controlErrors = this.control.errors;
if (controlErrors) {
const firstKey = Object.keys(controlErrors)[0];
const getError = this.errors[firstKey];
const text = getError(controlErrors[firstKey]);
this.setError(text);
} else if (this.ref) {
this.setError(null);
}
})
}
ngOnDestroy(): void {
console.log("Destroy control-error directive");
}
setError(text: string) {
console.log("set error: ", text);
if (!this.ref) {
console.log("Create error control");
//
// Here I am injecting dynamically the control to the view
//
const factory = this.resolver.resolveComponentFactory(ControlErrorComponent);
this.ref = this.vcr.createComponent(factory);
}
this.ref.instance.text = text;
}
}
And here is the source code of the Component created dynamically:
import { Component, ChangeDetectionStrategy, Input, ChangeDetectorRef } from '@angular/core';
@Component({
template: `<mat-error class="help is-danger" [class.hide]="_hide">{{_text}}</mat-error>`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ControlErrorComponent {
_text: string;
_hide = true;
@Input() set text(value) {
if (value !== this._text) {
this._text = value;
this._hide = !value;
this.cdr.detectChanges();
}
};
constructor(private cdr: ChangeDetectorRef) { }
}
An this is the HTML output when the control created:
<ng-component class="ng-star-inserted"><mat-error class="help is-danger mat-error" role="alert" id="mat-error-2">This field is required</mat-error></ng-component>
This happens because your didn't set a selector for your custom component, in such cases Angular falls back on the default value ng-component as the selector. You will see that if you change your component declaration with your own custom selector it will change ng-component accordingly into the declared value. In the example below with my-control-error:
@Component({
selector: 'my-control-error',
template: `<mat-error class="help is-danger" [class.hide]="_hide">{{_text}}</mat-error>`,
changeDetection: ChangeDetectionStrategy.OnPush
})
The problem that you will have now is that your output is double wrapped with both <my-control-error> and the <mat-error> selector which is probably also not what you want. A component simply will be wrapped in a selector in the html, that is nothing you can work around. You should in such cases consider using a directive instead of a component. (See update at the end of the answer for an alternative)
Maybe you should skip using the <mat-error> in your template completely and use your own error component instead!?
import { Component, ChangeDetectionStrategy, HostBinding, Input, ChangeDetectorRef } from '@angular/core';
@Component({
selector: 'my-control-error',
template: '{{_text}}',
host: {
class: 'help is-danger'
}
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ControlErrorComponent {
public _text: string;
@HostBinding('class.hide') public _hide: boolean = true;
@Input() set text(value) {
if (value !== this._text) {
this._text = value;
this._hide = !value;
this.cdr.detectChanges();
}
};
constructor(private cdr: ChangeDetectorRef) { }
}
I used @HostBinding which will bind the class hide depending on whether it resolves to true or false. The static classes I added using the host metadata property to the component. Your compiler might complain about this depending on your configuration, then you can also remove it and add it similarly with @HostBinding:
@HostBinding('class') public _class = 'help is-danger';
I suggest using your own custom error classes. The above example is probably more like what you were after in the first place. If not please leave a comment on how this is not sufficing our needs.
A possible alternative to the element selector is an attribute selector which is commonly used for directives, but it can also be used for components.
I will refer to the answer from @Eliseo to another question about the Angular component selector nicely demonstrating the difference between component selectors and attribute selectors.
And here a nice blog post on Medium about using the attribute selector for Angular components.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With