Dans Angular 2, comment vérifier si <ng-content> est vide?

92

Supposons que j'ai un composant:

@Component({
    selector: 'MyContainer',
    template: `
    <div class="container">
        <!-- some html skipped -->
        <ng-content></ng-content>
        <span *ngIf="????">Display this if ng-content is empty!</span>
        <!-- some html skipped -->
    </div>`
})
export class MyContainer {
}

Maintenant, je voudrais afficher du contenu par défaut si <ng-content>pour ce composant est vide. Existe-t-il un moyen simple de le faire sans accéder directement au DOM?

Slawomir Dadas
la source
1
Possible duplicata de Comment vérifier si ng-content existe
titusfx
Pour info, je sais que la réponse acceptée fonctionne, mais je pense qu'il est préférable de passer un paramètre d'entrée de type "useDefault" aux composants, par défaut à false.
bryan60

Réponses:

97

Enveloppez ng-contentdans un élément HTML comme a divpour obtenir une référence locale à celui-ci, puis liez l' ngIfexpression à ref.children.length == 0:

template: `<div #ref><ng-content></ng-content></div> 
           <span *ngIf="ref.nativeElement.childNodes.length == 0">
              Display this if ng-content is empty!
           </span>`
pixelbits
la source
24
N'y a-t-il pas d'autre moyen? Parce que c'est moche, comparé aux slots de repli Aurelia
Astronaut
18
C'est plus sûr à utiliser ref.children.length. childNodes contiendra des textnœuds si vous formatez votre html avec des espaces ou de nouvelles lignes, mais les enfants seront toujours vides.
parlement
5
Il y a une demande de fonctionnalité pour une meilleure méthode sur le suivi des problèmes angulaires: github.com/angular/angular/issues/12530 (cela pourrait valoir la peine d'ajouter un +1 ici).
eppsilon
5
J'étais sur le point de poster que cela ne semblait pas fonctionner jusqu'à ce que je réalise que j'ai utilisé l'exemple de à la ref.childNodes.length == 0place de ref.children.length == 0. Cela aiderait beaucoup si vous pouviez modifier la réponse pour qu'elle soit cohérente. Erreur facile, ne pas vous dénigrer. :)
Dustin Cleveland
3
J'ai suivi cet exemple et cela fonctionne très bien pour moi. Mais j'ai utilisé à la !ref.hasChildNodes()place deref.nativeElement.childNodes.length == 0
Sergey Dzyuba
34

Il manque des réponses @pixelbits. Nous devons vérifier non seulement la childrenpropriété, car tout childrensaut de ligne ou espace dans le modèle parent entraînera un élément avec du texte vide \ sauts de ligne. Mieux vaut vérifier .innerHTMLet .trim()ça.

Exemple de travail:

<span #ref><ng-content></ng-content></span>
<span *ngIf="!ref.innerHTML.trim()">
    Content if empty
</span>
lfoma
la source
1
meilleure réponse pour moi :)
Kamil Kiełczewski
Pouvons-nous utiliser cette méthode pour vérifier si l'élément enfant est vide et masquer le parent si c'est le cas? J'ai essayé, pas de chance
Kavinda Jayakody
@KavindaJayakody Oui, cela vérifiera que l'élément enfant est vide. Montrez votre code.
lfoma
* ngIf = "! ref.innerHTML.trim ()" Cette partie entraînera des problèmes de performances. Trim est O (n).
RandomCode le
1
@RandomCode a raison, la fonction sera appelée plusieurs fois. Une meilleure façon est de vérifier element.children.length ou element.childnodes.length, en fonction de ce que vous voulez demander.
Stefan Rein
31

MODIFIER 17.03.2020

CSS pur (2 solutions)

Fournit du contenu par défaut si rien n'est projeté dans ng-content.

Sélecteurs possibles:

  1. :only-childsélecteur. Voir cet article ici:: Sélecteur pour enfant unique

    Celui-ci nécessite moins de code / balisage. Prise en charge depuis IE 9: Puis-je utiliser: enfant unique

  2. :emptysélecteur. Lisez plus loin.

    Prise en charge d'IE 9 et partiellement depuis IE 7/8: https://caniuse.com/#feat=css-sel3

HTML

<div class="wrapper">
    <ng-content select="my-component"></ng-content>
</div>
<div class="default">
    This shows something default.
</div>

CSS

.wrapper:not(:empty) + .default {
    display: none;
}

Au cas où ça ne marche pas

Sachez qu'avoir au moins un espace blanc est considéré comme n'étant pas vide. Angular supprime les espaces, mais juste au cas où ce ne serait pas le cas:

<div class="wrapper"><!--
    --><ng-content select="my-component"></ng-content><!--
--></div>

ou

<div class="wrapper"><ng-content select="my-component"></ng-content</div>
Stefan Rein
la source
1
C'était de loin la meilleure solution pour mon cas d'utilisation. Je ne me souvenais pas que nous avions une emptypseudoclasse css
julianobrasil
@Stefan le contenu par défaut sera de toute façon rendu ... il ne sera que masqué. Donc, pour un contenu par défaut complexe, ce n'est pas la meilleure solution. Est-ce correct?
BossOz
1
@BossOz Vous avez raison. Il sera rendu (le constructeur etc. sera appelé si vous avez un composant angulaire). Cette solution ne fonctionne que pour les vues stupides. Si vous avez une logique complexe, impliquant un chargement ou des trucs comme ça, votre meilleur pari à mon avis est d'écrire une directive structurelle, qui peut obtenir un modèle / une classe (comme vous voulez l'implémenter) et en fonction de la logique, puis rendre le le composant souhaité ou celui par défaut. La section des commentaires est bien trop petite pour un exemple. Je commente à nouveau pour un autre exemple que nous avons dans l'une de nos applications.
Stefan Rein
Une façon serait d'avoir un composant sélectionnant ContentChildren via une directive (requête avec DirectiveClass, mais utilisation comme directive structurelle, donc pas de bootstrapping initial impliqué simplement avec le balisage): <loader [data]="allDataWeNeed"> <desired-component *loadingRender><desired-component> <other-default-component *renderWhenEmptyResult></other-default-component> </loader>et dans le composant de chargement, vous pourriez afficher le chargement de performance perçu, tandis que les données sont en cours de chargement. Vous pouvez l'écrire / le résoudre de différentes manières. Ceci est une démonstration de la façon dont vous pourriez le résoudre.
Stefan Rein
21

Lorsque vous injectez le contenu, ajoutez une variable de référence:

<div #content>Some Content</div>

et dans votre classe de composant, obtenez une référence au contenu injecté avec @ContentChild ()

@ContentChild('content') content: ElementRef;

donc dans votre modèle de composant, vous pouvez vérifier si la variable de contenu a une valeur

<div>
  <ng-content></ng-content>
  <span *ngIf="!content">
    Display this if ng-content is empty!
  </span>    
</div> 
Lerner
la source
C'est la réponse la plus propre et bien meilleure. Il prend en charge l'API ContentChild prête à l'emploi. Je ne comprends pas pourquoi la première réponse a obtenu autant de votes.
CarbonDry
10
L'OP a demandé si ngContent est vide ou non - cela signifie <MyContainer></MyContainer>. Votre solution attend aux utilisateurs de créer un sous-élément sous MyContainer: <MyContainer><div #content></div></MyContainer>. Bien que ce soit une possibilité, je ne dirais pas que c'est supérieur.
pixelbits
8

Injectez elementRef: ElementRefet vérifiez si elementRef.nativeElementa des enfants. Cela pourrait fonctionner uniquement avecencapsulation: ViewEncapsulation.Native .

Enveloppez l' <ng-content>étiquette et vérifiez si elle a des enfants. Cela ne fonctionne pas avec encapsulation: ViewEncapsulation.Native.

<div #contentWrapper>
  <ng-content></ng-content>
</div>

et vérifiez s'il a des enfants

@ViewChild('contentWrapper') contentWrapper;

ngAfterViewInit() {
  contentWrapper.nativeElement.childNodes...
}

(pas testé)

