angulartypescriptlessangular17

How can I dynamically add wrapper to an Angular component?


In my project I have wrapper classes that are reused often and some contain the 'gap' css property. Now I have a component that loads data asynchronously and that component is hidden if the returned data is empty.

The problem I face is that the wrappers I used to surround my component are still displayed and therefore cause the gap to activate. Below is an example with two wrappers, but depending on the use-cases it could be more or other wrappers that are needed.

At the moment:

Working data:

<card-wrapper>
  <specific-margin-wrapper>
    <component-loading-data />
  </specific-margin-wrapper>
</card-wrapper>

Without data:

<card-wrapper>
  <specific-margin-wrapper>
    <component-loading-data style="display: none" />
  </specific-margin-wrapper>
</card-wrapper>

My goal:

Working data:

<card-wrapper>
  <specific-margin-wrapper>
    <component-loading-data />
  </specific-margin-wrapper>
</card-wrapper>

Without data:

<card-wrapper style="display: none" >
  <specific-margin-wrapper style="display: none" >
    <component-loading-data style="display: none" />
  </specific-margin-wrapper>
</card-wrapper>

I want to "display: none" the wrapper classes when the child-component is not displayed too.

I tried to use css pseudo classes like :empty, but that didn't fully work:

:host:empty {
  display: none;
}

Is there a css (less) solution that works like :empty but includes children with display:none ? Or some ngTemplate / ngContent magic that let's me dynamically add wrapper inside my component.

Ideally I want a solution within my "component-loading-data" so that I don't need to change the parent component where I use it

EDIT: Added StackBlitz example


Solution

  • Unfortunately, I didn't find a solution inside my loading-component, but I have a somewhat clean solution by referencing a parent element via id in the template.

    I've created a directive that allows to hide given elements:

    @Directive()
    export abstract class HideableContainerDirective {
      private readonly platformId = inject(PLATFORM_ID);
      private readonly document: Document = inject(DOCUMENT);
      private readonly renderer: Renderer2 = inject(Renderer2);
    
      @Input() public hideableComponentId?: string;
    
      protected hideComponent(): void {
        if (!isPlatformBrowser(this.platformId)) return;
    
        if (this.hideableComponentId) {
          const element = this.document.getElementById(this.hideableComponentId);
          if (element) {
            this.renderer.setStyle(element, 'display', 'none');
          }
        }
      }
    }
    

    Components that need to be hidden can extend the Directive and call the hideComponent() function accordingly.

    export class MessageContainerComponent extends HideableContainerDirective implements OnInit {
    
      public ngOnInit(): void {
        const data = this.loadData();
        if (data === undefined) {
          this.hideComponent();
        }
      }
    }
    

    And set an id in the template to hide the referenced component:

    <card-wrapper id='custom_data_container'>
      <specific-margin-wrapper>
        <component-loading-data [hideableComponentId]="'custom_data_container'" />
      </specific-margin-wrapper>
    </card-wrapper>