Listening for changes in class property values in TypeScript with Angular involves using the `ngOnChanges`

Back in the days of AngularJS, we could easily listen for variable changes using $watch, $digest... but with the newer versions like Angular 5 and 6, this feature is no longer available.

In the current version of Angular, handling variable changes has become a part of the component lifecycle.

I delved into official documentation, various articles, and particularly sought answers on Angular 5 change detection on mutable objects to learn how to observe changes in variables (class properties) within a TypeScript class / Angular.

The recommended approach nowadays is:

import { OnChanges, SimpleChanges, DoCheck } from '@angular/core';
@Component({
  selector: 'my-comp',
  templateUrl: 'my-comp.html',
  styleUrls: ['my-comp.css'],
  inputs:['input1', 'input2']
})
export class MyClass implements OnChanges, DoCheck, OnInit {
  @Input() input1:string;
  @Input() input2:string;

  myProperty_1: boolean
  myProperty_2: ['A', 'B', 'C'];
  myProperty_3: MysObject;

  constructor() { }

  ngOnInit() { }

  ngOnChanges(changes: SimpleChanges) {
    //Action for change
  }

  ngDoCheck() {
    //Action for change
  }
}

This approach only works for @Input() properties!

If I need to monitor changes in my component's own properties (myProperty_1, myProperty_2, or myProperty_3), the above solution won't suffice.

Can anyone provide assistance in resolving this issue? Ideally, I am looking for a solution that is compatible with Angular 5.

Answer №1

To monitor changes in the values of a component's field members, use KeyValueDiffers with the DoCheck lifecycle method.

import { DoCheck, KeyValueDiffers, KeyValueDiffer } from '@angular/core';

differ: KeyValueDiffer<string, any>;
constructor(private differs: KeyValueDiffers) {
  this.differ = this.differs.find({}).create();
}

ngDoCheck() {
  const change = this.differ.diff(this);
  if (change) {
    change.forEachChangedItem(item => {
      console.log('item changed', item);
    });
  }
}

Check out the demo for reference.

Answer №2

In my opinion, one of the best ways to address your problem is by implementing a decorator that automatically replaces the original field with a property. This allows you to create a SimpleChanges object on the setter method, similar to how Angular handles notifications. Alternatively, you could define a different interface for these notifications while following the same general principle.

import { OnChanges, SimpleChanges, DoCheck, SimpleChange } from '@angular/core';

function Watch() : PropertyDecorator & MethodDecorator{
    function isOnChanges(val: OnChanges): val is OnChanges{
        return !!(val as OnChanges).ngOnChanges
    }
    return (target : any, key: string | symbol, propDesc?: PropertyDescriptor) => {
        let privateKey = "_" + key.toString();
        let isNotFirstChangePrivateKey = "_" + key.toString() + 'IsNotFirstChange';
        propDesc = propDesc || {
            configurable: true,
            enumerable: true,
        };
        propDesc.get = propDesc.get || (function (this: any) { return this[privateKey] });

        const originalSetter = propDesc.set || (function (this: any, val: any) { this[privateKey] = val });

        propDesc.set = function (this: any, val: any) {
            let oldValue = this[key];
            if(val != oldValue) {
                originalSetter.call(this, val);
                let isNotFirstChange = this[isNotFirstChangePrivateKey];
                this[isNotFirstChangePrivateKey] = true;
                if(isOnChanges(this)) {
                    var changes: SimpleChanges = {
                        [key]: new SimpleChange(oldValue, val, !isNotFirstChange)
                    }
                    this.ngOnChanges(changes);
                }
            }
        }
        return propDesc;
    }
}

// Usage
export class MyClass implements OnChanges {


    //Properties what I want to track !
    @Watch()
    myProperty_1: boolean  =  true
    @Watch()
    myProperty_2 =  ['A', 'B', 'C'];
    @Watch()
    myProperty_3 = {};

    constructor() { }
    ngOnChanges(changes: SimpleChanges) {
        console.log(changes);
    }
}

var myInatsnce = new MyClass(); // outputs original field setting with firstChange == true
myInatsnce.myProperty_2 = ["F"]; // will be notified on subsequent changes with firstChange == false

