Why two ways are allowed to access json keys in angularjs? - angularjs

Below is my code snippet
<body ng-app='abc' ng-controller='MainCtrl as ctrl'>
<div ng-repeat="note in ctrl.notes">
<span class="label"> {{note[1]}}</span>
<span class="label"> {{note.label}}</span>
<span class="status" ng-bind="note.done"></span>
</div>
</body>
and my notes object contains
self.notes = [
{ id : 1 , label : 'First Note' , done : false, [1]:3, 'string key':4 },
{ id : 2 , label : 'Second Note' , done : false, 1:2, 'string key':4 },
{ id : 3 , label : 'Done Note' , done : true, 1:2,'string key':4 },
{ id : 4 , label : 'Last Note' , done : false, 1:2 , 'string key':4}
];
I am able to access the label with a.label and a['label'], may I know why these two ways? The second a['label'] only is enough right?
And also if you observe in my notes object, one of the key in first row is [1], I am able to access it with note[1] in html, please let me know how come it is possible?

You can get access by a.label and a['label'] because in Javascript you can get object properties in two ways: object.property and object['property'].
let obj = {
property: "OK"
}
console.log(obj.property);
console.log(obj['property']);

Update:
With reference the article from
javascript-object-syntax
Evaluating each dot or bracket expression takes time. If the same property is used multiple times, then it makes more sense to access the property once, and then store the value in a local variable for all future uses.
The following example uses bar many times within a loop. However, instead of wasting time computing the same value over and over, bar is stored in a local variable.
var object = {
baz: {
foo: {
bar: 5
}
}
};
var bar = object.baz.foo.bar;
var count = 0;
for (var i = 0; i < 100000; i++) {
count += bar;
// better than count += object.baz.foo.bar;
}
If you are accessing a property that is not a valid identifier or if you want to dynamically populate properties from loops or expressions you should use object['property'] otherwise you use object.property.
var obj = {
'prop1': 'Valid identifier',
'2prop': 'Invalid identifier'
};
var invalidProp = Object.keys(obj)[1];
console.log(obj.prop1);
// console.log(obj.2prop); // Throws error
console.log(obj['2prop']);
console.log(obj[invalidProp]);

Related

AngularJS Custom Filter duplicates

