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

TypeScript encountered an error with code TS2305, stating that the module "constants" does not have any exported members

My Vite + React + TypeScript application has the following structure: src constants a.ts b.ts index.ts components Comp.tsx tsconfig file with "baseUrl": "src" The content of a.ts is as follows: export const ARRAY = ...

Dealing with multiple validation error messages in Angular Material 2

Working on a form using Angular Material 2, I am implementing a template-driven approach with an email input that requires two validators: required and email. The documentation for the input component (available at https://material.angular.io/components/co ...

Obtain the value of the background image's URL

Is there a way to extract the value of the background-image URL that is set directly in the element tag using inline styling? <a style="background-image: url(https:// ....)"></a> I attempted to retrieve this information using var url = $(thi ...

Uninitialized Array Member in Angular 8

Can anyone explain why the code snippet below is printing "undefined"? I have created several objects and intended to display the corresponding images, but after iterating through them using ngfor, nothing appeared. To investigate, I logged the array and ...

How can you refresh the .replaceWith method in jQuery?

Is there a way to reset the .replaceWith function so that the html, css and javascript elements are restored to their original state? I am looking to have an icon replace the text when the user clicks on "contact", and then have the text return when the u ...

Struggling to align a box perfectly to the right within an AppBar

I'm struggling with aligning the box containing user info to the right edge of my MUI App bar. It seems to be stuck at the end of the last box instead of going all the way to the right. Can anyone help me achieve this? https://i.stack.imgur.com/uJ1on. ...

Looking for a way to dynamically append a child element within another child

Struggling to include a new child within a specific child in Json myObject:any[] = []; this.myObject = { "type": "object", "properties": { "first_name": { "type": "string" }, "last_name": { "type": "string" }, } } addF ...

Error encountered when initializing a variable within the constructor of a TypeScript file in Angular 4

This is the content of my app.component.html file PL Auth Username: Password : Generate OTP Enter OTP : Login This is the code in my app.component.ts file import { Component, OnInit } from '@angular/core' ...

Leverage the power of Material UI makeStyles in conjunction with Typescript

In an effort to keep things organized, I've created a specific file for the classes prop, such as MuiAlert. Is there a way to specify to makeStyles that only Alert classes should be used? The current method works, but I'm sure there's a mo ...

Tips for customizing the appearance of Java FX TableView column headers with CSS

I am relatively new to JavaFX and delving into CSS stylesheets, as well as using stackoverflow. I am curious about how one can customize the styling of a table column header. This is what my current column header looks like: Current appearance of my table ...

Adding click functionality to dynamically generated list items in jQuery and HTML

I'm encountering an issue while trying to assign click events to dynamically added HTML elements in jQuery. Despite extensive research within this community, I find myself more confused than before. Below is the snippet of code causing me trouble: v ...

The back button functionality in Android cannot be altered or overridden using JavaScript

Hey everyone, I'm currently in the process of developing a mobile application using phonegap. I'm facing an issue where I want to disable the functionality of the back button from navigating to the previous page. I simply want it to do nothing or ...

`MongoDb aggregation performance degradation with numerous collections (join)`

I am currently working on a project using the MEAN stack and I have noticed that I am utilizing a significant number of collections in my aggregation, resulting in a heavy reliance on lookup. This has had a negative impact on performance, causing the execu ...

Achieving Centered Items with CSS Flexbox and Positioning

Struggling to arrange 3 elements using flexbox while keeping them centered both horizontally and vertically. Here is the desired layout: This is my current attempt, but it's not working as expected. Can anyone spot the mistake? .container { wid ...

Best Practices for Variable Initialization in Stencil.js

Having just started working with Stencil, I find myself curious about the best practice for initializing variables. In my assessment, there seem to be three potential approaches: 1) @State() private page: Boolean = true; 2) constructor() { this.p ...

Update the positioning of the element to center instead of the default top left origin using jQuery

I am facing an issue with positioning a marker inside a triangle, which is represented by a simple div, as shown in the image below: https://i.stack.imgur.com/0Q7Lm.png The marker needs to be placed exactly at the centroid of the triangle. However, it see ...

The wonders of jQuery popup windows

A code was discovered that displays a popup, but it currently relies on transparency (opacity: 0). I would like to modify it to use display: none instead, as the transparent window in the center of my website is causing issues. Here is the JavaScript code ...

Access an Angular 2 component through an email hyperlink including querystring parameters

I need to create a deep link with query string parameters for a component, so that when the link is clicked, it opens up the component in the browser. For example: exmaple.com/MyComponent?Id=10 I want to include a link in an email that will open the com ...

What is the best way to set the minDate and maxDate of the NgbDatePicker in the main component so that the settings can be applied

Within my Angular 4 project, I have integrated Ng-bootstrap (v1.1.0) which includes multiple date pickers across different modules. I am looking to enforce a global maxDate configuration for all these instances. Below is an overview of my folder structure: ...

What is the process for defining child routes in Angular 2.0 for Dart?

When working with TypeScript, a child route can be easily defined as shown below: export const routes: Routes = [ { path: '', redirectTo: 'product-list', pathMatch: 'full' }, { path: 'product-list', component: P ...