Using ng-if and ng-repeat correctly - angularjs

I have a rudimentary understanding of AngularJS and have a couple (possibly stupid) questions about ng-if and ng-repeat.
If I have a ng-repeat that looks like this:
For all containers without titles, would the ng-repeat produce those containers and not show them? Or do they simply don't exist? The reason I ask is I've tried to replace ng-if with ng-show, but it does not produce the same outcome of "hiding" containers that do not have titles.
Is there a way to code a ng-if to say if the next container has no title, then do something. I tried something like ng-if="item.title=='' && $index+1", without any luck.
Any suggestions?
My data looks like this:
"_sections": [{
"_bootstrap_cells": 6,
"_count": 2,
"visible": true,
"columns": [{
"fields": [{
"name": "type_of_account",
"type": "field"
}, {
"name": "routing_transit_number",
"type": "field"
}]
}, {
"fields": [{
"name": "type_of_payment",
"type": "field"
}, {
"name": "check_digit",
"type": "field"
}]
}],
"caption": "Direct Deposit",
"id": "b456b9d2137ac340177c36328144b0ef"
}, {
"_bootstrap_cells": 12,
"_count": 1,
"visible": true,
"columns": [{
"fields": [{
"name": "account_number",
"type": "field"
}, {
"name": "account_title",
"type": "field"
}, {
"name": "financial_institution_name",
"type": "field"
}]
}],
"caption": "",
"id": ""
}
}]
The first section has two columns and a bootstrap cell value of 6 for each column, the second section only has one column and a bootstrap cell of 12. Since the second doesn't have a caption, I want it to be appended to the first section, but keep both sections' bootstrap formatting.

If containers is an array then it does not have a title property. You need to check the title on each item.
Also note that ng-if will not hide the content, but remove it or not insert it into the DOM. The counterpart of ng-show is ng-hide.
angular.module('appModule', [])
.controller('MyController', [function() {
this.containers = [{
aaa: 1,
title: 'One'
}, {
aaa: 2,
title: ''
}, {
aaa: 3,
title: 'Three'
}]
}]);
angular.bootstrap(window.document, ['appModule'], {
strictDi: true
});
<div ng-controller="MyController as myCtrl">
<div ng-repeat="item in myCtrl.containers track by $index">
<div ng-if="item.title!=''">{{$index}}. {{item.title}}</div>
</div>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.7/angular.min.js"></script>

Related

angularjs: extending directive then make modifications

I am working in ServiceNow and am trying to extend an ootb directive, then make some modifications to a couple functions. So far I've figured out how to extend the directive:
function (spModelDirective){
return angular.extend({}, spModelDirective[0], {
templateUrl:'lbl_custom_template.xml'
});
}
Within this directive, there's a function called getNestedFields that I would like to make edits to:
function getNestedFields(fields, containers) {
if (!containers)
return;
for (var _container in containers) {
var container = containers[_container];
if (container.columns) {
for (var _col in container.columns) {
var col = container.columns[_col];
for (var _field in col.fields) {
var field = col.fields[_field];
if (field.type == "container" && container.caption != "")
getNestedFields(fields, [field]);
else if (field.type == "checkbox_container")
getNestedFields(fields, field.containers);
else if (field.type == "field" || container.caption=="")
fields.push(formModel._fields[field.name]);
}
}
}
}
}
Can someone provide some guidance on what the correct syntax for this would be?
More information
Our team cloned the ootb widget-form and am trying to create a custom layout. Basically, we want each form section to be it's own tab much like the back-end form instead of one long form, which is what the ootb widget-form currently does. In the very first line of the sp-variable-layout template, it shows:
<fieldset ng-init="$last ? execItemScripts() : null" ng-show="isContainerVisible(container)" ng-repeat="container in containers">
The ng-repeat of container in containers considers each form section as a separate container (which is perfect), BUT it also considers any splits as a separate container as well. So for example, if my form's layout looks like this:
This will create two tabs: one that has every field within the begin and end splits AND a separate tab with everything after the end split. The JSON object that is created looks like this:
{
"_bootstrap_cells": 6,
"_count": 2,
"visible": true,
"columns": [{
"fields": [{
"name": "type_of_account",
"type": "field"
}, {
"name": "routing_transit_number",
"type": "field"
}]
}, {
"fields": [{
"name": "type_of_payment",
"type": "field"
}, {
"name": "check_digit",
"type": "field"
}]
}],
"caption": "Direct Deposit",
"id": "b456b9d2137ac340177c36328144b0ef",
"scope_name": "x_dnf_table"
}, {
"_bootstrap_cells": 12,
"_count": 1,
"visible": true,
"columns": [{
"fields": [{
"name": "account_number",
"type": "field"
}, {
"name": "account_title",
"type": "field"
}, {
"name": "financial_institution_name",
"type": "field"
}]
}],
"caption": "",
"id": "",
"scope_name": "x_dnf_table"
}
Notice the first "section" has a caption, but ServiceNow treats the split section as its own separate section with no caption at all.
I want to change the spModel directive to produce only containers with captions as their own tab and if a container does NOT have a caption, to append it to the previous container that does have a caption.
I don't think you can edit this function as this is hosted as a file on Servicenow. See https://hi.service-now.com/scripts/app.$sp/directive.spModel.js then just control-f for the getNestedFields.
Per this thread; https://community.servicenow.com/thread/247907#1059129 I believe spModal is just a wrapper for $uibModal.
What you can do is make your own directive on sp_angular_provider.