I currently meet an issue with a custom filter. I have an array of CVE objects called 'cves' (in my scope), and for each item I generate a tr line in a table using ng-repeat.
Here is the global structure of a CVE:
cve: {
id: integer,
cve: string,
summary: text,
description: text,
cvss:{
score: float,
vector: string
}
}
Here is my HTML code
<input type='text' ng-model='searchField'/>
....
<tr ng-repeat="cve in cves | cveFilter:[ad_filters, searchField] as filtered_cves"
ng-if="cves.length > 0">
<td colspan="7" class="no-padding">
//printing infos in a custom directive
</td>
</tr>
....
Here is my filter :
.filter('cveFilter', function () {
return function (cves, params) {
console.log(cves);
let items = {
ids: params[0],//the ids (array of ids)
text: params[1],//the text
filtered_cves: []//the output
};
// for each cve, if its ID is in items.ids
// AND if one of the property of the CVE match with items.text
// push it to items.filtered_cves
cves.forEach(function (cve) {
if (
items.ids.includes(cve.id) &&
(
cve.cve.match(items.text) ||
cve.summary.match(items.text) ||
cve.description.match(items.text) ||
cve.cvss.score.match(items.text) ||
cve.cvss.vector.match(items.text)
)
) {
items.filtered_cves.push(cve)
}
});
return items.filtered_cves;
};
});
My problem is the following : my filter seems to work, it keeps only the matching CVEs but it displays each CVE in duplicate. That means if I have 6 cves in my $scopes.cves array, i will have 12 lines in my html table.
It's my first custom filter but I think it's a stupid mistake.
Do you know where I failed ?
Thanking you in advance,
It is duplicating the data, I don't get blank lines.
If I print the content of $scope.filtered_cves, I get (let's say I am supposed to get 8) 16 elements.
I didn't mention that earlier, but $scope.ad_filters is an array of CVE IDs I want to display. A CVE is displayed only if its ID is in $scope.ad_filters AND if one of its property matches with the content of the input form text.
I can't screenshot at the moment, I need to put fake data.
Here is the updated code for my filter (nothing really changed, just added some functions) :
.filter('cveFilter', function () {
return function (cves, params) {
let items = {
ids: params[0],
text: params[1],
filtered_cves: []
};
cves.forEach(function (cve) {
console.log(cve);
if (items.ids.includes(cve.id)) {
if (items.text === '' || items.text === null || items.text === undefined ||
(
cve.cve.toString().includes(items.text) ||
cve.summary.toString().includes(items.text) ||
cve.description.toString().includes(items.text) ||
cve.cvss.score.toString().includes(items.text) ||
cve.cvss.vector.toString().includes(items.text)
)
) {
items.filtered_cves.push(cve)
}
}
});
return items.filtered_cves;
};
});
I ran it and I noticed that it is executed several times, and the last time it prints twice too much lines.

Group objects by field, then group by a conditional field

I have a list of objects that I'm using an ng-repeat to display.
I need to be be able to group these objects by a field, such as an departmentID.
I then need to be able to loop through each department, and then by the 2nd field, which will either be preliminary or final. If there is a final record for a given department, the preliminary record should be hidden. If there is no final for a given department, show the preliminary.
Ex.
Department 1
Preliminary - not visible
Department 1
Final - visible
Department 2
Final - visible
Department 3
Preliminary - not visible
Department 3
Final - visible
Department 4
Preliminary - visible
Data Sample
var data = [
{ departmentID: 1, status: 'preliminary' },
{ departmentID: 1, status: 'final' },
{ departmentID: 2, status: 'final' },
{ departmentID: 3, status: 'preliminary' },
{ departmentID: 3, status: 'final' },
{ departmentID: 4, status: 'preliminary' },
];
Current code below
if (item.result_status !== "preliminary") {
return item;
}
else if (item.result_status === "final") {
return item;
}
else {
return false;
}
<div ng-repeat="(key, value) in Data | groupBy: 'department_ID'">
<div ng-repeat="v in value | filter:prelimFilter(value)">
<a ng-click="showPopUp(key)">{{Some Other Field}} </a>
</div>
</div>
<!-- begin snippet: js hide: false console: true babel: false -->
You are close! Your first half of the code is correct where you group the object based on departmentID using groupBy filter. The issue is with your custom filter. You have created a filter function instead of custom filter and the issue is filter function does not have access to entire array. It has access only to individual elements. This blog explains clearly the different types of filter that can be created in angularjs.
You can create a custom filter which has access to the array and you can return the filtered array based on status
.filter('myFilter', function () {
return function(inputArr) {
var outputArr = [];
outputArr = inputArr.filter(function(obj){return obj.status === "final"});
outputArr = outputArr.filter(function(e){return e}); //To remove empty elements
if(outputArr.length === 0){
outputArr = inputArr; //inputArr does not have obj with status 'final'
}
return outputArr;
};
});
Working jsfiddle here. Good luck!
NOTE: groupBy filter is not part of angular.js You have to include angular.filter module.

AngularJS nested filter hiding elements without destined property

I'm using AngularJS on a project and I need to implement a select box with a filter to a nested property. My object (I have an array of those and I'm iterating through them via ng-repeat) has a structure similar to this:
{
id: 1,
name: 'Example',
groups: [
{ id: 1, name: 'Group 1' },
{ id: 2, name: 'Group 2' }
]
}
I need to filter the group ID of the elements, and after searching I've come up with two ways to do it:
1.
| filter: { $: { id: search.item_id } }
Which has these problems:
Apparently it searches for any nested properties named ID, so if I have more than one object inside my main object, with a property called ID too, it would add it to the filter. Example:
{
id: 2,
name: 'Example 2',
groups: [
{ id: 1, name: 'Group 1' },
{ id: 2, name: 'Group 2' }
],
categories: [
{ id: 1, name: 'Cat 1' },
{ id: 2, name: 'Cat 2' }
]
}
Filtering for ID 1 would select not only group with ID 1, but category with ID 1 too.
Also, with this method, even before setting the filter (search.item_id model is null), objects without groups are being filtered and not appearing in the list. These objects are like this:
{
id: 3,
name: 'Example 3',
groups: []
}
and the other way is:
2.
| filter: { groups: [{ id: search.item_id }] }
In this case, the problem is that it simply doesn't work, it filters everything, leaving the list blank, no matter if it's set or which option is selected.
How can I make it work? I've been searching and couldn't find anything about this. Filtering nested properties is (or should be) a very basic thing.
Update:
So, xtx first solution kinda did it, but only if I use input text or number, but I need to use a select box (more specifically uib-dropdown, but working in a regular select is the next step). My select box is looking like this:
<select name="filter_classes_groups_test" ng-model="search.group_id">
<option val="group.id" ng-repeat="group in classesGroups">{{ group.name }}</option>
</select>
When I interact with it, nothing happens.
If creating a custom filter works for you, here is how you can do that:
app.filter('groupsFilter', function() {
return function(input, groupId) {
var out = [];
if (!groupId || isNaN(parseInt(groupId))) {
return input;
}
angular.forEach(input, function(item) {
if (item.groups && angular.isArray(item.groups)) {
angular.forEach(item.groups, function (group) {
if (group.id === parseInt(groupId)) {
out.push(item);
}
});
}
});
return out;
}
});
As you can see, the custom filter has name groupsFilter, and takes group id to search for as a parameter. The filter can be applied like this:
<div ng-repeat="item in data | groupsFilter:search.item_id">
...
</div>
UPDATE:
Instead of creating a custom filter, you can just create a function that implements filtering logic, in scope like this:
$scope.groupsFilterLocal = function(value) {
if (!$scope.search.item_id || isNaN(parseInt($scope.search.item_id))) {
return true;
}
if (!value || !value.groups || !angular.isArray(value.groups)) {
return false;
}
for (var i = 0; i < value.groups.length; i++) {
if (value.groups[i].id === parseInt($scope.search.item_id)) {
return true;
}
}
return false;
};
and then apply it using build-in filter like this:
<div ng-repeat="item in data | filter:groupsFilterLocal ">
...
</div>
Notice that in this case you can't pass the value to search for (search.item_id) into your function groupsFilterLocal like it is done for the custom filter groupsFilter above. So groupsFilterLocal has to access search.item_id directly
UPDATE 2: How to create a select properly
The reason why the filter is not applied when you pick a group in your dropdown, is in the way how you defined the select. Instead of the id of the selected group, search.group_id gets assigned group's name.
Try defining the select like shown below:
<select name="filter_classes_groups_test" ng-model="search.item_id" ng-options="group.id as group.name for group in classesGroups">
<option value="">-- Choose Group --</option>
</select>
To ensure that search.item_id gets id of the group that is selected (and not its name), try temporarily adding {{ search.item_id }} somewhere in your template.

