No Angular 2, como verificar se <ng-content> está vazio?

98

Suponha que eu tenha um componente:

@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 {
}

Agora, eu gostaria de exibir algum conteúdo padrão se <ng-content>este componente estiver vazio. Existe uma maneira fácil de fazer isso sem acessar o DOM diretamente?

Slawomir Dadas
fonte
1
Possível duplicata de Como verificar se o conteúdo do ng existe
titusfx
Para sua informação, eu sei que a resposta aceita funciona, mas acho que é melhor passar um parâmetro de entrada do tipo "useDefault" para os componentes, padronizado como falso.
bryan60

Respostas:

98

Envolva ng-contentem um elemento HTML como um divpara obter uma referência local a ele e vincule a ngIfexpressão a 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
fonte
25
Não há alternativa para isso? Porque isso é feio, em comparação com os slots alternativos Aurelia
Astronauta
18
É mais seguro usar ref.children.length. childNodes conterá textnós se você formatar seu html com espaços ou novas linhas, mas os filhos ainda estarão vazios.
parlamento de
5
Há uma solicitação de recurso para um método melhor no rastreador de problemas Angular: github.com/angular/angular/issues/12530 (pode valer a pena adicionar um +1 lá).
eppsilon
5
Eu estava prestes a postar que isso não parecia estar funcionando até que percebi que usei o exemplo de em ref.childNodes.length == 0vez de ref.children.length == 0. Ajudaria bastante se você pudesse editar a resposta para ser consistente. Erro fácil, não bater em você. :)
Dustin Cleveland
3
Segui este exemplo e funciona bem para mim. Mas eu usei em !ref.hasChildNodes()vez deref.nativeElement.childNodes.length == 0
Sergey Dzyuba
37

Faltam algumas respostas de @pixelbits. Precisamos verificar não apenas a childrenpropriedade, porque quaisquer quebras de linha ou espaços no modelo pai causarão o childrenelemento com texto em branco \ quebras de linha. Melhor verificar .innerHTMLe .trim()isso.

Exemplo de trabalho:

<span #ref><ng-content></ng-content></span>
<span *ngIf="!ref.innerHTML.trim()">
    Content if empty
</span>
Ifoma
fonte
1
melhor resposta para mim :)
Kamil Kiełczewski
Podemos usar este método para verificar se o elemento filho está vazio e ocultar o pai em caso afirmativo? Eu tentei, sem sorte
Kavinda Jayakody
@KavindaJayakody Sim, isso verificará se o elemento filho está vazio. Mostre seu código.
Ifoma
* ngIf = "! ref.innerHTML.trim ()" Esta parte causará problemas de desempenho. Trim é O (n).
RandomCode
1
@RandomCode está certo, a função será chamada várias vezes. A melhor maneira é verificar element.children.length ou element.childnodes.length, dependendo do que você deseja solicitar.
Stefan Rein,
35

EDITAR 17/03/2020

CSS puro (2 soluções)

Fornece conteúdo padrão se nada for projetado no conteúdo ng.

Seletores possíveis:

  1. :only-childseletor. Veja esta postagem aqui:: seletor filho único

    Este requer menos código / marcação. Suporte desde o IE 9: Posso usar: filho único

  2. :emptyseletor. Apenas leia mais.

    Suporte do IE 9 e parcialmente desde 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;
}

Caso não esteja funcionando

Esteja ciente de que ter pelo menos um espaço em branco não é considerado vazio. Angular remove os espaços em branco, mas apenas no caso de não ser:

<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
fonte
1
De longe, essa foi a melhor solução para meu caso de uso. Não me lembrava que tínhamos uma emptypseudoclasse css
julianobrasil
@Stefan o conteúdo padrão será renderizado de qualquer maneira ... será apenas oculto. Portanto, para conteúdo padrão complexo, esta não é a melhor solução. Isto está certo?
BossOz
1
@BossOz Você está certo. Ele será renderizado (construtor etc. será chamado se você tiver um componente angular). Esta solução funciona apenas para visualizações burras. Se você tem uma lógica complexa, envolvendo carregamento ou coisas assim, sua melhor aposta na minha opinião é escrever uma diretiva estrutural, que pode obter um modelo / classe (como você deseja implementá-lo) e, dependendo da lógica, renderizar o componente desejado ou o padrão. A seção de comentários é muito pequena para um exemplo. Comento novamente para outro exemplo que temos em um de nossos aplicativos.
Stefan Rein,
Uma maneira seria ter um componente selecionando ContentChildren por meio de uma diretiva (consulta com DirectiveClass, mas uso como diretiva estrutural, portanto, nenhum bootstraping inicial envolvido apenas com a marcação): <loader [data]="allDataWeNeed"> <desired-component *loadingRender><desired-component> <other-default-component *renderWhenEmptyResult></other-default-component> </loader>e no componente de carregamento, você poderia mostrar o carregamento de desempenho percebido, enquanto os dados estão carregando. Você pode escrever / resolver de maneiras diferentes. Esta é uma demonstração de como você pode resolver isso.
Stefan Rein,
De longe a melhor e mais elegante solução! obrigado cara!
Mehdi
22

Quando você injeta o conteúdo, adicione uma variável de referência:

<div #content>Some Content</div>

e em sua classe de componente obtenha uma referência ao conteúdo injetado com @ContentChild ()

@ContentChild('content') content: ElementRef;

então, em seu modelo de componente, você pode verificar se a variável de conteúdo tem um valor

<div>
  <ng-content></ng-content>
  <span *ngIf="!content">
    Display this if ng-content is empty!
  </span>    
</div> 
Lerner
fonte
Esta é a resposta mais limpa e muito melhor. Ele suporta a API ContentChild pronta para uso. Não consigo entender por que a melhor resposta recebeu tantos votos.
CarbonDry
10
O OP perguntou se ngContent está vazio ou não - isso significa <MyContainer></MyContainer>. Sua solução espera que os usuários para criar uma sub-elemento sob myContainer: <MyContainer><div #content></div></MyContainer>. Embora essa seja uma possibilidade, eu não diria que é superior.
pixelbits
8

Injete elementRef: ElementRefe verifique se elementRef.nativeElementtem filhos. Isso pode funcionar apenas com encapsulation: ViewEncapsulation.Native.

Enrole a <ng-content>tag e verifique se ela tem filhos. Isso não funciona com encapsulation: ViewEncapsulation.Native.

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

e verifique se tem algum filho

@ViewChild('contentWrapper') contentWrapper;

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

(não testado)

Günter Zöchbauer
fonte
3
Eu votei contra isso por causa do uso de @ViewChild. Embora "Legal", ViewChild não deve ser usado para acessar nós-filhos dentro de um modelo. Isso é um antipadrão porque, embora os pais possam saber sobre os filhos, eles não devem acasalar-se com eles, pois geralmente leva ao acasalamento patológico ; uma forma mais apropriada de transporte de dados ou tratamento de solicitações é usar metodologias orientadas a eventos .
Cody
5
@Cody obrigado por postar um comentário em sua votação negativa. Na verdade, não sigo sua argumentação. O modelo e a classe de componentes são uma única unidade - um componente. Não vejo por que acessar o modelo do código deve ser um antipadrão. Angular (2/4) não tem quase nada em comum com AngularJS, exceto que ambos são frameworks da web e o nome.
Günter Zöchbauer
2
Gunter, você fez dois pontos muito bons. O Angular não deve necessariamente eclodir com o estigma dos caminhos do AngularJS. Mas eu observaria que o primeiro ponto (e aqueles que mencionei acima) caem na sarjeta filosófica da engenharia. Dito isso, tornei-me um especialista em acoplamento de software e desaconselho o uso [pelo menos pesado] de @ViewChild. Obrigado por adicionar algum equilíbrio aos meus comentários - considero ambos os nossos comentários tão dignos de consideração quanto o outro.
Cody
2
@Cody talvez sua opinião forte sobre @ViewChild()venha do nome. "Filhos" não são necessariamente filhos, são apenas a parte declarativa do componente (a meu ver). Se as instâncias do componente criadas para esta marcação forem acessos, isso é claro, uma história diferente, porque exigiria conhecimento pelo menos sobre sua interface pública e isso criaria um acoplamento estreito. Esta é uma discussão muito interessante. Preocupa-me não ter tempo suficiente para pensar sobre esses assuntos porque, com essas estruturas, muitas vezes se gasta tempo para descobrir qualquer meio.
Günter Zöchbauer
3
Não expor isso na API é o verdadeiro anti-padrão :-(
Simon_Weaver
5

Se você deseja exibir um conteúdo padrão, por que você não usa apenas o seletor 'filho único' do css.

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

por exemplo: HTML

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

css

.default-content:not(:only-child) {
   display: none;
}
ecossistema 31
fonte
Solução bastante inteligente, se você não quiser lidar com ViewChild e outras coisas no componente. Eu gosto disso!
M'sieur Toph '
Observe que transcluir o conteúdo deve ser um nó HTML (não apenas texto)
M'sieur Toph '06 de
2

Implementei uma solução usando o decorador @ContentChildren , que é semelhante à resposta de @Lerner .

De acordo com a documentação , este decorador:

Obtenha a QueryList de elementos ou diretivas do conteúdo DOM. Sempre que um elemento filho é adicionado, removido ou movido, a lista de consulta será atualizada e as alterações observáveis ​​na lista de consulta emitirão um novo valor.

Portanto, o código necessário no componente pai será:

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

Na classe do componente:

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

Então, no modelo de componente:

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

Exemplo completo : https://stackblitz.com/edit/angular-jjjdqb

Eu encontrei esta solução implementada em componentes angulares, para matSuffix, no componente de campo de formulário .

Na situação em que o conteúdo do componente é injetado posteriormente, depois que o aplicativo é inicializado, também podemos usar uma implementação reativa, inscrevendo-se no changesevento de 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();
  }

}

Exemplo completo : https://stackblitz.com/edit/angular-essvnq

andreivictor
fonte
2

Com o Angular 10, ele mudou ligeiramente. Você usaria:

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

No meu caso, tenho que ocultar o pai do conteúdo-ng vazio:

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

O css simples funciona:

.wrapper {
  display: inline-block;

  &:empty {
    display: none;
  }
}
smg
fonte