Beautiful ExpressionChangedAfterItHasBeenCheckedError

I need your help

  • Input field where I can enter a Student email or IAM, which will be added to a string array
  • List displaying all the students I have added, using a for loop as shown below
  • Delete button to remove a student from the array

The list has a specified maximum height, and I want to implement a feature that checks if the content exceeds this height. If it does, display a "read more" button; otherwise, hide it.

While the initial implementation works fine, I am facing an issue. When the number of students added causes the total height to exceed the maximum limit, the "read more" button appears correctly. However, upon deleting some students and bringing the total height back under the limit, an error occurs.

I prefer not to use setTimeout unless absolutely necessary

<div class="input-row">
  <input #studentsInput
         (keypress)="onKeyPressStudent($event, studentsInput)"
         class="input-field"
         placeholder="Student Email or IAM"
         required
         type="text">
</div>

<ul #listStudents
    [ngClass]="{ 'scrollable': studentsScroll }"
    class="list-row">
  <li *ngFor="let student of students; index as i"
      class="list-item">
    {{ student }}

    <span (click)="deleteStudent(i)"
          class="material-icons">
      close
    </span>
  </li>
</ul>

<div *ngIf="listStudents.offsetHeight < listStudents.scrollHeight"
     class="more">
  <span class="material-icons">
    more_horiz
  </span>
</div>
export class Component implements OnInit {
  public students: string[] = [];
  public studentsScroll = false;

  public ngOnInit(): void {
  }

  public onKeyPressStudent(
    event: KeyboardEvent,
    studentsInput: HTMLInputElement,
  ): void {
    if (event.key === 'Enter') {
      if (studentsInput.checkValidity()) {
        this.students.push(studentsInput.value);
        studentsInput.value = '';
      } else {
        this.toastrService.error('Please enter a valid Email Address!');
      }
    }
  }

  public deleteStudent(index: number): void {
    this.students.splice(index, 1);
  }

  public toggleStudentScroll(): void {
    this.studentsScroll = !this.studentsScroll;
  }
}

Console Error

Answer №1

When the deleteStudent() function is called, it mutates the students array using splice, resulting in a change in the number of list items (li) due to the *ngFor directive being used. This leads to property changes in the listStudents element (offsetHeight, scrollHeight) that are bound to the *ngIf directive.

As a result, the expression "listStudents.offsetHeight < listStudents.scrollHeight" is altered after the change detection process triggered by the mutation of the students array is completed in the template.

To resolve this issue, you must ensure that the check for showing/hiding the button occurs outside the initial change detection cycle (which is why setTimeout works as it creates its own change detection cycle).

You can achieve this by utilizing the afterContentChecked lifeCycle hook: https://angular.io/api/core/AfterContentChecked

Determine if the "showmore" should be visible in the DOM (true) or not (false) by evaluating the statement inside that hook:

<div class="input-row">
  <input #studentsInput
         (keypress)="onKeyPressStudent($event, studentsInput)"
         class="input-field"
         placeholder="Student Email or IAM"
         required
         type="text">
</div>

<ul #listStudents
    [ngClass]="{ 'scrollable': studentsScroll }"
    class="list-row">
  <li *ngFor="let student of students; index as i"
      class="list-item">
    {{ student }}

    <span (click)="deleteStudent(i)"
          class="material-icons">
      close
    </span>
  </li>
</ul>

<div *ngIf="showmore" class="more">
  <span class="material-icons">
    more_horiz
  </span>
</div>

In TypeScript:

import { Component, ViewChild, ElementRef } from '@angular/core';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: [ './app.component.css' ]
})
export class AppComponent  {

  @ViewChild('listStudents', { static: true }) listStudents: ElementRef;
  private listStudentsNativeElement: HTMLElement;

  public showmore: boolean = false;

  public students: string[] = [];
  public studentsScroll = false;

  public ngOnInit(): void {
  }

