Trigger the digest cycle in callback events - angularjs

I want to create a corresponding textarea along with a handstontable, such that modifying the table has impact to the text, and vice-versa. Here is a JSBin.
<!DOCTYPE html>
<html>
<head>
<script src="https://handsontable.github.io/ngHandsontable/node_modules/angular/angular.js"></script>
<script src="https://docs.handsontable.com/pro/1.8.2/bower_components/handsontable-pro/dist/handsontable.full.js"></script>
<link type="text/css" rel="stylesheet" href="https://docs.handsontable.com/pro/1.8.2/bower_components/handsontable-pro/dist/handsontable.full.min.css">
<script src="https://handsontable.github.io/ngHandsontable/dist/ngHandsontable.js"></script>
</head>
<body ng-app="app">
<div ng-controller="Ctrl">
<hot-table settings="settings" on-after-create-row="onAfterCreateRow" datarows="dataJson"></hot-table>
<br><br>
<textarea cols=40 rows=20 ng-model="dataString"></textarea>
</div>
<script>
var app = angular.module('app', ['ngHandsontable']);
app.controller('Ctrl', ['$scope', '$filter', 'hotRegisterer', function ($scope, $filter, hotRegisterer) {
$scope.dataJson = [[5, 6], [7, 8]];
$scope.onAfterCreateRow = function (index, amount) {
$scope.$digest();
};
$scope.$watch('dataJson', function (dataJson_new) {
$scope.dataString = $filter('json')(dataJson_new);
}, true);
$scope.$watch('dataString', function (dataString_new) {
try {
$scope.dataJson = JSON.parse(dataString_new);
} catch (e) {
}
}, true);
$scope.settings = {
contextMenu: true,
contextMenuCopyPaste: {
swfPath: 'zeroclipboard/dist/ZeroClipboard.swf'
}
};
}]);
</script>
</body>
</html>
However, one thing I realise is that, adding/deleting rows/columns will NOT fire the watcher of dataJSON (whereas modifying a cell value will do). So I have to use $scope.$digest() in the callbacks such as onAfterCreateRow to reflect the change of adding rows. But it raises Uncaught TypeError: Cannot set property 'forceFullRender' of null:
Having $scope.$digest() in other callbacks (i.e., onAfterCreateCol, onAfterRemoveRow and onAfterRemoveCol) will raise the same error. I think it is a serious problem, if we cannot well trigger the digest cycle in these callback events. Does anyone know how to solve this or have any workaround?

The $timeout was a workaround in some older versions of angular currently, you can use $applyAsync that was created to replace the $timeout as workaround and work for the cases where you would get an error $digest already in progress

One possible way to fix this problem is to use $timeout service which implements so called . See here updated JSBin. Also, to see explanation of how $timeout works read the answer Here. Hope this will help.

Related

Pass value from one controller to another in angular js1

I am using $rootScope for passing value from one controller to another but i did not get the value in second controller(cart).
x.controller("first",function($scope,$http,$rootScope){
$scope.cart1=function(x,y){
$rootScope.img=x;
$rootScope.login=y;
}
});
x.controller("cart",function($scope,$rootScope){
$scope.cart2=$rootScope.img;
console.log($scope.cart2)
});
$rootScope does not get the value assigned if you simply define in controller, you need to transfer the data from controller1 to controller2 based on some event
DEMO
var x =angular.module('app', [])
x.controller("first",function($scope,$rootScope){
$scope.cart1=function(x,y){
console.log(x);
$rootScope.img=x;
$rootScope.login=y;
}
});
x.controller("cart",function($scope,$rootScope){
$scope.getData=function(){
$scope.cart2=$rootScope.img;
console.log($scope.cart2)
};
});
<!DOCTYPE html>
<html ng-app="app">
<head>
<script data-require="angular.js#1.4.3" data-semver="1.4.3" src="https://code.angularjs.org/1.4.3/angular.js"></script>
<link rel="stylesheet" href="style.css" />
<script src="script.js"></script>
</head>
<body >
<div ng-controller="first">
<button ng-click="cart1('test','test2')">transfer</button>
</div>
<div ng-controller="cart">
<button ng-click="getData()">getStoredValue</button>
{{cart2}}
</div>
</body>
</html>
NOTE
Using $rootScope is not a recommended way to transfer variable across controllers, instead try using services.
why not using $broadcast, considering your controller structure $scope.$parent is enough, in oter case inject $rootScope from where you are firing the $broadcast
var x =angular.module('app', [])
x.controller("first",function($scope){
$scope.cart1=function(x,y){
$scope.$parent.$broadcast('value changed', {x: X, y: y});
}
});
x.controller("cart",function($scope){
$scope.$on('value changed', function(data){
$scope.login = x;
$scope.cart2 = y;
});
});
Now your values are set in scope, use it whenever needed (in case of binding it will automatically reflect in UI)
(function() {
"use strict";
angular
.module('app')
.factory("cartFactory", cartFactory);
function cartFactory($http) {
return {
login: '',
cart2:''
};
}})();
This is your factory
you can use multiple controller like this...
var x =angular.module('app', [])
x.controller("first",function($scope,cartFactory){
cartFactory.login=x;
cartFactory.cartFactory=y;
}
});
x.controller("cart",function($scope,cartFactory){
$scope.login = cartFactory.login;
$scope.cart2 = cartFactory.cart2;
});
This is very simple to pass data one controller to another multiple controller.

