I'm currently trying to create a card component for my application as learning purposes. My goal to get a generic reusable card component. I do not want the div with the class class-footer be loaded in when I'm not using the card-footer directive
card.component.ts
import { Component, Directive } from '@angular/core';
@Directive({
selector: 'card-header',
host: {
'class': 'card-header',
},
})
export class CardHeaderDirective {}
@Directive({
selector: 'card-content',
host: {
'class': 'card-content',
},
})
export class CardContentDirective {}
@Directive({
selector: 'card-footer',
host: {
'class': 'card-footer',
},
})
export class CardFooterDirective {}
@Component({
selector: 'app-card',
standalone: true,
imports: [],
templateUrl: './card.component.html',
styleUrl: './card.component.scss',
})
export class CardComponent {
}
card.component.html
<div class="card">
<div class="card-header">
<ng-content select="[card-header]"></ng-content>
</div>
<div class="card-content">
<ng-content select="[card-content]"></ng-content>
</div>
<div class="card-footer">
<ng-content select="[card-footer]"></ng-content>
</div>
</div>
here's an example of how I would use it:
<app-card>
<div card-header>
<span>Header</span>
</div>
<div card-content>
<span>Some content</span>
</div>
<div card-footer>
<span>Sometimes I'm using this and sometimes not</span>
</div>
</app-card>
The problem lays into that the class card-footer has a border-top for UI reasons. Sometimes I want to use this component but not use the card-footer. I don't want to see that border-top, but ofcourse it's now always there because of the div.
So far I've tried using @if and optionally load in the div card-footer when checking the content using ContentChildren of ViewChild in AfterContentInit but no luck
I see three possible solutions for your particular case:
You can use ViewChild
on the footer container div element and check for its content length; if length more than 0 show the footer otherwise hide the footer.
export class CardComponent implements AfterViewInit {
public emptyFooter: boolean;
@ViewChild('footer') public footer: ElementRef<HTMLDivElement>;
constructor(private changeDetectorRef: ChangeDetectorRef) {
}
public ngAfterViewInit(): void {
this.emptyFooter = this.footer.nativeElement.children.length === 0;
this.changeDetectorRef.detectChanges();
}
}
Checking for content needs to be done after view init and to prevent a ExpressionChangedAfterItHasBeenCheckedError
we call detectChanges()
after setting this.emptyFooter
.
Here is a working StackBlitz with a simplified working example.
You can improve the example to your needs and use it.
You could also consider replacing the div
elements that you are using for projecting your content with ng-container
elements and solve the styling of your header, content and footer purely with the elements that are projected. Like that when there is no footer there will be nothing rendered in the first place (so also no need to hide anything).
See another working StackBlitz here with the pure css example.
You can also use ContentChild
and it seems like this last example will be more in line what you were trying, but not sure if that is the best solution to get to the result you were after.
In this example there is actually also no need to do anything with the content children inside the card component. I am getting them and outputting them in console for demonstration purposes, but I am actually not utilizing them in any way; hiding the footer is again purely resolved with the css classes added by the directives.
See a third StackBlitz with the ContentChild example here.
The main mistake you made to make this example work was that the selector needs to be wrapped in []
. So:
@Directive({
selector: '[card-footer]',
host: {
'class': 'card-footer',
},
})
export class CardFooterDirective {}