3

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>
Koby Mizrahy
  • 1,361
  • 2
  • 12
  • 23
  • Did you find a solution for this? Thanks – CoderBang May 13 '21 at 18:26
  • This question is more-or-less reproducible in the [example](https://angular.io/generated/live-examples/dynamic-component-loader/stackblitz.html) provided in the [angular docs](https://angular.io/guide/dynamic-component-loader). If you inspect the html rendered in that example, the ad banner is surrounded by an `` tag. How can we create the ad banner without the `` wrapper? – Him Apr 04 '23 at 21:27
  • [This looks to be basically the same question](https://stackoverflow.com/questions/56903646/angular-8-remove-ng-component-tag-table-row-template) also unanswered. – Him Apr 04 '23 at 21:32
  • @Him In the example you are linking to, both the `hero-job-add.component` and the `hero-profile.component` are missing a custom selector in the component declaration hence it falls back to the default `ng-component` selector. Check out [this fork of the example](https://stackblitz.com/edit/angular-aq95qs?file=src/app/hero-profile.component.ts) where I declared selectors in both components. The `` in the html output is replaced accordingly. – Wilt Apr 06 '23 at 18:23
  • 1
    @Him also added an answer [to the other question](https://stackoverflow.com/questions/56903646/angular-8-remove-ng-component-tag-table-row-template) you were referring to, but in that case the solution provided is a simple css hack. – Wilt Apr 06 '23 at 20:37
  • 1
    @Wilt the answer to the other question solves my personal use case. – Him Apr 07 '23 at 03:16

1 Answers1

1

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.

UPDATE

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.

Wilt
  • 41,477
  • 12
  • 152
  • 203
  • @Him see my recently added answer. Maybe it will help you? – Wilt Apr 05 '23 at 13:19
  • @Him see my recently added update, there actually is an alternative. I forgot that possibility since I hardly ever use attribute selectors myself. – Wilt Apr 13 '23 at 09:18