Unending digest loop when using ng-repeat and filter with Angular.js

I have implemented a custom filter in AngularJS that organizes my elements based on date.

Here is the HTML code snippet:

<table ng-repeat="(index, groupData) in something = (recentTasks | groupBy:dataGroupBy) track by $index">

The custom filter function looks like this:

module.filter('groupBy', function () {
    return function(items, field) {
        var groups = [];

        switch (field) {
            case 'week':
                angular.forEach(items, function(item) {
                    // Logic for grouping tasks by week
                });

                break;

            case 'month':
                angular.forEach(items, function(item) {
                    // Logic for grouping tasks by month
                });

                break;

            default:
                angular.forEach(items, function(item) {
                    // Default logic for grouping tasks
                });
        }
        return groups;
    }
});

This filter takes an array of tasks ('recentTasks') as input and groups them based on date into separate tables. However, it is causing an infinite digest loop error.

Can anyone provide guidance on how to resolve this issue? Alternatively, are there better solutions for achieving the desired outcome?

EDIT: Here are examples of input and output:

$scope.items = [
    {name: 'Abc', date: '2014-03-12'},
    {name: 'Def', date: '2014-03-13'},
    {name: 'Ghi', date: '2014-03-11'},
    {name: 'Jkl', date: '2014-03-12'}
]

The expected output should be grouped like this:

[
    '2013-03-11': [
        {name: 'Ghi'}
    ],
    '2013-03-12': [
        {name: 'Abc'},
        {name: 'Jkl'}
    ],
    '2013-03-13': [
        {name: 'Def'}
    ]
]

Each day's items should be displayed in separate tables within the HTML structure.

<table ng-repeat="dayData in groupBy(items, 'day')">
    <thead>
        <tr><td>{{ dayData.date }}</td> </tr>
    </thead>
    <tbody>
        <tr ng-repeat="item in dayData.items">
            <td>{{ item.name }}</td>
        </tr>
    </tbody>
</table>

Answer №1

In order to grasp the reason behind this phenomenon, it is important to have a good understanding of the digest cycle in Angular. The core concept of Angular involves "dirty checking", where a digest cycle occurs as Angular checks all properties on the scope to identify any changes. When a property has changed, Angular triggers all watches associated with that property to notify them of the change. Subsequently, if a watch modifies properties on the scope, Angular initiates another round of dirty checking once the watches are completed. This process continues until Angular goes through all properties and recognizes that none of them have been altered. An infinite digest ensues when a watch consistently assigns a new value to a property. To clarify, consider the following example:

$scope.myNumber = Math.random();
$scope.$watch('myNumber', function() {
  $scope.myNumber = Math.random();
});

As shown above, the watch will continue to be invoked endlessly since it perpetually updates the value of myNumber. Another common scenario leading to this error is when two properties and corresponding watches interact recursively:

$scope.prop1 = Math.random();
$scope.prop2 = Math.random();

$scope.$watch('prop1', function() {
  $scope.prop2 = Math.random();
});
$scope.$watch('prop2', function() {
  $scope.prop1 = Math.random();
});

These watches trigger each other in an endless loop.

The issue in your case arises from your filter repeatedly producing a new array with fresh objects within. Instead of comparing objects by scrutinizing all their properties, Angular introduces a $$hashkey property for comparison purposes. Objects with matching $$hashkey values are considered identical. Hence, even if you return an identical data structure every time, Angular regards it as a new object and executes additional digest cycles until it reaches a dead end.

To resolve this, ensure that your filter code refrains from generating entirely new objects for groups[back], opting instead to reuse previously created objects.

EDIT:

I suggest reconsidering your filter approach. Having a filter on an ng-repeat that doesn't filter but transforms the list can be perplexing for future developers (or yourself). For instance:

// Controller
$scope.items = [
  {name: 'Buy groceries', time: 10},
  {name: 'Clean the kitchen', time: 20}
];

// Template
<li ng-repeat="item in items | groupBy:something">
  {{item.total_time}}
</li>

Looking at the controller, one expects an item to contain a name and time property. However, in the template, there's a total_time property without clear indication of its origin from the groupBy filter—since Angular filters typically don't mutate objects.

It would be more coherent to extract the grouping logic into a function on the scope or via a service and reserve the filter solely for item filtering purposes:

<li ng-repeat="item in groupBy(items, 'something')">
  {{item.total_time}}
</li>

This approach clarifies that the groupBy() method returns a new data structure, alleviating confusion and resolving the infinite digest issue.

Answer №2

It's commonly advised not to create new mutable objects each time a function is called within ngRepeat, as it can lead to multiple checks during the digest cycle, causing issues with model stability. The recommended approach is to use a stable model by assigning the array to the scope or controller instead of using a getter method. This issue becomes noticeable when dealing with nested ngRepeats, making it challenging to implement without a getter.

An easy workaround is to utilize ngInit to set a variable referencing the function call result, and then iterate through that stable variable in ngRepeat. For instance:

<div ng-init="issues=myCtrl.getUniqueIssues()">
    <div ng-repeat="issue in issues">{{issue.name}}</div>
</div>

You can also find an example involving a filter in my reference:

Answer №3

To tackle this issue, there are two effective strategies:

  1. One approach is to include a $$hashKey attribute with each element in your array. Angular does not perform object comparison by examining all properties; instead, it introduces a $$hashkey property for comparison. If two objects possess the same $$hashkey property, they are deemed equal. Consequently, even if you return an identical data structure every time, Angular interprets it as a new object and triggers another digest cycle until it ceases processing.

    • View Old fiddle demonstrating $digest problem - jsfiddle.net/LE6Ay

    • Showcase of $$hashKey property allocation - jsfiddle.net/LE6Ay/1

  2. Avoid generating new objects during each getList() invocation. The recommended course of action involves utilizing a stable model by assigning the array to the scope/controller rather than utilizing a getter.

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

