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?
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.