Using ng-init to call controller functions and passing vars between controllers - angularjs

The below method works...however, it only does so with $timeout added to the tabList() function. The ng-init is executing before the DOM renders thus the document.getElementById('')'s is coming back as undefined. I must force a delayed timer of 1 to 2 seconds until the DOM loads before appending the elements. This is not optimal but it does work. I am looking for another method that is cleaner and not dependent on delayed execution.
angular.module('starter.controllers', [])
.constant('constants', {
tabColors: {
curID:0,
},
})
.controller('TabsCtrl', function($scope,Tabs,constants) {
$scope.constants = constants;
$scope.tabList = function() {
var tID = $scope.constants.tabColors ;
console.log(tID.curID) ;
if (tID.curID) {
$timeout(function() {
document.getElementById('bike_tabItem_'+tID.curID).style.color = 'green' ;
document.getElementById('bike_tabItem_'+tID.curID).style.color = 'black' ;
},1000) ;
}
}
})
.controller('TabDetailCtrl', function($state,$scope,$stateParams,Tabs,constants) {
$scope.constants = constants; //make it available constants on html
$scope.itemSelect = function(thisID) {
$scope.constants.tabColors.oldID = $scope.constants.tabColors.curID ;
delete $scope.constants.tabColors['tabID_'+$scope.constants.tabColors.curID] ;
$scope.constants.tabColors.curID = thisID ;
$scope.constants.tabColors['tabID_'+thisID] = 'green' ;
}
})
// In HTML on Tab.html :
<ion-item cache-view="false" id="tab_tabItem_{{tab.tabID}}" ng-init="tabList()">
// In HTML on Tab-Detail.html
<button id="tab_button" class="button button-small button-outline button-positive" ng-click="itemSelect({{tab.tabID}});">
Select this item
</button>
On a side note, another way to call tabList() is like:
ng-init="tabList('{{tab.tabID}}')"
This gives you a way of passing values through the ng-init which, unlike my above call, gives you better control without having to define globals. Though you would still need a global for the above to track which element was turned green so you could then set it back to black before setting the new element green.

As said in the AngularJS documentation you should avoid the use of ng-init.
The only appropriate use of ngInit is for aliasing special properties of ngRepeat, as seen in the demo below. Besides this case, you should use controllers rather than ngInit to initialize values on a scope.
To passes variables between controllers you can use a provider.

Related

AngularJS ng-repeat update does not apply when object keys stay the same?

