En Angular 2, ¿cómo verificar si <ng-content> está vacío?

92

Supongamos que tengo un 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 {
}

Ahora, me gustaría mostrar contenido predeterminado si <ng-content>este componente está vacío. ¿Existe una manera fácil de hacer esto sin acceder directamente al DOM?

Slawomir Dadas
fuente
1
Posible duplicado de Cómo comprobar si existe ng-content
titusfx
FYI, sé que la respuesta aceptada funciona, pero creo que es mejor estilo pasar un parámetro de entrada de tipo "useDefault" a los componentes, predeterminado en falso.
bryan60

Respuestas:

97

Envuelva ng-contentun elemento HTML como a divpara obtener una referencia local, luego vincule la ngIfexpresión 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
fuente
24
¿No hay una forma alternativa a esto? Porque esto es feo, comparado con las ranuras de respaldo de Aurelia
Astronaut
18
Es más seguro de usar ref.children.length. childNodes contendrá textnodos si formatea su html con espacios o nuevas líneas, pero los niños aún estarán vacíos.
parlamento
5
Hay una solicitud de función para un mejor método en el rastreador de problemas de Angular: github.com/angular/angular/issues/12530 (podría valer la pena agregar un +1 allí).
eppsilon
5
Estaba a punto de publicar que esto no parecía funcionar hasta que me di cuenta de que usé el ejemplo de en ref.childNodes.length == 0lugar de ref.children.length == 0. Sería de gran ayuda si pudieras editar la respuesta para que sea coherente. Fácil error, no golpearte. :)
Dustin Cleveland
3
Seguí este ejemplo y me funciona bien. Pero usé en !ref.hasChildNodes()lugar deref.nativeElement.childNodes.length == 0
Sergey Dzyuba
34

Hay algunos que faltan en la respuesta de @pixelbits. Necesitamos verificar no solo la childrenpropiedad, porque cualquier salto de línea o espacio en la plantilla principal provocará un childrenelemento con texto en blanco \ saltos de línea . Mejor comprobar .innerHTMLy.trim() él.

Ejemplo de trabajo:

<span #ref><ng-content></ng-content></span>
<span *ngIf="!ref.innerHTML.trim()">
    Content if empty
</span>
lfoma
fuente
1
la mejor respuesta para mí :)
Kamil Kiełczewski
¿Podemos usar este método para verificar si el elemento hijo está vacío y ocultar el padre si es así? Lo intenté, sin suerte
Kavinda Jayakody
@KavindaJayakody Sí, esto comprobará que el elemento hijo está vacío. Muestre su código.
lfoma
* ngIf = "! ref.innerHTML.trim ()" Esta parte provocará problemas de rendimiento. Trim es O (n).
RandomCode
1
@RandomCode es correcto, la función se llamará varias veces. Una mejor forma es comprobar element.children.length o element.childnodes.length, según lo que quieras pedir.
Stefan Rein
32

EDITAR 17.03.2020

Pure CSS (2 soluciones)

Proporciona contenido predeterminado si no se proyecta nada en ng-content.

Posibles selectores:

  1. :only-childselector. Vea esta publicación aquí: Selector de hijo único

    Este requiere menos código / marcado. Soporte desde IE 9: ¿Puedo usar: hijo único?

  2. :emptyselector. Solo sigue leyendo.

    Soporte de IE 9 y 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;
}

En caso de que no funcione

Tenga en cuenta que tener al menos un espacio en blanco se considera que no está vacío. Angular elimina los espacios en blanco, pero por si acaso no es así:

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

o

<div class="wrapper"><ng-content select="my-component"></ng-content</div>
Stefan Rein
fuente
1
De lejos, esta fue la mejor solución para mi caso de uso. No recordaba que teníamos una emptypseudoclase css
julianobrasil
@Stefan, el contenido predeterminado se procesará de todos modos ... solo se ocultará. Por lo tanto, para contenido predeterminado complejo, esta no es la mejor solución. ¿Es esto correcto?
BossOz
1
@BossOz Tienes razón. Se renderizará (se llamará al constructor, etc. si tiene un componente angular). Esta solución solo funciona bien para vistas tontas. Si tiene una lógica compleja, que implica la carga o cosas así, su mejor apuesta en mi opinión es escribir una directiva estructural, que puede obtener una plantilla / clase (como quiera implementarla) y dependiendo de la lógica, luego renderizar la componente deseado o el predeterminado. La sección de comentarios es demasiado pequeña para un ejemplo. Vuelvo a comentar por otro ejemplo que tenemos en una de nuestras aplicaciones.
Stefan Rein
Una forma sería tener un componente que seleccione ContentChildren a través de una directiva (consulta con DirectiveClass, pero uso como directiva estructural, por lo que no hay arranque inicial involucrado con solo tener el marcado): <loader [data]="allDataWeNeed"> <desired-component *loadingRender><desired-component> <other-default-component *renderWhenEmptyResult></other-default-component> </loader>y en el componente de carga, podría mostrar la carga de rendimiento percibida, mientras los datos se están cargando. Puedes escribirlo / resolverlo de diferentes formas. Esta es una demostración de cómo podría resolverlo.
Stefan Rein
21