  public onKeyPressStudent(
    event: KeyboardEvent,
    studentsInput: HTMLInputElement,
  ): void {
    if (event.key === 'Enter') {
      if (studentsInput.checkValidity()) {
        this.students.push(studentsInput.value);
        studentsInput.value = '';
      } else {
        console.log('Please enter a valid Email Address!');
      }
    }
  }

  public deleteStudent(index: number): void {
    this.students.splice(index, 1);
  }

  public toggleStudentScroll(): void {
    this.studentsScroll = !this.studentsScroll;
  }

  ngAfterViewInit() {
    this.listStudentsNativeElement = this.listStudents.nativeElement;
  }

  ngAfterContentChecked() {
    if (this.listStudentsNativeElement) {
      this.showmore = (this.listStudentsNativeElement.offsetHeight < this.listStudentsNativeElement.scrollHeight);
    }
  }
}

Additional Notes:

  • We need to capture the reference to the listStudents nativeElement after full initialization (ngAfterViewInit)
  • We still need to verify if the listStudents nativeElement is defined since ngAfterContentChecked fires before view initialization

Alternatively, you can use setTimeout or RequestAnimationFrame API:

import { Component, ViewChild, ElementRef } from '@angular/core';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: [ './app.component.css' ]
})
export class AppComponent  {

  @ViewChild('listStudents', { static: true }) listStudents: ElementRef;
  private listStudentsNativeElement: HTMLElement;

  public showmore: boolean = false;
  public students: string[] = [];
  public studentsScroll = false;

  public ngOnInit(): void {
  }

  public onKeyPressStudent(
    event: KeyboardEvent,
    studentsInput: HTMLInputElement,
  ): void {
    if (event.key === 'Enter') {
      if (studentsInput.checkValidity()) {
        this.students.push(studentsInput.value);
        studentsInput.value = '';
        this.checkForShowMore();
      } else {
        console.log('Please enter a valid Email Address!');
      }
    }
  }

  public deleteStudent(index: number): void {
    this.students.splice(index, 1);
    this.checkForShowMore();
  }

  public toggleStudentScroll(): void {
    this.studentsScroll = !this.studentsScroll;
  }

  checkForShowMore() {
    requestAnimationFrame(()=>{
      this.showmore = (this.listStudentsNativeElement.offsetHeight < this.listStudentsNativeElement.scrollHeight);
    })
  }

  ngAfterViewInit() {
    this.listStudentsNativeElement = this.listStudents.nativeElement;
  }

}

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

What measures can be taken to stop LESS partials from compiling independently?

When working with LESS, I utilize partials that are included (@include) into the main LESS stylesheet. A common issue I face is that each partial ends up compiling to its own CSS file, leading to project clutter. In SASS, if a file is named with an unders ...

Struggling to track down the issue in my ts-node express project (Breakpoint being ignored due to generated code not being located)

For my current project, I decided to use the express-typescript-starter. However, when I attempted to debug using breakpoints in VS Code, I encountered an issue where it displayed a message saying "Breakpoint ignored because generated code not found (sourc ...

Having trouble running ng e2e on TeamCity with an @angular/cli script that refuses to cooperate

While using TeamCity 10, I encountered an issue with running an @angular/cli project during a build step. Everything worked smoothly until the e2e script was executed, causing the build to halt and require a forced shutdown. To troubleshoot, I accessed my ...

Looping issue with ForEach in Typscript with Firebase Functions

While browsing through various questions on this topic, I've noticed that the specific answers provided don't quite fit my situation. My query involves converting a Google Firebase RTB datasnapshot into an array of custom objects, each representi ...

NodeJs backend encounters undefined object due to FormData format request sent from Angular frontend