AngularJS - two way binding not working using service

I am learning Angular using W3Schools.
I just modified an example about "Services"... The following is the code:
<!DOCTYPE html>
<html>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>
<body>
<div ng-app="myApp" ng-controller="myCtrl">
<p><input type="text" ng-model="num"></p>
<h2>{{num}}</h2>
<h1>{{hex}}</h1>
</div>
<p>A custom service whith a method that converts a given number into a hexadecimal number.</p>
<script>
var app = angular.module('myApp', []);
app.service('hexafy', function() {
this.myFunc = function (x) {
return x.toString(16);
}
});
app.controller('myCtrl', function($scope, hexafy) {
$scope.num = 200;
$scope.hex = hexafy.myFunc($scope.num);
});
</script>
</body>
</html>
When I update the textbox, the "HEX" part is not updating. Why?
Your hexafy.myFunc is called only once when the controller is initialized, hence only the initial value is converted to hex. If you want a function to be called on the value change of a scope variable in runtime, you need filters. AngularJS has a lot of inbuilt filters that are ready to use.
You can also define a custom filter, just like you define services or controllers.
<!DOCTYPE html>
<html>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>
<body>
<div ng-app="myApp" ng-controller="myCtrl">
<p><input type="text" ng-model="num"></p>
<h2>{{num}}</h2>
<h1>{{num | hexafy}}</h1>
</div>
<p>A custom filter that converts a given number into a hexadecimal number.</p>
<script>
var app = angular.module('myApp', []);
app.filter('hexafy', function() {
return function (x) {
return Number(x).toString(16); // Also convert the string to number before calling toString.
}
});
app.controller('myCtrl', function($scope) {
$scope.num = 200;
//$scope.hex = hexafy.myFunc($scope.num);
});
</script>
</body>
</html>
Further reading: AngularJS Filters
NOTE: A filter is a good idea if you're gonna be using the hexafy functionality at multiple places in/across views. Otherwise, it is just an overkill and the $scope.$watch method will work fine for you, as described in other answers
$scope.hex is not updating because there're no way for it update itself.
The hexafy.myFunc is called only once when the controller is loaded for the first time.
If you want want the $scope.hex property to change with num, you might need a watch on the num property.
$scope.$watch('num', function(newVal, oldVal) {
$scope.hex = hexafy.myFunc($scope.num); /// or newVal
}
The function passed in $scope.$watch will be called everytime the value of $scope.num changes.
for more info see https://docs.angularjs.org/api/ng/type/$rootScope.Scope (the watch section)
Hope it helps.
No need of a service here, you can simple use $watch on the num. See below code snippet, it will update your ui, I have updated your controller code, please check.
app.controller('myCtrl', function($scope, hexafy) {
$scope.num = 200;
$scope.hex = "some default val";
$scope.$watch('num', function(newValue, oldValue) {
$scope.hex = newValue.toString();
});
});
Your Text box is only bind to 'num'. '$scope.hex is not binded to your text box'. So that it is not update when you typing text. You could use '$watch' on 'num'. Read here
app.controller('myCtrl', function($scope, hexafy) {
$scope.num = 200;
$scope.$watch('num', function() {
$scope.hex = hexafy.myFunc(parseInt($scope.num));
});
});

Angular $animate misunderstanding

There's something I'm missing in AngularJS $animate. I have no idea if my error is trivial or a massive misunderstanding. I tried to cut this down to the minimum example but it's still quite large. At least, however, it should run out of the box.
<!doctype html>
<html data-ng-app="MyModule">
<head>
<style type="text/css">
.msg.ng-leave {
transition: all 1s;
opacity: 1;
}
.msg.ng-leave.ng-leave-active {
opacity: 0;
}
.fade.ng-enter {
transition: opacity 1s;
opacity: 0;
}
.fade.ng-enter.ng-enter-active {
opacity: 1;
}
</style>
</head>
<body data-ng-controller="MyController">
<div class="navpanel">
<p class="msg"><strong>This is a message, make it go away</strong></p>
<h3>Menu</h3>
<ul id="menulist">
<li>Do This</li>
<li>Do That</li>
</ul>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular-animate.min.js"></script>
<script>
angular.module('MyModule', ['ngAnimate'])
.controller('MyController', ['$scope', '$animate', function($scope, $animate) {
let npanel = angular.element(document.querySelector('.navpanel'));
let msg = angular.element(document.querySelector('.msg'));
let menulist = angular.element(document.getElementById('menulist'));
let counter = 0;
npanel.on('click', function() {
$animate.leave(msg);
let newpara = angular.element(document.createElement('p'));
newpara.html('new paragraph ' + (++counter)).addClass('fade');
let children = npanel.children();
$animate.enter(angular.element(newpara), npanel, menulist);
});
}]);
</script>
</body>
</html>
It's intended to do two main things on clicking. One, it should remove the message, two, it should insert a new paragraph. It does the second (using $animate.enter) but the first fails completely (using $animate.leave).
$animate.leave seems, from the documentation, to be supposed to run through any animation, then remove the item from the DOM, but my code does none of that. The $animate.enter by contrast, does not succeed in performing any animations, but does manage to insert the new item (which seems to imply that $animate has been properly injected).
What am I missing?
on is a jqLite/jQuery method and will not trigger AngularJS' digest cycle. This means that when you click on the element the attached event listener will execute, the DOM operations will be performed, but since the digest cycle doesn't start AngularJS' animation hooks will not handle the animation.
You can use $apply:
$apply() is used to execute an expression in angular from outside of
the angular framework. (For example from browser DOM events,
setTimeout, XHR or third party libraries). Because we are calling into
the angular framework we need to perform proper scope life cycle of
exception handling, executing watches.
For example:
npanel.on('click', function() {
$scope.$apply(function() {
$animate.leave(msg);
let newpara = angular.element(document.createElement('p'));
newpara.html('new paragraph ' + (++counter)).addClass('fade');
let children = npanel.children();
$animate.enter(angular.element(newpara), npanel, menulist);
});
});

Finished Loading of ng-include in angular js

What is the best way to detect the end of html loading by ng–include? I want to write some code that runs when it has finished loading.
There are two ways to detect when ng-include finished loading, depending on your need:
1) via onload attribute - for inline expressions. Ex:
<div ng-include="'template.html'" onload="loaded = true"></div>
2) via an event $includeContentLoaded that ng-include emits - for app-wide handling. Ex:
app.run(function($rootScope){
$rootScope.$on("$includeContentLoaded", function(event, templateName){
//...
});
});
when it finishes loading the content
you can use onload for it as below
<div ng-include=".." onload="finishLoading()"></div>
in controller,
$scope.finishLoading = function() {
}
after loading the ng-include finishLoading scope function will call.
here is the working Demo Plunker
You can use this code:
$scope.$on('$includeContentLoaded', function () {
// it has loaded!
});
Here's a complete example that will catch all the ng-include load events emitted by the application:
<html ng-app="myApp">
<head>
<script src="angular.min.js"></script>
</head>
<body ng-controller="myController">
<div ng-include="'snippet.html'"></div>
<script>
var myApp = angular.module("myApp",[]);
myApp.controller('myController', function($scope) {
$scope.$on('$includeContentLoaded', function(event, target){
console.log(event); //this $includeContentLoaded event object
console.log(target); //path to the included resource, 'snippet.html' in this case
});
});
</script>
</body>
</html>
A problem I had, and the reason I take the time to post this, is I failed to include both the ng-app and ng-controller in my HTML markup.
If I have to wait for the element to be present, I wrap a $timeout with $includeContentLoaded:
var selector = '#foo';
$rootScope.$on('$includeContentLoaded', function(event, templateName){
$timeout(() => {
const $el = angular.element(selector);
if ($el.length) {
// Do stuff with element.
}
});
});
The timeout gives it time load properly, specially if the ng-include contains a directive that takes a few milliseconds to render.
There is an alternative for that only using JS call stack tricks. put ng-init="eventName" on the desired element. After that, declare the event on the angular controller:
$scope.eventName = () => {
setTimeout(() => { alert('loaded!'); }, 0);
}
That makes the alert only pop up when everything about angular is loaded, that occur because of the call-stack of JavaScript that considers some codes as Microtasks and some others as Macrotasks and they have priorities like the Microtasks run always first and just after all Microtasks run, the Macrotasks can take the place on the call-stack.
And, setTimeout() is considered a Macrotask for JavaScript, so it will only run as the latest tasks.