Cuando inyecte el contenido agregue una variable de referencia:

<div #content>Some Content</div>

y en su clase de componente obtenga una referencia al contenido inyectado con @ContentChild ()

@ContentChild('content') content: ElementRef;

por lo que en su plantilla de componente puede verificar si la variable de contenido tiene un valor

<div>
  <ng-content></ng-content>
  <span *ngIf="!content">
    Display this if ng-content is empty!
  </span>    
</div> 
Lerner
fuente
Esta es la respuesta más limpia y mucho mejor. Admite la API ContentChild lista para usar. No puedo entender por qué la respuesta principal obtuvo tantos votos.
CarbonDry
10
El OP preguntó si ngContent está vacío o no, es decir <MyContainer></MyContainer>. Su solución de espera a los usuarios crear un sub-elemento de bajo MyContainer: <MyContainer><div #content></div></MyContainer>. Si bien esta es una posibilidad, no diría que esto sea superior.
pixelbits
8

Inyecte elementRef: ElementRefy compruebe si elementRef.nativeElementtiene hijos. Es posible que esto solo funcione con encapsulation: ViewEncapsulation.Native.

Envuelva la <ng-content>etiqueta y verifique si tiene hijos. Esto no funciona con encapsulation: ViewEncapsulation.Native.

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

y comprueba si tiene hijos

@ViewChild('contentWrapper') contentWrapper;

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

(no probado)

Günter Zöchbauer
fuente
3
Bajé la votación por el uso de @ViewChild. Si bien es "Legal", ViewChild no debe usarse para acceder a los nodos secundarios dentro de una plantilla. Esto es un antipatrón porque, aunque los padres pueden saber acerca de los niños, no deben emparejarse con ellos, ya que generalmente conduce a un acoplamiento patológico ; Una forma más apropiada de transporte de datos o manejo de solicitudes es utilizar metodologías basadas en eventos .
Cody
5
@Cody, gracias por publicar un comentario en su voto negativo. En realidad, no sigo tu argumentación. La plantilla y la clase de componentes son una sola unidad: un componente. No veo por qué acceder a la plantilla desde el código debería ser un antipatrón. Angular (2/4) no tiene casi nada en común con AngularJS excepto que ambos son frameworks web y el nombre.
Günter Zöchbauer
2
Gunter, haces dos puntos realmente buenos. Angular no debería nacer necesariamente con un estigma de las formas de AngularJS. Pero señalaría que el primer punto (y los que he señalado anteriormente) caen hacia la cuneta filosófica de la ingeniería. Dicho esto, me he convertido en una especie de experto en acoplamiento de software y desaconsejaría el uso [al menos intensivo] de @ViewChild. Gracias por agregar algo de equilibrio a mis comentarios. Encuentro nuestros dos comentarios tan dignos de consideración como el otro.
Cody
2
@Cody tal vez tu fuerte opinión @ViewChild()proviene del nombre. Los "niños" no son necesariamente niños, son solo la parte declarativa del componente (como yo lo veo). Si las instancias de componentes creadas para este marcado son accesos, esto es, por supuesto, una historia diferente, porque requeriría conocimiento sobre al menos su interfaz pública y esto en realidad crearía un acoplamiento estrecho. Ésta es una conversación muy interesante. Me preocupa que no tenga tiempo suficiente para pensar en estos asuntos porque con tales marcos a menudo se quema el tiempo para encontrar alguna manera.
Günter Zöchbauer
3
No exponer esto en la API es el verdadero anti-patrón :-(
Simon_Weaver
5

Si desea mostrar un contenido predeterminado, ¿por qué no usa el selector 'hijo único' de css?

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

por ejemplo: HTML

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

css

.default-content:not(:only-child) {
   display: none;
}
ecosistema31
fuente
Una solución bastante inteligente, si no quiere lidiar con ViewChild y otras cosas en el Componente. ¡Me gusta!
M'sieur Toph '
Tenga en cuenta que el contenido de transcluir debe ser un nodo HTML (no solo texto)
M'sieur Toph '
2

Con Angular 10, ha cambiado ligeramente. Usarías:

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

En mi caso, tengo que ocultar el padre del contenido ng vacío:

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

CSS simple funciona:

.wrapper {
  display: inline-block;

  &:empty {
    display: none;
  }
}
smg
fuente
1

Implementé una solución usando el decorador @ContentChildren , que de alguna manera es similar a la respuesta de @ Lerner.

Según los documentos , este decorador:

Obtenga la QueryList de elementos o directivas del DOM de contenido. Cada vez que se agrega, elimina o mueve un elemento secundario, la lista de consultas se actualizará y los cambios observables de la lista de consultas emitirán un nuevo valor.

Entonces, el código necesario en el componente principal será:

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

En la clase de componente:

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

Luego, en la plantilla de componentes:

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

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

Encontré esta solución implementada en componentes angulares, para matSuffix, en el componente de campo de formulario .

En la situación en la que el contenido del componente se inyecta más tarde, después de inicializar la aplicación, también podemos usar una implementación reactiva, suscribiéndonos al 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();
  }

}

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

andreivictor
fuente