calling function nested within ng-repeat not returning correct value to $scope

I'm trying to count the number of trivia questions a user has answered. For each questio (statement), where the value is greater than 0 -- that indicates the question is answered. I can see these correct values in the console.log, but I cannot get the value to show in $scope or displayed on the frontend: It displays a number, but the number does not match the results of the console.log:
html / haml :
categories(ng-repeat='category in service.categories' ng-class="{'first':$first}" )
.row(ng-click='showQuestions = !showQuestions' ng-class="{'ballotSelected' : showQuestions}")
.label
.chevron(ng-class="showQuestions ? 'fa fa-chevron-down ballotSelected' : 'fa fa-chevron-right'")
.name.text-uppercase
{{category.name}} {{category.statements.length}}
.questionCount(ng-init='getStatementCount(category)')
{{statementCounts}}
.showRow(ng-show='showQuestions')
controller:
$scope.getStatementCount = function(category) {
var i, questionCount;
console.log(category);
i = 0;
questionCount = 0;
while (i < category.statements.length) {
if (category.statements[i].value > 0) {
questionCount += 1;
}
i++;
}
console.log(questionCount, 'COUNT');
return $scope.statementCounts = questionCount;
};
Eevery time you call getStatementsCount(), you overwrite a unique scope variable statementCounts, that is displayed for all the categories.
That can't be right.
So,
either you store the result in the category itself, and display the category's result in each category. You should do that without using ng-init, in the controller, as soon as the categories are loaded/provided.
or you don't assign any variable with the result, and display the result directly in the page:
.
$scope.getStatementCount = function(category) {
...
return questionCount;
}
and
.questionCount
{{ getStatementCounts(category) }}

select box : display text 'error' if value not exist in array