AngularJS custom directive scope not reflecting

I have a custom directive called crust:
JS:
.directive('crust', function(){
return{
restrict: 'EA',
replace: true,
scope: {
datasource: '='
},
templateUrl: '../../configurator/partials/crust.html'
}
})
HTML template (crust.html):
<li data-ng-repeat="type in datasource.types">
<input type="radio"
name="{{datasource.id}}"
data-ng-class="radio-selector"
data-ng-true-value="true"
value="true"
data-ng-model="type.selected"
data-ng-change="updatePizza(type)"
id="{{type.id}}">
<label for="{{type.id}}"> <span></span>
<h2 data-ng-bind="type.name"></h2>
<p data-ng-bind="type.description"></p>
</label>
</li>
The Model (crustTypes) is pulled via a service from this JSON:
{
"id": "crt",
"types": [{
"id": "crt1",
"name": "original",
"description": "Our traditional scratch-made crust",
"price": "5",
"selected":"false"
}, {
"id": "crt2",
"name": "thin",
"description": "A light crispier crust",
"price": "6",
"selected":"false"
}, {
"id": "crt3",
"name": "fresh pan",
"description": "A thick buttery crust",
"price": "7",
"selected":"false"
}, {
"id": "crt4",
"name": "stuffed",
"description": "Two layers of original crust",
"price": "8",
"selected":"false"
}]
}
The directive is being invoked in the HTML like so:
<ul>
<crust data-datasource="crustTypes" data-datavalue="pcrustType"></crust>
</ul>
The looping is working fine, and ng-repeat is rendering the list properly. The problem is that I want to assign datasource.id as the common name of the radio group, and due to some reason, datasource.id is coming up as undefined. Consequently, the name is not being assigned and the user is being allowed to enter multiple selections.
If instead I pass type to updatePizza(item) it comes up fine. Its just the parent model that's not being displayed
If I try to return datasource through updatePizza(), it is still coming up as undefined.
I'm sure I'm missing something basic here. Help!
Here is a Plunker of the code
Replace name="{{datasource.id}}" with name="{{$parent.datasource.id}}"

ng-repeat + ng-bind Generates 120 loops when should be only 20. No idea why