Günter Zöchbauer
la source
3
J'ai voté contre cela en raison de l'utilisation de @ViewChild. Bien que "Légal", ViewChild ne doit pas être utilisé pour accéder aux nœuds enfants dans un modèle. C'est un anti-modèle parce que, bien que les parents connaissent les enfants, ils ne devraient pas s'accoupler avec eux car cela conduit généralement à un couplage pathologique ; une forme plus appropriée de transport de données ou de traitement des demandes consiste à utiliser des méthodologies basées sur les événements .
Cody
5
@Cody merci d'avoir publié un commentaire sur votre vote défavorable. En fait, je ne suis pas votre argumentation. Le modèle et la classe des composants sont une seule unité - un composant. Je ne vois pas pourquoi accéder au modèle à partir du code devrait être un anti-modèle. Angular (2/4) n'a presque rien de commun avec AngularJS sauf que les deux sont des frameworks Web et le nom.
Günter Zöchbauer
2
Gunter, vous faites deux très bons points. Angular ne doit pas nécessairement éclore avec un stigmate des voies d'AngularJS. Mais je marquerais que le premier point (et ceux que j'ai soulevés ci-dessus) se situent dans le caniveau philosophique de l'ingénierie. Cela dit, je suis devenu un expert du couplage de logiciels et je déconseillerais une utilisation [au moins intensive] de @ViewChild. Merci d'avoir ajouté un peu d'équilibre à mes commentaires - je trouve nos deux commentaires tout aussi dignes d'être pris en considération que l'autre.
Cody
2
@Cody peut-être que votre opinion sur le sujet @ViewChild()vient du nom. Les "enfants" ne sont pas nécessairement des enfants, ils ne sont que la partie déclarative du composant (comme je le vois). Si les instances de composant créées pour ce balisage sont des accès, il s'agit bien sûr d'une histoire différente, car cela nécessiterait des connaissances au moins sur leur interface publique et cela créerait en fait un couplage étroit. C'est une discussion très intéressante. Cela m'inquiète de ne pas avoir assez de temps pour réfléchir à de telles questions, car avec de tels cadres trop souvent, le temps est brûlé pour trouver un moyen.
Günter Zöchbauer
3
Ne pas exposer cela dans l'API est le véritable anti-pattern :-(
Simon_Weaver
5

Si vous voulez afficher un contenu par défaut, pourquoi n'utilisez-vous pas simplement le sélecteur 'only-child' de css.

https://www.w3schools.com/cssref/sel_only-child.asp

par exemple: HTML

<div>
  <ng-content></ng-content>
  <div class="default-content">I am deafult</div>
</div>

css

.default-content:not(:only-child) {
   display: none;
}
écosystème31
la source
Solution plutôt intelligente, si vous ne voulez pas vous occuper de ViewChild et des choses dans le composant. Je l'aime!
M'sieur Toph '
Veuillez noter que le contenu de transclusion doit être un nœud HTML (pas seulement du texte)
M'sieur Toph '
2

Avec Angular 10, cela a légèrement changé. Vous utiliseriez:

<div #ref><ng-content></ng-content></div> 
<span *ngIf="ref.children.length == 0">
  Display this if ng-content is empty!
</span>
John Hamm
la source
1

Dans mon cas, je dois masquer le parent du ng-content vide:

<span class="ml-1 wrapper">
  <ng-content>
  </ng-content>
</span>

Le CSS simple fonctionne:

.wrapper {
  display: inline-block;

  &:empty {
    display: none;
  }
}
smg
la source
1

J'ai implémenté une solution en utilisant @ContentChildren décorateur , qui est en quelque sorte similaire à la réponse de @ Lerner.

Selon la documentation , ce décorateur:

Obtenez la QueryList des éléments ou des directives du DOM de contenu. Chaque fois qu'un élément enfant est ajouté, supprimé ou déplacé, la liste de requêtes sera mise à jour et les modifications observables de la liste de requêtes émettront une nouvelle valeur.

Ainsi, le code nécessaire dans le composant parent sera:

<app-my-component>
  <div #myComponentContent>
    This is my component content
  </div>
</app-my-component>

Dans la classe de composants:

@ContentChildren('myComponentContent') content: QueryList<ElementRef>;

Ensuite, dans le modèle de composant:

<div class="container">
  <ng-content></ng-content>
  <span *ngIf="*ngIf="!content.length""><em>Display this if ng-content is empty!</em></span>
</div>

Exemple complet : https://stackblitz.com/edit/angular-jjjdqb

J'ai trouvé cette solution implémentée dans des composants angulaires, pour matSuffix, dans le composant form-field .

Dans la situation où le contenu du composant est injecté ultérieurement, après l'initialisation de l'application, on peut également utiliser une implémentation réactive, en souscrivant à l' changesévénement du QueryList:

export class MyComponentComponent implements AfterContentInit, OnDestroy {
  private _subscription: Subscription;
  public hasContent: boolean;

  @ContentChildren('myComponentContent') content: QueryList<ElementRef>;

  constructor() {}

  ngAfterContentInit(): void {
    this.hasContent = (this.content.length > 0);
    this._subscription = this.content.changes.subscribe(() => {
      // do something when content updates
      //
      this.hasContent = (this.content.length > 0);
    });
  }

  ngOnDestroy() {
    this._subscription.unsubscribe();
  }

}

Exemple complet : https://stackblitz.com/edit/angular-essvnq

andreivictor
la source