I tried using reference" /> I tried using reference" /> I tried using reference"/>

How to remove parent element if ng-content is empty

3.3k Views Asked by At

I want to hide remove div.parent if ng-content is empty from bellow code

<div class="parent">
 <ng-content></ng-content>
</div>

I tried using reference like bellow, but seems to be not working

<div class="parent" *ngIf="!child.children.length">
  <div class="child"> 
    <ng-content></ng-content>
  </div>
</div>

Is there a better approach to remove parent element if ng-content is empty?

6

There are 6 best solutions below

7
Joosep Parts On

You can not reference a class like that. I think you want to reference an element, for that you would use # on the element. But that won't help you either. Parent is rendered before child is and if parent is removed from DOM because of child changes you would stumble on an error Expression Changed After It Has Been Checked. For that I have implemented to viewChild of parentRef in cycle ngAfterContentChecked and from that we count if child has any children (if ng-container has any elements). If it does have children we remove parent.

If for some reason you want to remove parent a later time you can call removeParent() to do so.

Edit children = [1,2,3] to observe effects of having children and not having them.

<p>Remove parent if child is empty (look console)</p>
<br>
<div class="parent" *ngIf="!hideParent">
  I am parent
  <div class="child" #childRef> 
    <ng-container *ngFor="let child of children">
      <div>I am child nr: # {{child}}<br></div>
    </ng-container>
  </div>
</div>
@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements AfterContentChecked {
  name = 'Angular';
  @ViewChild('childRef') child: ElementRef;
  hideParent: boolean = false;
  children = [1, 2, 3];
  parentElem: any;

  ngAfterContentChecked() {
    this.removeParent();
  }

  removeParent() {
    if (this.child) {
      let childrenCount = this.child.nativeElement.childElementCount;
      console.log('child: ', this.child);
      console.log('child elements in child: ', childrenCount);
      if (childrenCount == 0) {
        console.log('hiding');
        this.hideParent = true;
      }
    }
  }
}

Here is a working example: https://stackblitz.com/edit/angular-4krctv?file=src%2Fapp%2Fapp.component.html

6
James D On

Your name for parent is confusing considering the ng-content holder is normally the child of another component which calls it.

<div class="parent">
 <ng-content></ng-content>
</div>

The calling HTML would look something like this: (where your ng-content is in app-footer)

<app-footer>
   <p>MY CONTENT TO THE ng-content THINGY</p>
</app-footer>

You'll want to upgrade your app-footer (ng-content component) to have an input field

import { Component, Input } from '@angular/core'; 

//.......
export class FooterComponent {
  @Input() displayParentDiv = true; 

And in you template

<div class="parent" *ngIf="displayParentDiv">
 <ng-content></ng-content>
</div>

And then in your footer (ng-content holder) file:

<p>Pass the input false if you don't want it displayed:</p>
<app-footer [displayParentDiv]="false">
   <p>MY CONTENT TO THE ng-content THINGY</p>
</app-footer>

<p> To display it, don't give it any value considering default is true</p>
<app-footer>
   <p>MY CONTENT TO THE ng-content THINGY</p>
</app-footer>

Example code of a object which I reuse in many of my projects is the use of a material icon navigation item for a toolbar is the code below. Notice that I defaulted all the values, making them not obligated fields, I could for instance only pass an material icon reference and no hint or routerlink

import { Component, Input, Output, EventEmitter } from '@angular/core';

@Component({
  selector: 'ps-nav-item',
  template: `
    <a mat-list-item [routerLink]="routerLink" (click)="navigate.emit()">
      <mat-icon mat-list-icon>{{ icon }}</mat-icon>
      <span mat-line><ng-content></ng-content></span>
      <span mat-line class="secondary">{{ hint }}</span>
    </a>
  `,
  styles: [
    `
      .secondary {
        color: rgba(0, 0, 0, 0.54);
      }
    `,
  ],
})
export class NavItemComponent {
  @Input() icon = '';
  @Input() hint = '';
  @Input() routerLink: string | any[] = '/';
  @Output() navigate = new EventEmitter();
}

The parents HTML then calls this multiple times:

        <ps-nav-item (navigate)="closeSidenav()" *ngIf="loggedIn$ | async" routerLink="/" icon="book" hint="View your report collection">
          My Collection
        </ps-nav-item>
        <ps-nav-item (navigate)="closeSidenav()" *ngIf="loggedIn$ | async" routerLink="/reports/find" icon="search" hint="Search for a report!">
          Browse Reports
        </ps-nav-item>
        <ps-nav-item (navigate)="closeSidenav()" *ngIf="!(loggedIn$ | async)">
          Sign In
        </ps-nav-item>
        <ps-nav-item (navigate)="logout()" *ngIf="loggedIn$ | async">
          Sign Out
        </ps-nav-item>

ref angular docs

0
James D On

If you have no access to the ng-content nor to the possible input from the people using your components, then you have to validate it with javascript or CSS

CSS method:

.displayNoneWhenEmpty:empty{
  display:none;
}
<div class="displayNoneWhenEmpty">Not Empty</div>

<div class="displayNoneWhenEmpty"></div>

reference

Javascript method:

Add an id to the parent div and a boolean check:

<div class="parent" id="parentDivId" *ngIf="hideParent">
 <ng-content></ng-content>
</div>

Then in your component add a boolean variable, which you set in the constructor

hideParent: boolean = false;
constructor(){
    hideParent = document.getElementById("parentDivId")?.childElementCount > 0;
}
0
JimmyTheCode On

I've achieved like this:

<div #parentRef *ngIf="showParent" class="parent">
 <ng-content></ng-content>
</div>
@ViewChild('parentRef') parentRef: ElementRef;

  showParent: boolean = true;;

  constructor(private cdRef:ChangeDetectorRef){  }

  ngAfterViewInit(){
    if(!this.parentRef?.nativeElement.children){
      this.showParent = false;
    }
    this.cdRef.detectChanges();
  }

This feels very hacky, but I haven't found a neater solution in a few hours of playing.

(I introduced this.cdRef.detectChanges(); in order to prevent the expression has changed after it was checked... error)

0
Otto On

My team uses a very Angular solution. Doesn't require any CSS or DOM checking/manipulating.

May only work if you're selecting a specific sub-set of the content supplied by the consumer, using <ng-content select=... >.

In the TS you can dynamically check what content has been supplied by the consumer using @ContentChildren.

@ContentChildren(MyDirective)
myDirectiveChildren: QueryList<MyDirective>;

If there's at least one content child, show parent the div. Otherwise hide the whole thing.

<div *ngIf="myDirectiveChildren.length > 0">
 <ng-content select="[myDirective]"></ng-content>
</div>

This list of content is updated dynamically by Angular. It also doesn't care if that parent element exists or not, the list represents the content supplied by the consumer regardless of where/if it's displayed.

Unfortunately, I just can't find any way to get @ContentChildren() to include all child content, it always takes a parameter. A solution where the consumer doesn't have to tag any content with a directive would be welcome.

0
nawfel bgh On

Here is a CSS solution which hides the parent (display: none) instead of removing it from the DOM:

@Component({
  selector: 'my-component',
  template: `
    <div class="parent">
      Parent content (hidden when ng-content is empty)
      <div class="ng-content-container">
        <ng-content></ng-content>
      </div>
    </div>
  `,
  styles: [
    `
      .parent:has(.ng-content-container:empty) {
        display: none;
      }
    `,
  ],
})
export class NavItemComponent {}