I am trying to implement the directive that is consuming the data from the service and reacts accordingly. However, something is going wrong and I need some assistance.
Here is the sample of the code that also can be found at http://jsfiddle.net/3c9h7/5/
var app = angular.module('myApp', []);
app.service('MyService', [
'$rootScope', function($rootScope) {
this.value = 4;
var self = this;
function inc(){
self.value += 3;
}
setInterval(inc, 1000);
}
]);
app.directive('myDir', ['MyService', function(MyService){
return {
link : function(scope, element){
function expr(){
return MyService.value;
}
function react(){
element.html(MyService.value);
}
scope.$watch(expr, react);
react();
}
}
}]);
<div ng-app='myApp'my-dir>
</div>
As the result Div element displaying the initial value of MyService.value but is ignoring the updates that happens in the service every second.
I have found the solution which involves rootScope(i.e. uncomment lines 7 and 9 in the jsFiddle sample):
function inc(){
$rootScope.$apply(function(){
self.value += 3;
});
}
setInterval(inc, 1000);
However, it does seem to be right to me..All the samples I found are not using this trick..So, am I missing something? Is "rootScope" solution appropriate? Maybe there is a better way to achieve the goal?
Thanks!
Use Angular's $interval (ref) instead of window.setInterval().
What it does though is actually calling apply() under the hoods, so your solution is correct, just a bit more complex. Also $interval can be mocked for testing.
Related
hi all i am using angulrajs passing one value from one controller to another controller using service it's work fine but my need is when service value change in controller 2 i get the service value in one scope when scope value change i need trigger the function it's called refresh function when service value change and that i need to call the refresh function here my fiddle
https://jsfiddle.net/ctawL4t3/10/
You can just $watch your value.storeObject. Though it's not best of the practices, but it suits this kind of feature.
$scope.$watch('value.storedObject', function(newVal) {
if(newVal !== '') {
refresh()
}
})
working fiddle (open console to see refresh function logging)
You can try to use angular default $emit, $broadcast, or try to do 2 simple functions in own service
angular.module('app').factory('StoreService', function() {
var listeners = {};
var emit = function(name, val) {
if(listeners[name]) {
listeners[name](val)
}
}
var on = function(name, callback) {
listeners[name] = callback;
}
return {
emit: emit,
on: on,
storedObject: ''
};
});
JSFiddle example
JSFiddle example $watch
JSFiddle example ng-change is better because, you can use easily debounce
you can use broadcast function for that
Please check this SO link to find the related answer
How to call a function from another controller in angularjs?
app.controller('One', ['$scope', '$rootScope'
function($scope) {
$rootScope.$on("CallParentMethod", function(){
$scope.parentmethod();
});
$scope.parentmethod = function() {
// task
}
}
]);
app.controller('two', ['$scope', '$rootScope'
function($scope) {
$scope.childmethod = function() {
$rootScope.$emit("CallParentMethod", {});
}
}
]);
Angular newbie here.
I have the following div:
<div id="mainfr" data-curpos="dy[ {{curPosObj.dy}} ]" ...>blah blah </div>
And in my controller I have:
var nxtest = angular.module('nxtest', []);
var appController = nxtest.controller('AppCtrl', ['$scope', function ($scope) {
$scope.curPosObj = { dir: "down", dy:5 };
$scope.clr = window.setTimeout(function(){ $scope.curPosObj.dy = 777;
window.clearTimeout($scope.clr); }, 5000); //see if the attr responds to a random change
}])
In firebug, inspecting the scope object shows that it is indeed modified. I want to understand why the bound attribute {{curPosObj.dy}} is not 'bound' and the view does not respond to the changing values? Thanks very much in advance.
Update: added link to plunker as suggested - the red text never changes:
http://plnkr.co/edit/HJxEpgR8VepxuT47zJDJ?p=preview
Update 2: OK so there may be a separate issue here - the red text is in a pseudo element whose contrent attribute depends on the main divs attribute... and I'm not calling setAttribute anywhere... but regardless: in firebug, the 'data-curpos' attribute itself is NOT updating, never mind the pseudo elem that depends on it...
That's because angular doesn't tracking scope changes out of the dygest cycle and window.setTimeout that case. You should use the $timeout service instead of window.setTimeout or put code which chenge scope into $scope.$apply call
angularjs API reference - $timeout service
angularjs API reference - scope guide
try this:
var nxtest = angular.module('nxtest', []);
var appController = nxtest.controller('AppCtrl', ['$scope', '$timeout',
function($scope, $timeout) {
$scope.curPosObj = {
dir: "down",
dy: 5
};
$scope.clrPromise = $timeout(function() {
$scope.curPosObj.dy = 777;
}, 5000); //see if the attr responds to a random change
}
])
Not able to figure out what the bug in this code is.I've tried to only post the relevant parts of the code here.
Controller
myApp.controller('MessageCtrl', function ($scope, notificationService, $rootScope) {
$scope.notificationService = notificationService;
$scope.msgCount = 0;
$scope.notificationService.subscribe({channel : 'my_channel'});
$rootScope.$on('pubnub:msg',function(event,message){
$scope.msgCount = $scope.msgCount + 1;
//$scope.$digest();
});
});
My Notification Angular Service
myApp.factory('notificationService',['$rootScope', function($rootScope) {
var pubnub = PUBNUB.init({
publish_key : '..',
subscribe_key : '..'
});
var notificationService = {
subscribe : function(subscription) {
pubnub.subscribe({
channel : subscription.channel,
message : function(m){
$rootScope.$broadcast('pubnub:msg', m);
}
});
}
};
return notificationService;
}]);
And the template :
<div>
Count = {{msgCount}}
</div>
The problem :
Using console logs & using karma tests I have confirmed that the $rootScope.$on method in MessageCtrl is getting called when I do a $broadcast from Notification Service. And that the msgCount variable is getting incremented. However, I don't see the updated value being reflected in the template without running a $scope.$digest() . I am pretty sure I shouldn't be needing to have to call $scope.$digest , ie Angular should be providing me this binding.
Interestingly, when I tried a $rootScope.$broadcast from another controller, the msgCount in the template got incremented without having to call $scope.$digest().
Can anyone kindly help me here. Thank you.
Update
Thanks to Peter and looking at the google group discussion, wrapping the $broadcast in an $apply did the trick.
$rootScope.$apply(function(){
$rootScope.$broadcast('pubnub:question', m);
});
It seems that your $broadcast happens outside AngularJS and you need to notify your app about it with calling $apply(), but better do it in the notificationService.
As for $broadcast and $on trigger a apply/digest you can read in this post. Brief overview of AngularJs source files make me sure that $broadcast does not auto-apply changes (look here ). $broadcast just calling listeners and nothing else.
Please, take a look at this simple example on jsFiddle .
The template
<div ng-controller="myCtrl">
<p>Count: {{ count }}</p>
<button ng-click="fireEvent()">Fire Event</button>
</div>
The controller
angular.module("app", [])
.controller('myCtrl', function($scope, $rootScope, notificationService) {
$scope.count = 0;
notificationService.subscribe();
$rootScope.$on('event', function() {
console.log("event listener");
$scope.count++;
});
$scope.fireEvent = function() {
// it is ok here due to ngClick directve
$rootScope.$broadcast('event', true);
};
})
And factory
.factory('notificationService',['$rootScope', function($rootScope) {
var notificationService = {
subscribe : function() {
setInterval(function(){
console.log("some event happend and broadcasted");
$rootScope.$broadcast('event', true);
// angular does not know about this
//$rootScope.$apply();
}, 5000);
}
};
return notificationService;
}]);
Of course in both cases you will see that event listener fires, but ngClick fires $digest and your notificationService does not.
Also you can get some info about sources that will start the digest cicle in this nice answer https://stackoverflow.com/a/12491335/1274503
I am looking for a way to execute code when after I add changes to a $scope variable, in this case $scope.results. I need to do this in order to call some legacy code that requires the items to be in the DOM before it can execute.
My real code is triggering an AJAX call, and updating a scope variable in order to update the ui. So I currently my code is executing immediately after I push to the scope, but the legacy code is failing because the dom elements are not available yet.
I could add an ugly delay with setTimeout(), but that doesn't guarantee that the DOM is truly ready.
My question is, is there any ways I can bind to a "rendered" like event?
var myApp = angular.module('myApp', []);
myApp.controller("myController", ['$scope', function($scope){
var resultsToLoad = [{id: 1, name: "one"},{id: 2, name: "two"},{id: 3, name: "three"}];
$scope.results = [];
$scope.loadResults = function(){
for(var i=0; i < resultsToLoad.length; i++){
$scope.results.push(resultsToLoad[i]);
}
}
function doneAddingToDom(){
// do something awesome like trigger a service call to log
}
}]);
angular.bootstrap(document, ['myApp']);
Link to simulated code: http://jsfiddle.net/acolchado/BhApF/5/
Thanks in Advance!
The $evalAsync queue is used to schedule work which needs to occur outside of current stack frame, but before the browser's view render. -- http://docs.angularjs.org/guide/concepts#runtime
Okay, so what's a "stack frame"? A Github comment reveals more:
if you enqueue from a controller then it will be before, but if you enqueue from directive then it will be after. -- https://github.com/angular/angular.js/issues/734#issuecomment-3675158
Above, Misko is discussing when code that is queued for execution by $evalAsync is run, in relation to when the DOM is updated by Angular. I suggest reading the two Github comments before as well, to get the full context.
So if code is queued using $evalAsync from a directive, it should run after the DOM has been manipulated by Angular, but before the browser renders. If you need to run something after the browser renders, or after a controller updates a model, use $timeout(..., 0);
See also https://stackoverflow.com/a/13619324/215945, which also has an example fiddle that uses $evalAsync().
I forked your fiddle.
http://jsfiddle.net/xGCmp/7/
I added a directive called emit-when. It takes two parameters. The event to be emitted and the condition that has to be met for the event to be emitted. This works because when the link function is executed in the directive, we know that the element has been rendered in the DOM. My solution is to emit an event when the last item in the ng-repeat has been rendered.
If we had an all Angular solution, I would not recommend doing this. It is kind of hacky. But, it might be an okey solution for handling the type of legacy code that you mention.
var myApp = angular.module('myApp', []);
myApp.controller("myController", ['$scope', function($scope){
var resultsToLoad = [
{id: 1, name: "one"},
{id: 2, name: "two"},
{id: 3, name: "three"}
];
function doneAddingToDom() {
console.log(document.getElementById('renderedList').children.length);
}
$scope.results = [];
$scope.loadResults = function(){
$scope.results = resultsToLoad;
// If run doneAddingToDom here, we will find 0 list elements in the DOM. Check console.
doneAddingToDom();
}
// If we run on doneAddingToDom here, we will find 3 list elements in the DOM.
$scope.$on('allRendered', doneAddingToDom);
}]);
myApp.directive("emitWhen", function(){
return {
restrict: 'A',
link: function(scope, element, attrs) {
var params = scope.$eval(attrs.emitWhen),
event = params.event,
condition = params.condition;
if(condition){
scope.$emit(event);
}
}
}
});
angular.bootstrap(document, ['myApp']);
Using timeout is not the correct way to do this. Use a directive to add/manipulate the DOM. If you do use timeout make sure to use $timeout which is hooked into Angular (for example returns a promise).
If you're like me, you'll notice that in many instances $timeout with a wait of 0 runs well before the DOM is truly stable and completely static. When I want the DOM to be stable, I want it to be stable gosh dang it. And so the solution I've come across is to set a watcher on the element (or as in the example below the entire document), for the "DOMSubtreeModified" event. Once I've waited 500 milliseconds and there have been no DOM changes, I broadcast an event like "domRendered".
IE:
//todo: Inject $rootScope and $window,
//Every call to $window.setTimeout will use this function
var broadcast = function () {};
if (document.addEventListener) {
document.addEventListener("DOMSubtreeModified", function (e) {
//If less than 500 milliseconds have passed, the previous broadcast will be cleared.
clearTimeout(broadcast)
broadcast = $window.setTimeout(function () {
//This will only fire after 500 ms have passed with no changes
$rootScope.$broadcast('domRendered')
}, 500)
});
//IE stupidity
} else {
document.attachEvent("DOMSubtreeModified", function (e) {
clearTimeout(broadcast)
broadcast = $window.setTimeout(function () {
$rootScope.$broadcast('domRendered')
}, 500)
});
}
This event can be hooked into, like all broadcasts, like so:
$rootScope.$on("domRendered", function(){
//do something
})
I had a custom directive and I needed the resulting height() property of the element inside my directive which meant I needed to read it after angular had run the entire $digest and the browser had flowed out the layout.
In the link function of my directive;
This didn't work reliably, not nearly late enough;
scope.$watch(function() {});
This was still not quite late enough;
scope.$evalAsync(function() {});
The following seemed to work (even with 0ms on Chrome) where curiously even ẁindow.setTimeout() with scope.$apply() did not;
$timeout(function() {}, 0);
Flicker was a concern though, so in the end I resorted to using requestAnimationFrame() with fallback to $timeout inside my directive (with appropriate vendor prefixes as appropriate). Simplified, this essentially looks like;
scope.$watch("someBoundPropertyIexpectWillAlterLayout", function(n,o) {
$window.requestAnimationFrame(function() {
scope.$apply(function() {
scope.height = element.height(); // OK, this seems to be accurate for the layout
});
});
});
Then of course I can just use a;
scope.$watch("height", function() {
// Adjust view model based on new layout metrics
});
interval works for me,for example:
interval = $interval(function() {
if ($("#target").children().length === 0) {
return;
}
doSomething();
$interval.cancel(interval);
}, 0);
I recently chose AngularJS over ember.js for a project I am working on, and have been very pleased with it so far. One nice thing about ember is its built in support for "computed properties" with automatic data binding. I have been able to accomplish something similar in Angular with the code below, but am not sure if it is the best way to do so.
// Controller
angular.module('mathSkills.controller', [])
.controller('nav', ['navigation', '$scope', function (navigation, $scope) {
// "Computed Property"
$scope.$watch(navigation.getCurrentPageNumber, function(newVal, oldVal, scope) {
scope.currentPageNumber = newVal;
});
$scope.totalPages = navigation.getTotalPages();
}]);
// 'navigation' service
angular.module('mathSkills.services', [])
.factory('navigation', function() {
var currentPage = 0,
pages = [];
return {
getCurrentPageNumber: function() {
return currentPage + 1;
},
getTotalPages: function() {
return pages.length;
}
};
});
// HTML template
<div id=problemPager ng-controller=nav>
Problem {{currentPageNumber}} of {{totalPages}}
</div>
I would like for the UI to update whenever the currentPage of the navigation service changes, which the above code accomplishes.
Is this the best way to solve this problem in AngularJS? Are there (significant) performance implications for using $watch() like this? Would something like this be better accomplished using custom events and $emit() or $broadcast()?
While your self-answer works, it doesn't actually implement computed properties. You simply solved the problem by calling a function in your binding to force the binding to be greedy. I'm not 100% sure it'd work in all cases, and the greediness might have unwanted performance characteristics in some situations.
I worked up a solution for a computed properties w/dependencies similar to what EmberJS has:
function ngCreateComputedProperty($scope, computedPropertyName, dependentProperties, f) {
function assignF($scope) {
var computedVal = f($scope);
$scope[computedPropertyName] = computedVal;
};
$scope.$watchCollection(dependentProperties, function(newVal, oldVal, $scope) {
assignF($scope);
});
assignF($scope);
};
// in some controller...
ngCreateComputedProperty($scope, 'aSquared', 'a', function($scope) { return $scope.a * $scope.a } );
ngCreateComputedProperty($scope, 'aPlusB', '[a,b]', function($scope) { return $scope.a + $scope.b } );
See it live: http://jsfiddle.net/apinstein/2kR2c/3/
It's worth noting that $scope.$watchCollection is efficient -- I verified that "assignF()" is called only once even if multiple dependencies are changed simultaneously (same $apply cycle).
"
I think I found the answer. This example can be dramatically simplified to:
// Controller
angular.module('mathSkills.controller', [])
.controller('nav', ['navigation', '$scope', function (navigation, $scope) {
// Property is now just a reference to the service's function.
$scope.currentPageNumber = navigation.getCurrentPageNumber;
$scope.totalPages = navigation.getTotalPages();
}]);
// HTML template
// Notice the first binding is to the result of a function call.
<div id=problemPager ng-controller=nav>
Problem {{currentPageNumber()}} of {{totalPages}}
</div>
Note that with ECMAScript 5 you can now also do something like this:
// Controller
angular.module('mathSkills.controller', [])
.controller('nav', function(navigation, $scope) {
$scope.totalPages = navigation.getTotalPages();
Object.defineProperty($scope, 'currentPageNumber', {
get: function() {
return navigation.getCurrentPageNumber();
}
});
]);
//HTML
<div ng-controller="nav">Problem {{currentPageNumber}} of {{totalPages}}</div>