Search code examples
angulartypescriptrandomgridviewpinterest

Random function makes component change on click


I am creating a "Pinterest" view in an Angular app. I want each tile to have a random height from 250px to 450px with a 50px step for each possible height. The problem I have is each time i click on the list of tiles (CardComponent), the function randomSize() resets tiles and change their height indefinitely. Check the end of the post to play with the Stackblitz that I've provide you :)

This is way I'm currently handling this :

DomainDetailsComponent.ts

export class DomainDetailsComponent implements OnInit {

  domain$!: Observable<DomainModel>;
  private domainId: any;
  catArray: CategoryModel[][] = [];

  constructor(
    private domainService: DomainsService,
    private route: ActivatedRoute) {
    this.route.paramMap.subscribe(params => {
      if (params.has('domainId')) {
        this.domainId = params.get('domainId');
        this.domain$ = this.domainService.getDomainById(this.domainId).pipe(map((res) => {
          let domain = res.data[0];
          if (domain && domain.categories?.length)
            this.divideArray(domain.categories, domain.categories.length / 3);
          return domain
        }));
      }
    })
  }

  ngOnInit(): void {

  }

  divideArray(arr: CategoryModel[], spliceAt: number) {
    while (arr.length) {
      let str = (arr.splice(0, spliceAt));
      this.catArray.push(str);
    }
    const average = this.catArray.reduce((a, b) => a + b.length, 0) / this.catArray.length;

    for (let j = 0; j < this.catArray.length; j++) {
      let line = this.catArray[j]
      if (line.length > average) {
        // Retrieve average+ indexes
        let overElts = line.slice(average - 1, this.catArray.length - average)
        // Remove them from the actual line
        this.catArray[j] = line.slice(0, average)
        // Moving them to the next line
        this.catArray[j + 1] = this.catArray[j + 1].concat(overElts);
      }
    }
  }

  randomSize(): number {
    let numbers = [250, 300, 350, 400, 450];
    let index = Math.floor(Math.random() * numbers.length)
    return numbers[index];
  }
}

DomainDetailsComponent.html


<div class="grid_row" *ngIf="catArray.length > 0">
    <ng-container *ngFor="let line of catArray">
      <div class="grid_column">
        <app-card *ngFor="let cat of line" [category]="cat" [maxWidth]="450" [maxHeight]="randomSize()"></app-card>
      </div>
    </ng-container>
  </div>

CardComponent.ts


import {Component, HostListener, Input, OnChanges, OnInit, SimpleChanges} from '@angular/core';
import {CategoryModel} from "../../models/CategoryModel";
import {ProjectModel} from "../../models/ProjectModel";
import {SiteModel} from "../../models/SiteModel";

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

  @Input()
  category?: CategoryModel;
  @Input()
  descLimit?: number = 20;
  @Input()
  allowRedirect: boolean = true;
  @Input()
  maxWidth?: number = 250;
  @Input()
  maxHeight?: number = 300;

  constructor() {
  }

  ngOnInit(): void {
  }

  @HostListener("click", ["$event"])
  public onClick(event: any): void {
    if (!this.allowRedirect)
      event.stopPropagation();
  }

  ngOnChanges(changes: SimpleChanges): void {
    if (changes['maxHeight'].firstChange && changes['maxHeight'] && changes['maxHeight'].previousValue !== changes['maxHeight'].currentValue) {
      this.maxHeight = changes['maxHeight'].currentValue;
    } else {
      return;
    }
  }
}

CardComponent.html

<ng-container *ngIf="category !== undefined">
  <div class="element" style="{{'max-width:' + this.maxWidth + 'px;'}}">
    <figure class="element__thumb" style="{{ 'max-height:' + this.maxHeight + 'px;' }}">
      <img src="{{category.imagePath}}" alt="{{category.title}}" class="element__image">
      <figcaption class="element__caption">
        <h2 class="element__title">{{category.title}}</h2>
        <p class="element__snippet">{{ category.description}}</p>
        <a [routerLink]=" this.allowRedirect ? ['/categories', category.id] : null" class="element__button">En
          savoir plus</a>
      </figcaption>
    </figure>
  </div>
</ng-container>

Im asking for help fixing this behavior !

Here is a StackBlitz editor for you to play with that reproduces the bug and here is the StackBlitz App

Thanks for your time and I hope you will find this picky problem


Solution

  • Calling a method from a template is always a bad idea in general (because Angular cannot predict whether the return value of the method has changed), and in the case where the function is not pure, we can not resolve the problem with ChangeDetectionStrategy.OnPush and Pipe, so I can suggest two solutions:

    1. Calculate maxHeight inside CardComponent.

    2. Pre-define all values inside DomainDetailsComponent, for example, like this:

    <ng-container *ngFor="let line of catArray; let i = index">
      <div class="grid_column">
        <app-card
          *ngFor="let cat of line; let j = index"
          [category]="cat"
          [maxWidth]="450"
          [maxHeight]="sizes[i][j]">
        </app-card>
      </div>
    </ng-container>
    
    catArray: CategoryModel[][] = [];
    sizes: number[][] = [];
    
    constructor() {
      this.divideArray(this.domain.categories, this.domain.categories.length / 3);
      this.sizes = this.catArray.map((line) => line.map((item) => this.randomSize()));
    }