I'm trying to make a minimal but fancy AngularJS tutorial example, and I am running into an issue where after updating the entire tree for a model (inside the scope of an ng-change update), a template that is driven by a top-level ng-repeat is not re-rendered at all.
However, if I add the code $scope.data = {} at a strategic place, it starts working; but then the display flashes instead of being nice and smooth. And it's not a great example of how AngularJS automatic data binding works.
What am I missing; and what would be the right fix?
Exact code - select a country from the dropdown -
This jsFiddle does not work: http://jsfiddle.net/f9zxt36g/
This jsFiddle works but flickers: http://jsfiddle.net/y090my10/
var app = angular.module('factbook', []);
app.controller('loadfact', function($scope, $http) {
$scope.country = 'europe/uk';
$scope.safe = function safe(name) { // Makes a safe CSS class name
return name.replace(/[_\W]+/g, '_').toLowerCase();
};
$scope.trunc = function trunc(text) { // Truncates text to 500 chars
return (text.length < 500) ? text : text.substr(0, 500) + "...";
};
$scope.update = function() { // Handles country selection
// $scope.data = {}; // uncomment to force rednering; an angular bug?
$http.get('https://rawgit.com/opendatajson/factbook.json/master/' +
$scope.country + '.json').then(function(response) {
$scope.data = response.data;
});
};
$scope.countries = [
{id: 'europe/uk', name: 'UK'},
{id: 'africa/eg', name: 'Egypt'},
{id: 'east-n-southeast-asia/ch', name: 'China'}
];
$scope.update();
});
The template is driven by ng-repeat:
<div ng-app="factbook" ng-controller="loadfact">
<select ng-model="country" ng-change="update()"
ng-options="item.id as item.name for item in countries">
</select>
<div ng-repeat="(heading, section) in data"
ng-init="depth = 1"
ng-include="'recurse.template'"></div>
<!-- A template for nested sections with heading and body parts -->
<script type="text/ng-template" id="recurse.template">
<div ng-if="section.text"
class="level{{depth}} section fact ng-class:safe(heading);">
<div class="level{{depth}} heading factname">{{heading}}</div>
<div class="level{{depth}} body factvalue">{{trunc(section.text)}}</div>
</div>
<div ng-if="!section.text"
class="level{{depth}} section ng-class:safe(heading);">
<div class="level{{depth}} heading">{{heading}}</div>
<div ng-repeat="(heading, body) in section"
ng-init="depth = depth+1; section = body;"
ng-include="'recurse.template'"
class="level{{depth-1}} body"></div>
</div>
</script>
</div>
What am I missing?
You changed reference of section property by executing section = body; inside of ng-if directives $scope. What happened in details (https://docs.angularjs.org/api/ng/directive/ngIf):
ng-repeat on data created $scope for ng-repeat with properties heading and section;
Template from ng-include $compile'd with $scope from 1st step;
According to documentation ng-if created own $scope using inheritance and duplicated heading and section;
ng-repeat inside of template executed section = body and changed reference to which will point section property inside ngIf.$scope;
As section is inherited property, you directed are displaying section property from another $scope, different from initial $scope of parent of ngIf.
This is easily traced - just add:
...
<script type="text/ng-template" id="recurse.template">
{{section.Background.text}}
...
and you will notice that section.Background.text actually appoints to proper value and changed accordingly while section.text under ngIf.$scope is not changed ever.
Whatever you update $scope.data reference, ng-if does not cares as it's own section still referencing to previous object that was not cleared by garbage collector.
Reccomdendation:
Do not use recursion in templates. Serialize your response and create flat object that will be displayed without need of recursion. As your template desired to display static titles and dynamic texts. That's why you have lagging rendering - you did not used one-way-binding for such static things like section titles. Some performance tips.
P.S. Just do recursion not in template but at business logic place when you manage your data. ECMAScript is very sensitive to references and best practice is to keep templates simple - no assignments, no mutating, no business logic in templates. Also Angular goes wild with $watcher's when you updating every of your section so many times without end.
Thanks to Apperion and anoop for their analysis. I have narrowed down the problem, and the upshot is that there seems to be a buggy interaction between ng-repeat and ng-init which prevents updates from being applied when a repeated variable is copied in ng-init. Here is a minimized example that shows the problem without using any recursion or includes or shadowing. https://jsfiddle.net/7sqk02m6/
<div ng-app="app" ng-controller="c">
<select ng-model="choice" ng-change="update()">
<option value="">Choose X or Y</option>
<option value="X">X</option>
<option value="Y">Y</option>
</select>
<div ng-repeat="(key, val) in data" ng-init="copy = val">
<span>{{key}}:</span> <span>val is {{val}}</span> <span>copy is {{copy}}</span>
</div>
</div>
The controller code just switches the data between "X" and "Y" and empty versions:
var app = angular.module('app', []);
app.controller('c', function($scope) {
$scope.choice = '';
$scope.update = function() {
$scope.data = {
X: { first: 'X1', second: 'X2' },
Y: { first: 'Y1', second: 'Y2' },
"": {}
}[$scope.choice];
};
$scope.update();
});
Notice that {{copy}} and {{val}} should behave the same inside the loop, because copy is just a copy of val. They are just strings like 'X1'. And indeed, the first time you select 'X', it works great - the copies are made, they follow the looping variable and change values through the loop. The val and the copy are the same.
first: val is X1 copy is X1
second: val is X2 copy is X2
But when you update to the 'Y' version of the data, the {{val}} variables update to the Y version but the {{copy}} values do not update: they stay as X versions.
first: val is Y1 copy is X1
second: val is Y2 copy is X2
Similarly, if you clear everything and start with 'Y', then update to 'X', the copies get stuck as the Y versions.
The upshot is: ng-init seems to fail to set up watchers correctly somehow when looped variables are copied in this situation. I could not follow Angular internals well enough to understand where the bug is. But avoiding ng-init solves the problem. A version of the original example that works well with no flicker is here: http://jsfiddle.net/cjtuyw5q/
If you want to control what keys are being tracked by ng-repeat you can use a trackby statement: https://docs.angularjs.org/api/ng/directive/ngRepeat
<div ng-repeat="model in collection track by model.id">
{{model.name}}
</div>
modifying other properties won't fire the refresh, which can be very positive for performance, or painful if you do a search/filter across all the properties of an object.

How to load more elements with AngularJS?

for a while I am trying to find how to make a load more(elements from an array) button,using Angular.
I have 9 elements in array, I use ng-repeat to loop them, and limitTo:3 to output first 3.
Questions:
1: is possible to make a load more button using only angular?(load more button is at bottom in example)
2: if not, how to make this work using jQuery?
http://plnkr.co/edit/1gHB9zr0lbEBwlCYJ3jQ
Thanks!
You don't need to think of jQuery, as you could solve this problem easily by using AngularJS itself.
You could maintain a variable inside your controller, name it as limit, then increment the limit variable inside loadMore() function.
Markup
<div ng-repeat="elem in travel.cruise | limitTo:travel.limit" class="cruises">
....COntent here...
</div>
Controller
app.controller('TravelController', function($scope) {
var vm = this;
vm.cruise = cruises;
vm.limit = 3;
$scope.loadMore = function() {
var increamented = vm.limit + 3;
vm.limit = incremented > vm.cruise.length ? vm.cruise.length : increamented;
};
});
Demo Plunkr
You can combine #Pankaj Parkar response with infiniteScroll so you dont need even the button.
No need of cracking any deep coding. Its simple to add just one line of code
<div ng-repeat="elem in travel.cruise | limitTo:travel.limit" class="cruises">
..NG Repeat..
</div>
Controller
$scope.loadMore = function() {
vm.limit = vm.limit + 3;
};
Assigning variable to increase by the 3 or more number will make your code work easily.
Simple and easy trick

angularjs change ng-repeat variable and call function

I am trying to toggle a class when clicking on the Save button but also call a function. I am using coffeescript. The function gets called but the variable never gets set to false.
div(ng-class="{'someclass':setListFocus}, ng-repeat="item in items")
a(ng-click="setListFocus=false;someFunction();")
span(class="gs-desktop") Save
// Delete user data.
a(ng-click="setListFocus=true")
span Edit
I assume that your small example is missing all kinds of data.
I would recommend either using controllerAs, for example:
<div ng-controller="myController as vm">
and then changing your bindings to vm.setListFocus.
Or another option would be to change your controller code like so:
function myController($scope) {
$scope.list = { focus: false };
}
And change your binding to list.focus instead. This will solve your scope inheritance problems.
You could begin someFunction() with toggling the value of setListFocus
$scope.someFunction = function(){
$scope.setListFocus = !$scope.setListFocus;
// ... rest of someFunction()
}
And then the angular template would be just this:
a(ng-click="someFunction()")
Another thing you could do is set up a toggle ListFocus function, that can take an optional callback parameter. This means you can toggle the listfocus between true or false, and optionally execute another function. I'm not going to continue answering in coffeescript because I'm not that familiar, and not everyone is.
$scope.toggleListFocus = function(andThen = false){
$scope.listFocus = !$scope.listFocus;
if(andThen==false) andThen();
}
//usage..
//toggle $scope.listFocus
enter code here
a(ng-click="toggleListFocus()")
//toggle then someFunction()
a(ng-click="toggleListFocus(someFunction())")

Where do you put this kind of controller code in an angular app?

The following code is needed in 2 different controllers (at the moment, maybe more controllers later). The code works around a problem I've found in ng-grid and allows the delayed selection of a row (once the data has been loaded).
// Watch for the ngGridEventData signal and select indexToSelect from the grid in question.
// eventCount parameter is a hack to hide a bug where we get ngGridEventData spam that will cause the grid to deselect the row we just selected
function selectOnGridReady(gridOptions, indexToSelect, eventCount) {
// Capture the grid id for the grid we want, and only react to that grid being updated.
var ngGridId = gridOptions.ngGrid.gridId;
var unWatchEvent = $scope.$on('ngGridEventData', function(evt, gridId) {
if(ngGridId === gridId) {
//gridEvents.push({evt: evt, gridId:gridId});
var grid = gridOptions.ngGrid;
gridOptions.selectItem(indexToSelect, true);
grid.$viewport.scrollTop(grid.rowMap[0] * grid.config.rowHeight);
if($scope[gridOptions.data] && $scope[gridOptions.data].length) {
eventCount -= 1;
if(eventCount <= 0) {
unWatchEvent(); // Our selection has been made, we no longer need to watch this grid
}
}
}
});
}
The problem I have is where do I put this common code? It's obviously UI code, so it doesn't seem like it belongs in a service, but there is no classical inheritance scheme (that I have been able to discover) that would allow me to put it in a "base class"
Ideally, this would be part of ng-grid, and wouldn't involve such a nasty hack, but ng-grid 2.0 is closed to features and ng-grid 3.0 is who knows how far out into the future.
A further wrinkle is the $scope that I guess I would have to inject into this code if I pull it from the current controller.
Does this really belong in a service?
I would probably just put this in a service and pass $scope into it but you do have other options. You may want to take a look at this presentation as it covers different ways of organizing your code: https://docs.google.com/presentation/d/1OgABsN24ZWN6Ugng-O8SjF7t0e3liQ9UN7hKdrCr0K8/present?pli=1&ueb=true#slide=id.p
Mixins
You could put it in its own object and mix it into any controllers using angular.extend();
var ngGridUtils = {
selectOnGridReady: function(gridOptions, indexToSelect, eventCount) {
...
}
};
var myCtrl = function() {...};
angular.extend(myCtrl, ngGridUtils);
Inheritance
If you use the 'controller as' syntax for your controllers then you can treat them like classes and just use javascript inheritance.
var BaseCtrl = function() {
...
}
BaseCtrl.prototype.selectOnGridReady = function(gridOptions, indexToSelect, eventCount) {
...
};
var MyCtrl = function() {
BaseCtrl.call(this);
};
MyCtrl.prototype = Object.create(BaseCtrl.prototype);
HTML:
<div ng-controller="MyCtrl as ctrl"></div>

AngularJs get count of element by class name

How can I count element by class name in angularJs?
I have tried with:
$scope.numItems = function() {
$window.document.getElementsByClassName("yellow").length;
};
Plunkr: http://plnkr.co/edit/ndCqpZaALfOEiYieepcn?p=preview
You have defined your function correctly, but made a mistake in showing its results: it should have been...
<p>{{numItems()}}</p>
... instead of plain {{numItems}}. You want to display the return value of the function, and not the function itself (that's meaningless), that's why you should follow the standard JS syntax for a function invocation.
Note that you can send arguments into this expression too: for example, I've rewritten that method like this:
$scope.numItems = function(className) {
return $window.document.getElementsByClassName(className).length;
};
... and then made three different counters in the template:
<p>Yellow: {{numItems('yellow')}}</p>
<p>Green: {{numItems('green')}}</p>
<p>Red: {{numItems('red')}}</p>
Plunker Demo.
But here's the real problem: numItems() result, used in one View, is based on DOM traversal - in other words, on another View. Not only that goes against Angular philosophy in general, it tends to break. In fact, it DOES break since this commit, as old as 1.3.0:
Now, even when the ngAnimate module is not used, if $rootScope is in
the midst of a digest, class manipulation is deferred. This helps
reduce jank in browsers such as IE11.
See, changes in classes are applied after digest - and that's after numItems() is evaluated, hence the delay in demo mentioned by #Thirumalaimurugan.
A quick-and-dirty solution is using another attribute for selector in numItems (in this plunker, it's data-color). But I would strongly advise you against it. The proper approach would be adding the data rendered by numItems() -using component into the model. For example:
app.js
// ...
var colorScheme = {
'toggle': {true: 'yellow', false: 'red'},
'toggle2': {true: 'green', false: 'red'},
'toggle3': {true: 'green', false: 'red'},
'toggle4': {true: 'red', false: 'green'}
};
$scope.getColor = function getColor(param) {
return colorScheme[param][$scope[param]];
};
$scope.countColor = function(color) {
return Object.keys(colorScheme).filter(function(key) {
return colorScheme[key][$scope[key]] === color;
}).length;
};
index.html
<p ng-class="getColor('toggle')">{{name}}</p>
<!-- ... -->
<p ng-repeat="color in ['Yellow', 'Green', 'Red']"
ng-bind="color + ' ' + countColor(color.toLowerCase())">
Demo.

Resources