Why doesn't ng-hide/show work if expression is a scope variable modified on broadcast event

I have the following code:
script.js:
var app = angular.module('TestApp',[]);
app.factory('testfactory', function($rootScope, $window){
var factory = {};
factory.auth = function() {
//This is only to make call to auth() async.
//Actual code is using google-api call.
setTimeout(auth, 2000);
}
$window.auth = function(){
$rootScope.$broadcast('loggedin');
}
return factory;
});
app.controller('MainCtrl', function($scope, testfactory) {
$scope.status = {
loggedIn: false
}
$scope.test = testfactory;
$scope.$on('loggedin', function(){
$scope.status.loggedIn = true;
alert('loggedIn');
});
});
index.html:
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="style.css" />
</head>
<body ng-app="TestApp">
<div ng-controller="MainCtrl">
<button ng-click="test.auth()" ng-hide="status.loggedIn">Auth</button>
</div>
<script data-require="angular.js#1.2.19" data-semver="1.2.19" src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.19/angular.min.js"></script>
<script src="script.js"></script>
</body>
</html>
This should hide the button "Auth" on clicking, however it does not. It works only when its clicked the second time. What's happening here? I'm modifying the scope variable inside angular's broadcasted event, so binding should work. What am I missing here?
Edit: I know wrapping the code within $scope.$apply works, but my question is why isn't it happening automatically, since I'm not modifying the variable from outside the scope.
Here's a plunker for this code - http://plnkr.co/edit/Ov568VDWCKarFHQjgbvG
Answer: This discussion on google groups says $broadcast doesn't trigger auto-apply. So, if $broadcast is called from outside of angular-world, $apply must be applied manually.
Because Angular use $digest (documentation, and why/where to use) to keep the binding between the $scope and the interface. Try to force the $digest:
$scope.$on('loggedin', function(){
$scope.status.loggedIn = true;
$scope.digest();
alert('loggedIn');
});
or
$scope.$on('loggedin', function(){
$scope.apply(function(){
$scope.status.loggedIn = true;
});
alert('loggedIn');
});
Edit:
why isn't it happening automatically ?
The setTimeout function runs outside the angular scope, therefore angular has no idea that you might change something.
You could also solve the problem with the #pixelbits solution. The $timeout service is just a wrapper around javascript's setTimeout witch executes within the angular scope.
http://plnkr.co/edit/o3XtMLpCquHcU2j2z4v9?p=preview
please add $scope.$apply();
app.controller('MainCtrl', function($scope, testfactory) {
$scope.status = {
loggedIn: false
}
$scope.test = testfactory;
$scope.$on('loggedin', function()
{
$scope.status.loggedIn = true;
$scope.$apply();
alert('loggedIn');
});
});
Use $timeout instead of setTimeout. $timeout will trigger a $digest cycle, but setTimeout will not:
$timeout(auth,2000);

Resources