Never encountered this issue before despite having used this method for a long time. (Angular Frontend) const myFormData = new FormData(); myFormData.append("ok", "true"); this.http.put(my_Express_backend_url, myFormData); (Express ...

Updating a string's value in Angular based on user input

I am currently developing a custom offer letter template that will dynamically update key data points such as Name, Address, Role, Salary, etc based on the selected candidate from a list. The dynamic data points will be enclosed within <<>> in ...

Discovering the origins of the node.js native modules and delving into the intricacies of typed modules

I am using a Windows machine and trying to locate where node fetches the source code for native modules. On my system, I can only find the @types file which contains "Typed Only" modules. For example, the module "assert" is available in the master/lib fold ...

Store user input in a paragraph

I want to create a unique program that allows users to input text in a field, and when they click "Start", the text will appear in a paragraph backwards. I plan to use Html, jQuery, and CSS for this project. Can anyone provide guidance on how to achieve th ...

choose exclusively the text within the elementor navigation menu

I've been tinkering with this issue for a few hours now. I have a vertical Elementor navigation menu and I'd like to add a hover effect to it. So far, I can only select the entire column and apply the effect to that, not just the length of the t ...

The standard jQuery Mobile CSS styling does not seem to be working across various browsers, extensive testing has been conducted

I'm currently experimenting with jQuery Mobile to enhance my skills, but I'm facing issues with applying the basic CSS styling. I have included the css link from the jQuery Mobile website and ensured that I am connected to the internet. However, ...

Position a div in the center and add color to one side of the space

I am seeking a way to create a centered div with the left side filled with color, as shown in examples. I have devised two solutions without using flexbox, but both seem somewhat like hacks. body { margin: 0; padding: 0; } .header { width: ...

How to Retrieve ViewChild Element from within Input Component in Angular 2

Currently, I am utilizing ViewChild to target a specific HTML element within an Angular 2 component. My goal is to access this element from an Input field, but I am struggling with the correct syntax to correctly target the element in question. Below is th ...

What is the best way to iterate through the result of an HTTP request in Angular 11?

I am a beginner with Angular and currently working in Angular 11. I am facing issues with making an http request. Despite going through numerous Stack Overflow posts, none of the solutions seem to work for me, even though some questions are similar to mine ...

Discover the utility of the useHistory() hook in TypeScript for Class Components

Hello there, I am currently attempting to implement the following code snippet in my TypeScript-based class component: this.history.push({ pathname: `/search-results`, search: `${job}$${location}` } ...

Modify text by adjusting text color based on the contrast with the background color using knockout databinding

I am working with a table that contains data from my database: Here is the ViewModel structure: function alertViewModel(i) { var self = this; self.Id = ko.observable(i.Id); self.AlertType = ko.observable(i.AlertType); self.Category = ko.observab ...

What is the reason for the disappearance of unordered list bullets when using padding: 0?

Updating an older website that contains numerous unordered lists has presented a challenge. When the padding is set to 0, the display markers on the unordered list disappear. The root cause of this issue was traced back to the CSS setting *{padding: 0; ma ...

Template containing an observable collection of observables

Since the title is not helping me achieve my goal, let me explain what I'm trying to accomplish. My objective is to show a list of items in the view, which are fetched asynchronously from the backend. The service I have has a method called fetchItems ...

Transforming the appearance of the menu element in Vue using transitions

In my Vue app, I have this SCSS code that I'm using to create a smooth transition effect from the left for a menu when the isVisible property is set to true. However, I am encountering an issue where the transition defined does not apply and the menu ...

I am looking to switch the content between two divs. Specifically, I want to take the content from div 1 and move it to div 2, while moving the

I am facing a challenge involving three divs. One of them is the main div, while the other two are positioned below it and contain different content. I am trying to find a way to swap the content between div one and div two in such a way that the original ...

The alignment of navbar elements appears to be off in Jquery mobile

Currently working on an app using Jquery mobile, and encountering an issue where the navbar is not displaying elements in line. The google chrome console is indicating spaces between list elements. Upon removing these &nbsp characters, the elements ali ...