Utilizing Vue.js: Dynamically linking v-model to route parameters depending on the current state

I'm currently in the process of developing an application that will serve as the backbone for a restaurant chain's website. The main functionality involves allowing users to modify page content and images. Given the complexity of the site with it ...

Issue encountered where Moment locale functionality is working in local development environment, but not functioning properly in the

My application built with Next.js requires displaying the date in Bengali instead of English. The issue arises after running the build command 'build:next build' where the production version displays the date in English (e.g. '20 January, Su ...

Exploring the possibilities of Obj file manipulation with threejs- Selection, updating, and transforming

I'm currently working on an application for 3D Visualization and Interactivity using threejs. Here are the main functionalities I aim to include in this project: In this app, users should be able to: Rotate and Scale the Object - completed Manipul ...

Why is it that a click event outside of an HTML element cannot be detected in this Vue 3 application?

I've been diving into building a Single Page Application using Vue 3, TypeScript, and The Movie Database (TMDB). Currently, I'm focused on developing a search form within my project. Within the file src\components\TopBar.vue, here&apo ...

Using the WordPress REST API to retrieve pages based on their URLs

Trying to work with the WP REST API on a website that uses user-friendly URLs instead of ID's, which is different from the typical scenario in a RESTful context. I only have the requested URL to use for querying. For example, if a visitor lands on ex ...

Running concurrent codes is not possible in Node.js serialport

I am attempting to stream data from the MSP432 xds110 in order to display it on my website in real-time. To achieve this, I have opted to utilize the node.js serialport library for data retrieval. You can find more information about serialport here: In m ...

Apply the Jquery class to only one row instead of multiple rows

Situation: my issue is quite straightforward. The class input-test is being applied to multiple rows. This class should only be applied once in the row I am editing. Currently, when I click on multiple rows to edit, the class input-test is applied, which ...

A guide to showcasing items based on their categories using React.js

After successfully displaying Categories from a port (http://localhost:5000), accessing my MongoDB database, I encountered an issue when attempting to display the products for each category separately. Despite trying the same method as before, I keep rec ...

Files with extensions containing wildcards will trigger a 404 error when accessed from the public folder in NextJS

I have successfully set up my public folder to serve static files, however I am encountering an issue with files that have a leading dot in their filename (.**). Specifically, I need to host the "well-known" text file for apple-pay domain verification, wh ...

Obtain the current date using Moment JS in JavaScript

Here is a scenario with code : let currentTime = moment(); console.log(currentTime.format()); // 2019-11-25T20:23:50+02:00 console.log(currentTime.toDate()); // 2019-11-25T18:23:50.916Z After applying the timezone change on Heroku using the command ...

PHP is unable to retrieve the formData object sent via ajax

I've been experimenting with sending a formData object to PHP (I'm following a tutorial at this link), but I'm encountering some obstacles. Firstly, I create the formData object and fill it with: var formdata = new FormData(); formdata.appe ...

Show the JSON data from the server in a table format

Is there a way to neatly display data received from an Ajax response within a table format? Below is the structure of the table: <table data-toggle="table" data-show-columns="true" data-search="true" data-select-item-name="toolbar1" id="menu_table"> ...

What is the best way to update the data-url attribute with a new value using jQuery?

Initially, before the page loads: <a data-url="htts://xyz@..." class="mybuttons" data-type="facebook"> After page load, I dynamically update the data-url using jQuery: <a data-url="https://abc@..." class="mybuttons" data-type="facebook"> ...

Issue with updating robot's direction using string in Javascript not resolving

Javascript Developing a simple game where a robot moves on a 5x5 board based on user input commands 'L' (move left), 'R' (move right), and 'F' (move forward) from a starting position while facing North (N). Clicking the Move ...

How Vue and Jest work together: parent component handles custom event triggered by child component

I am currently facing a challenge with testing a set of parent and child Vue components. The child component emits an event that is handled by the parent component. I want to verify that the custom event is handled correctly, but I've hit a roadblock. ...

How to Insert Data into SQLITE using Node.js and Retrieve the Inserted Object or Last Inserted ID

I've been struggling to retrieve the object I just inserted, following the example provided in the documentation. However, every time I try, I end up with an "undefined" result. Am I missing something here? Is it no longer possible? The sqlite3 libra ...

Creating a stylish look for a selection of multiple menu options

option:active { color: red; } option:focus { color: green; background: green; } option:checked { color: yellow; background: yellow; } <select multiple> <option>One</option> <option>Two</option> <option&g ...

Utilizing Modal Windows within an ng-repeat Directive

I'm working on a project with an ng-repeat and trying to incorporate a modal that passes the same scope variable to the modal window. The modal is opening successfully, but for some reason, the scope value from ng-repeat isn't displaying inside t ...

Verify if data already exists in an HTML table column using JavaScript

As a beginner in web programming, I am currently trying to dynamically add data to a table. But before inserting the data into the table, my requirement is to first verify if the data already exists in a specific column, such as the name column. function ...

[Vue alert]: Issue with rendering: "TypeError: Unable to access property 'replace' of an undefined value"

I'm currently working on a project similar to HackerNews and encountering the following issue: vue.esm.js?efeb:591 [Vue warn]: Error in render: "TypeError: Cannot read property 'replace' of undefined" found in ---> <Item ...