Answer №3

To implement proper checks when setting or getting a variable, you can use the following approach:

public set myProperty_2(value: type): void {
 if(value) {
  // Perform necessary checks
 }

 this._myProperty_2 = value;
}

If you need to retrieve the value later, you can use the following:

public get myProperty_2(): type {
  return this._myProperty_2;
}

By using these methods, you can ensure that all required validations are in place whenever the myProperty_2 property is accessed.

Here's a small demo showcasing this concept: https://stackblitz.com/edit/angular-n72qlu

Answer №4

If you're looking to monitor DOM changes in order to track any modifications made to your element, then these helpful hints and tips might just solve your problem. Follow the simple steps below:

Step 1: Begin by referencing your element like so:

In HTML:

<section id="homepage-elements" #someElement>
....
</section>

In the TS file of your component:

@ViewChild('someElement')
public someElement: ElementRef;

Step 2: Next, create an observer to monitor changes to that element. Make sure your component's ts file implements both AfterViewInit and OnDestroy, and then implement the ngAfterViewInit() function as follows (OnDestroy will have a role later):

private changes: MutationObserver;

ngAfterViewInit(): void {
  console.debug(this.someElement.nativeElement);

  setInterval(() => {
    this.renderer.setAttribute(this.someElement.nativeElement, 'my_custom', 'secondNow_' + (new Date().getSeconds()));
  }, 5000);

  this.changes = new MutationObserver((mutations: MutationRecord[]) => {
      mutations.forEach((mutation: MutationRecord) => {
        console.debug('Mutation record fired', mutation);
        console.debug(`Attribute '${mutation.attributeName}' changed to value `, mutation.target.attributes[mutation.attributeName].value);
      });
    }
  );

  this.changes.observe(this.someElement.nativeElement, {
    attributes: true,
    childList: true,
    characterData: true
  });
}

You'll notice the console starts logging any changes made to that element.

This is demonstrated further with another example where two mutation records are fired due to a change in class:

setTimeout(() => {
  this.renderer.addClass(this.someElement.nativeElement, 'newClass' + (new Date().getSeconds()));
  this.renderer.addClass(this.someElement.nativeElement, 'newClass' + (new Date().getSeconds() + 1));
}, 5000);

this.changes = new MutationObserver((mutations: MutationRecord[]) => {
    mutations.forEach((mutation: MutationRecord) => {
      console.debug('Mutation record fired', mutation);
      if (mutation.attributeName == 'class') {
        console.debug(`Class changed, current class list`, mutation.target.classList);
      }
    });
  }
);

Console log displays these changes for reference.

Lastly, don't forget about the cleanup step in OnDestroy:

ngOnDestroy(): void {
  this.changes.disconnect();
}

For more information, check out this Resource: Listening to DOM Changes Using MutationObserver in Angular

Answer №5

If you need to utilize ChangeDetectorRef, follow these steps:

 constructor(private cd: ChangeDetectorRef) {
          // Trigger change detection for the current component
            // The injected instance of ChangeDetectorRef can be accessed through this.cd
            this.cd.detectChanges();

            // Alternatively, trigger change detection for the entire application
            // The ApplicationRef instance can be accessed through this.appRef
            this.appRef.tick();
}

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

Reposition the checked box to the top of the list

My goal is to click on each item, and the selected item should move to the top of the list and be rendered at the top. However, I encountered an issue where when clicking on an item, it moves to the top but the item that replaces it also gets checked. Bel ...

Showing a 2D array in Jquery within an MVC environment - what's the solution?

I am in the process of building an MVC application. My goal is to transmit data from the controller and display it using JQuery. I have constructed an array in the controller and sent it to JQuery using Json. Here is the array... And here is the JQuery ...

Sorting through a table based on the name of the element

