2

I'm trying to create a carousel component that has n amount of child components based on whatever size list it will get. I want the parent (carousel) to control the styling for children as it manipulates the size of the list and track the indexes to rotate items around the carousel and style accordingly. There will buttons to control this behaviour. example of the carousel

I'm trying to apply styles and get the functionality working but cannot seem to access the styles by getting a query list of element refs using view children and template refs (I also tried with content children initially, with the template set up properly). I can change them if I inject the element ref and renderer at the child level, but I don't want to do this here. I get the error: TypeError: Cannot read properties of undefined (reading 'style') But not when I do the exact same thing doing it from the child level.

Am I going about this incorrectly? How do I change children styles in a list from @ViewChildren() from a parent?

Parent component (carousel.component.html)

  <div class="caro-body">
    <h3 class="caro-body__title">Carousel Title</h3>
    <div class="caro-body__items">
      <app-upcoming-card
      #caroItem
      *ngFor="let card of list; let i = index"
      ></app-upcoming-card>
    </div>
  </div>

(carousel.component.ts)

import {
  Component,
  OnInit,
  AfterViewInit,
  Renderer2,
  ViewChildren,
  QueryList,
  ElementRef,
} from '@angular/core';
import { UpcomingCardComponent } from '../upcoming-card/upcoming-card.component';

@Component({
  selector: 'app-carousel',
  templateUrl: './carousel.component.html',
  styleUrls: ['./carousel.component.scss'],
})
export class CarouselComponent implements OnInit, AfterViewInit {
  @ViewChildren('caroItem') caroItems: QueryList<ElementRef<UpcomingCardComponent>>

  list = [
    { billName: 'Rent', billAmount: 850, billDate: Date.now() },
    { billName: 'Groceries', billAmount: 250, billDate: Date.now() },
    { billName: 'Internet', billAmount: 80, billDate: Date.now() },
    { billName: 'Phone', billAmount: 45, billDate: Date.now() },
    { billName: 'Loan', billAmount: 50, billDate: Date.now() },
    { billName: 'Transit', billAmount: 20, billDate: Date.now() },
    { billName: 'Dining Out', billAmount: 50, billDate: Date.now() },
  ];

  constructor(private renderer: Renderer2) {}

  ngOnInit(): void {}

  ngAfterViewInit() {
    this.caroItems.forEach(item => {
      this.renderer.setStyle(item.nativeElement, 'background-color', 'red')
    })
  }

}

Child component (upcoming-card.component.html)

<div class="card">
  <div class="card__title">{{ card.billName }}</div>
  <div class="card__amount">{{ card.billAmount | currency }}</div>
  <div class="card__date">{{ card.billDate | date }}</div>
</div>

(upcoming-card.component.ts)

import { Component, OnInit, Input } from '@angular/core';

@Component({
  selector: 'app-upcoming-card',
  templateUrl: './upcoming-card.component.html',
  styleUrls: ['./upcoming-card.component.scss']
})
export class UpcomingCardComponent implements OnInit {

  constructor() { }

  ngOnInit(): void {
  }

}

I've tried accessing the list using @ContentChildren, having the template structure properly for this case as well as using the proper lifecycle hook, but it results in the same. I can update styles by injecting the element ref and renderer into the child, but I want to do it at the parent level.

lowkey
  • 41
  • 6

2 Answers2

3

Thanks to a small modification of your code, I was eventually able to access the nativeElement of the ViewChildren. I passed the argument {read: ElementRef} to @ViewChildren and from then on item.nativeElement was no longer undefined.

Please see my code-snippet below or check out my working example on stackblitz where I accessed two nav-bars via @ViewChildren and colored them red.

  // {read: ElementRef} seems to be crucial in accessing nativeElement of a component:
  @ViewChildren('caroItem', {read: ElementRef})
  caroItems!: QueryList<ElementRef<UpcomingCardComponent>>;

  constructor(private renderer: Renderer2) {}

  ngAfterViewInit(): void {
    this.caroItems.forEach(item => {
      this.renderer.setStyle(item.nativeElement, 'background-color', 'red');
    })
  }
kellermat
  • 2,859
  • 2
  • 4
  • 21
  • Thank you. This is exactly what I was looking for. I did not want to use data/property binding because I will have many conditions for style based on different indexes and though it best not to clutter the template with logic. Although I wish the docs would explain this better. Or maybe I need more experience to understand the definition from the docs. https://angular.io/api/core/ViewChildren – lowkey Nov 26 '22 at 23:43
2

It would be vastly easier to set the style using data binding:

<app-upcoming-card
      *ngFor="let card of list; let i = index"
      [style.background-color]="i < 2 ? 'red' : 'white'"
      ></app-upcoming-card>

(example highlights the first two items in red; adjust the conditions to whatever you wish to accomplish)

meriton
  • 68,356
  • 14
  • 108
  • 175
  • Thank you. I was trying this approach to begin with, but decided not to use data/property binding as I'll have many conditions for style based on different indexes. I thought it was best to remove this logic from the template as it grew. @kellermat had the solution I was hoping for with the { read: ElementRef } – lowkey Nov 26 '22 at 23:46
  • 1
    It would be trivial to move this logic into a component method. You could write `[style]="stylesFor(i)"`, where `stylesFor` is a component method that returns an object with all the styles you wish to apply to this particular index. – meriton Nov 27 '22 at 00:32
  • It is a small app so I guess it won't matter, I've just been trying to be wary of using functions in the template because they run every time change detection is triggered. – lowkey Nov 27 '22 at 00:56