View/Content queries

<< Proyección de contenido Server Side Rendering >>

En esta sección vamos a ver como referenciar elementos de la plantilla desde el componente. Ya sean elementos definidos en la propia plantilla o proyectados en el componente.

viewChild y viewChildren

Con viewChild podemos hacer referencia a un elemento de la plantilla. Para ello, si es un componente o directiva de Angular, podemos utilizarlo en combinación con la clase que representa dicho componente/directiva. Nos devolverá la referencia al componente dentro de una signal.

<otro-componente></otro-componente>

Otra forma de acceder a un elemento es creando una referencia con # en la plantilla (esto nos permite identificar a un componente o directiva específico cuando hay varios del mismo tipo). También se puede utilizar para referenciar elementos HTML nativos. En este caso tendremos que tipar utilizando genéricos el tipo de objeto que vamos a referenciar (ElementRef en el caso de elemento HTML nativos).

<!-- ... -->
<img ... #imgPreview />
<!-- ... -->

La función viewChildren actua de manera similar pero te referencia un array con todos los componentes o directivas del mismo tipo que encuentra en la plantilla (también se pueden utilizar varias referencias '#' bajo el mismo nombre). 

export class ProductsPageComponent {
  //...
  templateProducts = viewChildren(ProductItemComponent); // Signal<readonly ProductItemComponent[]>
  //...
}

Es importante saber que hasta que no se haya renderizado por primera vez la plantilla, no tendremos acceso a los objetos referenciados. Para evitar problemas podemos utilizar las funciones computed, effect o linkedSignal para procesar dichos elementos. Otra forma sería utilizar el método ngAfterViewInit del ciclo de vida, o mejor aún, la nueva función afterNextRender, que se integra mejor con Angular SSR (que veremos en breve).

export class EjemploComponent {
    imagen = viewChild.required<ElementRef<HTMLImageElement>>('imgPreview'); 

  constructor() {
    // Si referenciamos componentes o directivas, o no vamos a manipular el DOM, mejor usar effect!
    afterNextRender(() => {
      const img = this.imagen().nativeElement;
      // Acciones con el elemento nativo. Solo manipular el DOM nativo cuando no haya más remedio
    });
  }
}

Ejemplo: Referenciar formulario de plantilla

Para referenciar el formulario de plantilla en el componente, habría que referenciar su directiva NgForm. Ahí es donde podemos consultar tanto el valor como el estado del fomulario, resetearlo, etc.

Si en nuestro componente hay un solo formulario podríamos utilizar NgForm directamente para indicar lo que queremos a la función viewChild. Otra opción es crear una referencia en la plantilla a la directiva, y usar el nombre de la referencia. Esto segundo sería útil para referencia un campo (NgModel) ya que con mucha probabilidad sea una directiva repetida en la plantilla.

<!-- .... -->
    <form (ngSubmit)="addProduct()" #productForm="ngForm">
        <!-- .... -->
    </form>
<!-- .... -->

Al tener la referencia, podríamos hacer cosas como validar el formulario antes de enviarlo, controlar los cambios de valor con el observable valueChanges, etc.

export class ProductFormComponent implements CanComponentDeactivate {
  //...
  addProduct() {
    if(this.form().valid) {
      // Añadimos producto
    }
  }
  //...
}

Además, en este caso también podríamos mejorar la eficiencia del guard de tipo CanDeactivate para que no nos pregunte cuando el formulario no ha sido modificado (pristine). Esto con formularios reactivos es directo, pero con formularios de plantilla necesitamos crear la referencia en el componente.

export class ProductFormComponent implements CanComponentDeactivate {
  //...
  canDeactivate() {
    return (
      this.form().pristine || // Formulario no modificado
      this.saved ||
      confirm('¿Quieres abandonar la página?. Los cambios se perderán...')
    );
  }
  //...
}

Ejemplo: Campo de búsqueda con debounceTime

Igual que vimos en la sección correspondiente de los formularios reactivos donde implementabamos un campo de texto que filtraba productos pasados 600 milisegundos desde que el usuario deja de escribir. Se puede hacer lo mismo con formularios de plantilla si referenciamos la directiva ngModel en el componente con viewChild.

<!-- ... -->
<input
    type="text"
    [(ngModel)]="search"
    #searchModel="ngModel"
    class="form-control"
    id="filterDesc"
    placeholder="Filter..."
/>
<!-- ... -->

La solución anterior tiene un pequeño problema. La carga inicial del productos no se haría hasta 600 ms después de renderizar la plantilla. Esto se podría paliar llamando al servicio aparte en el constructor (fuera de la función afterNextRender).

También se puede utilizar un enfoque más orientado a señales. En este caso no necesitaríamos la referencia viewChild.

export class ProductsComponent {
  //...
  search = signal('');
  searchDebounce = toSignal(
    toObservable(this.search).pipe(
      debounceTime(600), // 600 milisegundos hasta que deja de escribir
      distinctUntilChanged() // Solo si el valor cambia
    ),
    { initialValue: '' }
  );
  #destroyRef = inject(DestroyRef);

  constructor() {
    effect(() => {
      this.#productsService
        .getProducts(this.searchDebounce())
        .pipe(takeUntilDestroyed(this.#destroyRef))
        .subscribe((products) => this.products.set(products));
    });
  }
}
  
  

Otra opción sería utilizar la API rxResource de Angular 19:

export class ProductsPageComponent {
  //...
  search = signal('');
  searchDebounce = toSignal(
    toObservable(this.search).pipe(
      debounceTime(600), // 600 milisegundos hasta que deja de escribir
      distinctUntilChanged() // Solo si el valor cambia
    ),
    { initialValue: '' }
  );

  productsResource = rxResource({
    request: () => this.searchDebounce(),
    loader: ({ request: search }) => this.#productsService.getProducts(search),
  });
  products = computed(() => this.productsResource.value() || []);

  //...

  addProduct(product: Product) {
    this.productsResource.update((products) => [...(products || []), product]);
  }

  deleteProduct(product: Product) {
    this.productsResource.update((products) => products?.filter((p) => p !== product));
  }
}

contentChild y contentChildren

contentChild es similar a viewChild pero hace referencia a contenido proyectado en el componente actual. 

 Para el siguiente ejemplo vamos a ver como si le pasamos un icono (Vamos a usar FontAwesome icons para el ejemplo) al botón, en lugar de mostar la animación de carga en el centro del botón, sustituiremos las clases del icono para que se muestre un icono de carga animado en su lugar. SI no recibimos un icono, se comportará como antes.

<button [class]="['btn', colorClass()]" [disabled]="loading()">
  @if(loading() && !icon()) {
    <!-- Overlay y SVG a mostrar cuando no hay icono -->
  }
  <ng-content></ng-content>
</button>

El uso de contentChildren es análogo a viewChildren para contenido que se repite, devuelve una colección de tipo QueryList.

<custom-menu>
  <custom-menu-item>Cheese</custom-menu-item>
  <custom-menu-item>Tomato</custom-menu-item>
</custom-menu>

<< Proyección de contenido Server Side Rendering >>