I am currently working on filtering an HTML table using a search form. It's working great, but I'm facing an issue where the filtered elements are trying to fill the entire width of the table instead of maintaining their original width (which is ...

Routes for Express are throwing a 500 internal server error

My server is unable to locate the APIs that I have created in the API directory, which is resulting in a 500 internal server error. I have thoroughly checked routes.js and everything appears to be correct. Additionally, I have an error.js file for handlin ...

The electron program is unable to locate the package.json module

I am new to electron and attempting to run an express app for the first time. However, I encountered this error: Need assistance updating code Error: Cannot find module 'C:\package.json' at Module._resolveFilename (module.js:440:15) ...

Incorporating an alternate object array to update an array of objects: A

There are two object arrays, the main array and the temp array. The goal is to compare the main array with the temp array and update the values in the main array based on matching IDs. In this example, IDs 2 and 3 match in both arrays. Therefore, the valu ...

Node.js is essential when using Angular for implementing ui-select components

I'm currently delving into learning AngularJS. I've successfully created a basic web application using AngularJS, with Java EE powering the backend (server side). This app is being hosted on Tomcat. The advantages of AngularJS over JQuery are bec ...

Loading an animated SVG sprite file in real-time

Recently, I received an SVG sprite file from our designers to use in my app. The specified convention is to load the sprite at the top of the <body> element and then render icons using a specific code snippet: <svg class="u-icon-gear-dims"> ...

Is it possible to utilize the function(e) multiple times within a single file?

Can the same function be used multiple times in a single file? document.getElementById('one').onlick = function test(e) { var key = e.which; if(key === 13) { document.getElementById('two').c ...

What is the best way to refresh a navigation bar after making an API request, such as when using Google Sign-In?

Struggling to grasp the hook concept in this particular scenario: The flow goes like this - the user logs in with Google, which updates the session state. Consequently, the visitorType state transitions from 'viewer' to 'buyside'... A ...

How can I use HTML and jQuery to send a button click event to a .py file using AJAX and cgi in web development?

I have encountered a challenge with posting data from button clicks on an HTML page to Python CGI for insertion into a PostgreSQL database. My script seems to be struggling with this task. Here is the structure of my HTML, ajax, and javascript: '&ap ...

Could you clarify that for me?

Let's take a look at the function isIsogram(str) which checks if a string is an isogram. An isogram is a word or phrase in which no letter occurs more than once. The code snippet for this function can be seen below: We are particularly interested in ...

Prevent the use of the + or - symbols within the body of a regular expression when

function validateNumberInput(){ userInput = document.getElementById('txtNumber').value; var numberPlusMinusRegex = /^[\+?\-?\d]+$/g; if (userInput.match(numberPlusMinusRegex)) { alert('Vali ...

Another option for handling a series of conditional statements instead of a bulky

Can someone help me with a coding issue I'm facing? I have an application that contains a large number of buttons which I need to trigger using keyboard presses. Currently, I am using a switch statement for this purpose. However, as the number of butt ...

When Vue detects a change in declared parameters from an empty string, it automatically sends a

Within my application, I am making a request to the backend app and receiving a response with an ID such as { 'id': '12345'}. This ID is then saved as loadId within the data object: export default { name: 'SyncProducts', d ...

What is the best way to display data in the User Interface when data is being received through the console in AngularJS?

I have created an HTML file and the corresponding controller logic for this page. I can see the data in the console, but it's not displaying on my UI. <div id="panelDemo14" class="panel panel-default" ng-controller="NoticeController"> < ...

The tooltip feature in jQuery is experiencing some stuttering issues

Sometimes, images can convey messages better than words. I encountered a strange issue with my self-made jQuery tooltip. I prefer not to use any libraries because my needs are simple and I don't want unnecessary bloat. When I move my mouse from righ ...

Combining all elements of my application into a single HTML, JS, and CSS file

My AngularJS app has a specific structure that includes different directories for each component: theprojectroot |- src | |- app | | |- index.html | | |- index.js | | |- userhome | | | |- userhome.html | | | ...

Creating a type declaration for an object by merging an Array of objects using Typescript

I am facing a challenge with merging objects in an array. Here is an example of what I am working with: const objectArray = { defaults: { size: { foo: 12, bar: { default: 12, active: 12 } }, color: {} } } ...

Navigating within a React application using React Router 2.6.0 by triggering a redirection within a click

Currently, I am experiencing an issue while utilizing react-router for constructing a login system with firebase and react. The desired functionality involves redirecting the user to the home page upon successful authentication of their username and passw ...