I have a key value pair defined as below, which is being used for select using ng-options
$scope.BucketEnum = [
{ display: 'Error', value: 0 },
{ display: '1', value: 1 },
{ display: '2', value: 2 },
{ display: '3', value: 3 },
{ display: '4', value: 4 },
{ display: '5', value: 5 },
{ display: 'Flows', value: 125 },
{ display: 'Recovery', value: 151 }
];
I am using this key value pair to display select box in ng-options
<select ng-model="selectedBucket" ng-options="row.value as rows.display for row in BucketEnum" multiple="multiple" ></select>
now if I set ng-model i.e. $scope.selectedBucket = 10, I want to display the text Error. Is it possible to show value Error for all the values which are not there in $scope.BucketEnum array.
NOTE
I am looking at a more generic way to do this e.g a filter for doing this
SCENARIO
There is certain historical data in database, which has some garbage and some good data.
For each garbage value, i need to show the current garbage value as well as the valid values to select from, so for the end users to fix it.
Would this fit your needs ?
jsfiddle
app.filter('bootstrapValues', function(){
return function(initial, baseBucket){
var result = [];
for(var i=0; i<initial.length; i++){
var flag = false;
for(var j=1; j<baseBucket.length; j++){ //from 1 or 0.. you call
if(initial[i] === baseBucket[j].value){
flag = true;
result.push(baseBucket[j]);
break; // if there are repeated elements
}
}
if(!flag)
result.push(baseBucket[0])
}
return result;
};
});
Using it to start the selectedBucket, in your controller:
// setting initials
$scope.selectedBucket = $filter('bootstrapValues')(initialSet, $scope.bucketEnum);
Does it help?
Edit: Here is other jsfiddle with little modifications, if the value is not in the bucket it add the element to the list with Error display and as a selected value.
Using ng-options generates multiple HTML <select> elements for each item in your BucketEnum array and 'returns' the selected value in your ng-model variable: selectedBucket. I think the only way to display the options without an additional blank entry is to ensure the value of selectedBucket is a valid entry in BucketEnum.
Your question states:
if I set ng-model i.e. $scope.selectedBucket = 10, I want to display
the text Error.
I assume you want to display the value: {{BucketEnum[selectedBucket].display}}
So... starting with $scope.selectedBucket = 10, we want some generic way of implementing a select using ng-options which will reset this value to a default.
You could do this by implementing an attribute directive, allowing you to write:
<select ng-model="selectedBucket" select-default="BucketEnum"
ng-options="row.value as row.display for row in BucketEnum"
multiple="multiple">
An example of this approach is shown below. Note that this assumes the default value is zero and does not handle multiple selections (you'd have to iterate over the selections when comparing to each item in BucketEnum and decide what to do if there is a mix of valid and invalid selections).
app.directive("selectDefault",function(){
return{
restrict: 'A',
scope: false,
link:function(scope,element,attrs){
var arr= scope[attrs.selectDefault]; // array from attribute
scope.$watch(attrs.ngModel,function(){
var i, ok=false;
var sel= scope[attrs.ngModel]; // ng-model variable
for( i=0; i<arr.length; i++){ // variable in array ?
if( arr[i].value == sel ) // nasty '==' only for demo
ok= true;
}
if( ! ok )
scope[attrs.ngModel]=0; // set selectedBucket to 0
});
}
};
});
I've run up a jsfiddle of this here
The downside of this is that I've used a $watch on the ng-model which causes side-effects, i.e. any assignment of the named variable will trigger the $watch function.
If this is the sort of solution you were looking for, you could expand the directive in all sorts of ways, for example:
<select ng-model="selectResult"
select-default="99" array="BucketEnum" initial="selectedBucket"
ng-options="row.value as row.display for row in BucketEnum"
multiple="multiple">
...the idea being that the select-default directive would read the default value ("99" here), the array and an initial value then set selectResult accordingly
You would need to code for this explicitly. Scan the choices you want to set against the choices that are present. If you don't find it, select the Error value too.
Note also that you need to pass an array for selectedBucket and it needs to include the actual option objects not just the values inside them.
<div ng-app="myApp">
<div ng-controller="myController">
<p>Select something</p>
<select ng-model="selectedBucket"
ng-options="row as row.display for row in bucketEnum" multiple="multiple">
</select>
</div>
</div>
.
var app = angular.module('myApp', []);
app.controller('myController', function ($scope) {
var initialSet = [1, 5, 10];
$scope.bucketEnum = [
{ display: 'Error', value: 0 },
{ display: '1', value: 1 },
{ display: '2', value: 2 },
{ display: '3', value: 3 },
{ display: '4', value: 4 },
{ display: '5', value: 5 },
{ display: 'Flows', value: 125 },
{ display: 'Recovery', value: 151 }
];
var selected = [];
var error = $scope.bucketEnum[0];
angular.forEach(initialSet, function(item) {
var found;
angular.forEach($scope.bucketEnum, function (e) {
if (+item == +e.value) {
console.log('Found ', e);
found = item;
selected.push(e);
}
});
if (typeof found === 'undefined') {
selected.push(error);
}
$scope.selectedBucket = selected;
console.log(selected);
});
});

Resources