Trying to create a dynamic svg icon generator to flesh out a grid of 5 days * 4 time periods per day with a built-on-the-fly svg.
Expected: 20 repeats
Result: 120 repeats, which has 20 svgs that repeat the contents 5 times
Why? No idea.
somecontroller.js:
var counter = 0;
$scope.iconGenerator = function ( i, parenti, tod ) {
counter++;
// console.log(counter);
//append svg contents
}`
index.html
<div class = "row">
<div class = "forecast-day col-md-fifth" ng-repeat = 'day in forecastData'>
<div class = "forecast-group" ng-repeat = "dayDetails in day.conditions">
<svg id = "{{myID($index, $parent.$index)}}"
ng-bind = "iconGenerator($index,$parent.$index, dayDetails)">
</svg>
</div>
</div>
</div>
data.json:
[
{
"ID" : 0,
"conditions": [
{
"key": "value"
},
{
"key": "value"
},
{
"key": "value"
},
{
"key": "value"
}
]
}
//array contains total: 5 objects like above
]
Edit:
I get it now. You are incrementing the value of a variable in the function you call in the ng-bind directive. This is never a good idea. I think that ng-bind must always refer to a variable or a 'getter' function that does not modify the state of the application.
To understand what I am trying to suggest, look again at the plunk http://plnkr.co/edit/eMams7nTe5qcxw2jC39i?p=preview. I added an input box with an ng-modelof x. Try modifying the input. Every time you change the input, angularjs has to reload the data in the entire document to check if anything changed. To do this, it must call the function iconGenerator repeatedly. In your case this function increments a value - and as a result the value increases every time you make a change.
You cannot rely on angular calling the iconGenerator function only once. So you need to change the code keeping that in consideration.
I tried to recreate your code here : http://plnkr.co/edit/eMams7nTe5qcxw2jC39i?p=preview and it seems to work fine. I think the problem is somewhere else (aside from the code you posted).
<head>
<script data-require="angular.js#1.3.0" data-semver="1.3.0" src="//code.angularjs.org/1.3.0/angular.js"></script>
<link rel="stylesheet" href="style.css" />
<script>
angular.module('app', [])
.controller('mainCtrl', function($scope) {
$scope.forecastData = [{
"ID": 0,
"conditions": [{
"key": "value"
}, {
"key": "value"
}, {
"key": "value"
}, {
"key": "value"
}]
}, {
"ID": 1,
"conditions": [{
"key": "value"
}, {
"key": "value"
}, {
"key": "value"
}, {
"key": "value"
}]
}, {
"ID": 2,
"conditions": [{
"key": "value"
}, {
"key": "value"
}, {
"key": "value"
}, {
"key": "value"
}]
}, {
"ID": 3,
"conditions": [{
"key": "value"
}, {
"key": "value"
}, {
"key": "value"
}, {
"key": "value"
}]
}, {
"ID": 4,
"conditions": [{
"key": "value"
}, {
"key": "value"
}, {
"key": "value"
}, {
"key": "value"
}]
}
];
var counter = 0;
$scope.iconGenerator = function(i, parenti, tod) {
counter++;
//append svg contents
};
});
</script>
</head>
<body ng-app="app">
<div ng-controller="mainCtrl">
<div ng-repeat="day in forecastData">
<div ng-repeat="dayDetails in day.conditions">
<svg id="{{myID($index, $parent.$index)}}" ng-bind="iconGenerator($index,$parent.$index, dayDetails)">
</svg>
{{$parent.$index}}-{{$index}}
</div>
</div>
</div>
</body>
</html>

Dropdown inside select in angular js

I have the following JSON object :
[{
"Question": "Sample question One",
"Options": [{
"OptionId": 1,
"Option": "Yes"
}, {
"OptionId": 2,
"Option": "No"
}]
}, {
"Question": "Sample question Two",
"Options": [{
"OptionId": 1,
"Option": "Yes"
}, {
"OptionId": 2,
"Option": "No"
}]
}, {
"Question": "Sample question Three",
"Options": [{
"OptionId": 1,
"Option": "Yes"
}, {
"OptionId": 2,
"Option": "No",
}, {
"OptionId": 3,
"Option": "May be",
}]
}]
I am using a select with ng-options to iterate each question. Inside the select I am adding a dropdown with the options binded to it.
This is works properly.
Now I have to hide a div if the option selected for the second question is no and if the option selected is yes, i have to show the div.
I tried using ng-show and ng-hide based on a model property "isDetailsVisible". I thought it would be easy to bind this to the second question's answer but I am unable to think of a way to do this.
Does anyone have ideas?
This may help you I think,
In this I have seperated the display as follows
Custom directive questioninfo: for displaying the information about the selected question. This also shows the options of the selected question, but before iterating, i use filter on the options to get only available options(with value 'Yes')
Filter optionFilter: For filtering the options, I have applied the filter to select options with given value('Yes') in 'Option'
var app = angular.module('plunker', []);
app.factory('dataService', function() {
return {
data: {
"Questions": [{
"Question": "Sample question One",
"Options": [{
"OptionId": 1,
"Option": "Yes"
}, {
"OptionId": 2,
"Option": "No"
}]
}, {
"Question": "Sample question Two",
"Options": [{
"OptionId": 1,
"Option": "No"
}, {
"OptionId": 2,
"Option": "No"
}]
}]
}
};
});
// I am using this filter because options should not be many
app.filter('optionFilter', function() {
return function(items, value) {
if (!angular.isUndefined(items)) {
var arrayToReturn = [];
for (var i = 0; i < items.length; i++) {
if (items[i].Option === value) {
arrayToReturn.push(items[i]);
}
}
return arrayToReturn;
}
};
});
app.directive('questioninfo', function() {
return {
restrict: 'AE',
scope: {
selectedQuestion:'=question'
},
controller: function($scope) {},
template: '<h2>{{selectedQuestion.Question}}</h2>\
<h3>Available Options</h3>\
<div ng-repeat="option in selectedQuestion.Options|optionFilter:\'Yes\'"> \
<div ng-if="shouldShowOption(option)"><b>Option {{option.OptionId}}</b> {{option.Option}}</div>\
</div>\
\
'
};
})
app.controller('MainCtrl', function($scope, dataService) {
$scope.name = 'World';
$scope.data = dataService.data;
$scope.Questions = dataService.data.Questions;
$scope.selectedQuestion = dataService.data.Questions[1].Question;
});
HTML Code:
<body ng-controller="MainCtrl">
<h1>Select Question</h1>
<select id="s1" ng-model="selectedQuestion" ng-options="item as item.Question for item in Questions">
</select>
<h3>The selected item just for reference:</h3>
<pre>{{selectedQuestion | json}}</pre>
<hr>
<questioninfo question="selectedQuestion"></questioninfo>
</body>
</html>
Check this link [Plunker]: http://plnkr.co/edit/LIVVofC8nyYtbYYVBBCb?p=preview
Thanks Ganesh for the detailed answer. I am expecting a little more simpler way.
I think I found an answer. I updated the JSON to make it easier, but this does not impact much.
[{
"Question": "Sample question One",
"Options": ["Yes", "No"],
"SelectedOption": "Yes"
}, {
"Question": "Sample question Two",
"Options": ["Yes", "No"],
"SelectedOption": "Yes"
}, {
"Question": "Sample question Three",
"Options": ["Yes", "No"],
"SelectedOption": "Yes"
}]
on the html, I have the following :
<div class="col-md-12 nopadding" data-ng-repeat="Question in Questions">
<select ng-model="question.SelectedOption" ng-options="option for option in Question.Options"> </select></div>
and for the div which I have to show and hide, I am using ng-show.
<div class="box-3" ng-show="Questions[1].SelectedOption === 'Yes'">
This works fine. But I feel the ng-show should have been mapped to a property or a method so that the logic resides in the controller and not in the html.
I would appreciate any comments on my above approach or any thoughts on how to have this logic moved to controller. Questions are not loaded initially. There is a button click on which the question loads and from that time I want the div to respond to the answer of the
second question. Till the question loads, the div is hidden.

ng-repeat does not react to given limitTo or filter

I have a directive that displays subcategories of a feature list.
Problem is, all items are displayed, regardles of the filters i use for the ng-repeat.
I use a filter to only show items with a certain category_uid and i also use a limitTo.
app.directive('featureCategory', function() {
return {
restrict: 'A',
transclude: true,
link: function($scope) {
$scope.featureCountBuffer = $scope.featureCount;
$scope.listLength = Object.keys($scope.featureList).length;
},
scope: {
featureList: '=',
featureCount: '#',
categoryId: '#'
},
template:
'<li>featureCountBuffer : {{featureCountBuffer}} / listLength {{listLength}} / categoryId {{categoryId}}</li>' +
'<check-item ng-repeat="feature in featureList | filter:{category_uid:categoryId} | limitTo: featureCountBuffer" feature="feature"></check-item>'
}
});
This shows above 40 items thou it starts with the li element showing featureCountBuffer is 5 andcategory_uid being 1 which only applies to 8 items.
Does anyone see my Mistake here?
The featureList looks like this:
{
"1": {
"uid": "1",
"title": "foo",
"category_uid": "1",
"checked": false
},
"2": {
"uid": "2",
"title": "bar",
"category_uid": "1",
"checked": false
},
"3": {
"uid": "3",
"title": "foobar",
"category_uid": "2",
"checked": false
},
"4": {
"uid": "4",
"title": "barfoo",
"category_uid": "2",
"checked": false
},
"5": {
"uid": "5",
"title": "barbar",
"category_uid": "3",
"checked": false
}
...
}
I found the Problem.
Angulars ng-repeat works with objects of objects but the filters do not.
You can either write your own filters to fix this or just use an array of objects.
I used lodash's _.toArray to convert mine. My Changes:
[..]
link: function($scope) {
[..]
$scope.featureList = _.toArray($scope.featureList);
},
[..]
Youcan read about it in more detail in this article.
Thanks for posting this! I found an alternative solution to use the $index property:
<ul>
<li ng-repeat="(id, value) in test" ng-if="$index < 3" >
{{ $index }} {{ id }} {{ value }}
</li>
</ul>

Resources