Search code examples
javascriptangularjsforeachfiltergetattribute

AngularJS- How to filter by property/ attribute of an object- forEach is not a function


I have an AngularJS application, and on one of the pages, there are a number of widgets displayed. One of the widgets displays a table containing information about a system that is being monitored by the application. This table currently contain two columns: a 'Type' column, displaying the type of information shown in that row; and a 'Data' column, displaying the value of the variable that holds that information.

It is possible to customise the information displayed on the widget using a dialog that is opened when the 'Settings' button is clicked on the widget toolbar. There are 'Columns' and 'Rows' text input fields on that dialog which are used to set the columns headings and rows to be displayed in the table. Currently, when the user starts typing into the 'Rows' text input field, a drop down list is displayed showing the user the variables that are available to be displayed in the table, and that match what the user has typed (i.e. as they continue to type, the list of options available will get more specific).

The function used to display this drop down to the user while they type in the text input field is:

$scope.autocompleteVarsFilter = function(query) {
    if(!query && lastVarQryKw) {
        query = lastVarQryKw;
    }
    var result = Object.keys(fxVar.getVars()).filter(function(name) {
        return name.indexOf(query.toLowerCase()) !== -1;
    });

    if(result.length > 0) {
        lastVarQryKw = query;
    }
    return result;
};

I am currently trying to add a third column to that table, to display a link to a given page that the user will choose. I decided that a 'link' would be denoted by a string starting with the character : (i.e. if the user types in :index, that would be a link to the home page of the site).

As soon as the user types :, the full list of available pages should be displayed in a 'drop-down', and as the user continues to type, this list should be filtered based on what they are typing (as it is with variable names)

I have updated my function as follows:

$scope.autocompleteVarsFilter = function(query) {
    if(query.startWith(":") {
        var buttonsQuery = query.substring(1);
        if(!buttonsQuery && lastVarQryKw) {
            buttonsQuery = lastVarQryKw;
        }

        var userPages = pagesPresets; //This is getting the full list of available pages from a service called 'pagesPresets'
        console.log("Value of userPages: ", userPages);

        var page;
        for(page in userPages) {
            console.log("for loop started");
            if(page.key.startWith(buttonsQuery)) {
                console.log("Matching page found: ", page);
            }
        }

        if(result.length > 0) {
            lastVarQryKw = query;
        }
        return result;

    } else {
        if(!query && lastVarQryKw) {
            query = lastVarQryKw;
        }
        var result = Object.keys(fxVar.getVars()).filter(function(name) {
            return name.indexOf(query.toLowerCase()) !== -1;
        });

        if(result.length > 0) {
            lastVarQryKw = query;
        }

        return result;
    }
};

The else clause of the if statement is doing what the function was originally doing (i.e. displaying the available variables), and currently still works as intended. However, when testing this feature, if I now type : followed by any string into the text input field, the if clause runs, but the available pages are not displayed in a drop down, and I see the following output displayed in the console by my console.log() statements:

Value of userPages:

0: {_id: "...", _rev: 1, _url: "...", key: "pages/auth", {...}, ...}

1: {_id: "...", _rev: 13, _url: "...", key: "pages/userpage2", value: {...}, ...}

2: {_id: "...", _rev:13, _url: "...", key: "pages/", value: "/pages/userpage1", ...}

for loop started

TypeError: Cannot read property 'startsWith' of undefined

The issue seems to be that I can't reference the key attribute/ property of the userpage objects to match their value to the value that the user is typing into the text box.

Given that I can see that the page objects I'm using have an attribute key that holds the value I want to filter by, how do I actually get hold of this key attribute to use it?

I tried changing that for loop to:

var page;
var key;
for(page in userPages) {
    console.log("for loop started");
    key = page.getAttribute("key");
    if(key.startsWith(buttonsQuery)) {
        console.log("Matching page found: ", page);
    }
}

but this just gives the output:

for loop started

TypeError: page.getAttribute is not a function

Anyone have any suggestions?

Edit

I tried doing what @Alex K suggested in their answer, and updated the for loop in my function to:

var page;
var pageKey;
var key;
var result;

console.log("Iterating using foreach");
for(page in userPages) {
    page = userPages[page];
    if(page.key.startsWith(buttonsQuery)) {
        console.log("Matching page found: ", page.key);
        pageKey = page.key;
        result = pageKey;
    }
};

I also tried updating it to:

userPages.forEach(function(page) {
    page = userPages[page];
    if(page.key.startsWith(buttonsQuery)) {
        console.log("Matching page found: ", page.key);
        pageKey = page.key;
        result = pageKey;
    }
});

and:

userPages.forEach(function(page) {
    page = userPages[page];
    if(page.key.startsWith(buttonsQuery)) {
        console.log("Matching page found: ", page.key);
        pageKey = page.key;
        result = pageKey;
    }
});

But these all gave the same output in the console, which was:

Iterating using foreach

ctrl.js:483 Matching page found: pages/userpage2

ctrl.js:483 Matching page found: pages/userpage1

ctrl.js:483 Matching page found: pages/

ctrl.js:483 Matching page found: pages/pagetitle

ctrl.js:483 Matching page found: pages/auth

angular.js:10160 TypeError: a.forEach is not a function
at Object.b.makeObjectArray (ng-tags-input.min.js:1)
at ng-tags-input.min.js:1
at wrappedCallback (angular.js:11735)
at wrappedCallback (angular.js:11735)
at angular.js:11821
at Scope.$eval (angular.js:12864)
at Scope.$digest (angular.js:12676)
at Scope.$apply (angular.js:12968)
at angular.js:14494
at completeOutstandingRequest (angular.js:4413)

So clearly the line from inside the if statement was being run, because I was getting all of those print statements in the console, and yet for some reason, it thinks that forEach is not a function...

Anyone have any suggestions for how to fix this?


Solution

  • A for...in loop iterates over the enumerable property names of an object. You then need to access the object using that property name to get the value. Here's an example:

    console.log('Using for...in on an object:');
    
    var myObj = {
      'prop1': 'Hello world!',
      'prop2': 12345,
      'some crazy property': null
    };
    
    for (var propName in myObj) {
    
      console.log('propName is ' + propName);
    
      //Now we can lookup the value using that property name.
      var propValue = myObj[propName];
    
      console.log('myObj[' + propName + '] is ' + propValue);
    
    }

    In contrast, a forEach loop iterates directly over the values of an array. Your callback function is provided the value directly, so you don't need to access the array to retrieve it. Example:

    console.log('Using forEach on an array:');
    
    var myArray = [
      {key: 'this is object 1!'},
      {key: 'this is object 2!'}
    ];
    
    myArray.forEach(function (obj) {
      
      console.log('in ForEach loop...');
      
      console.log('obj is ' + JSON.stringify(obj));
      
      //We can directly access the object's properties as needed
      console.log('obj.key is ' + obj.key);
      
    });

    So, here's how you can update your code using for...in or using forEach:

    var buttonsQuery = 'pages/userpage2';
    
    var userPages = [{
        _id: "...",
        _rev: 1,
        _url: "...",
        key: "pages/auth"
      },
      {
        _id: "...",
        _rev: 13,
        _url: "...",
        key: "pages/userpage2"
      },
      {
        _id: "...",
        _rev: 13,
        _url: "...",
        key: "pages/",
        value: "/pages/userpage1"
      }
    ]
    
    console.log('Iterating using for...in');
    for (var prop in userPages) {
    
      console.log('prop is ' + prop);
    
      //Access the page object using the property name
      var page = userPages[prop];
    
      console.log('page is ' + JSON.stringify(page));
    
      if (page.key.startsWith(buttonsQuery)) {
        console.log("Matching page found: ", page);
      }
    }
    
    
    //Better way to iterate if userPages is an array:
    console.log('iterating using forEach');
    userPages.forEach(function(page) {
    
      //page is already the object we're interested in.
      //No need to access the array again.
      console.log('page is ' + JSON.stringify(page));
    
      //Access the object's properties as needed
      if (page.key.startsWith(buttonsQuery)) {
        console.log("Matching page found: ", page);
      }
    });

    You can also use a for...of loop to iterate, but that's not supported in IE.