AngularJS watching an expression - angularjs

Say we have this expression
$scope.showButton = users.is_admin() && !document.is_provided;
And then in the same controller you have a button that updates the value of Document.is_provided:
<button ng-click="document.is_provided = true;">Provide document</button>
The problem is that $scope.showButton should now changed but it's not changing.
Updated:
Plnkr showing simplified issue: http://plnkr.co/edit/Qk2jnHAmqsrNXSAjDYEk?p=preview

I'd like to add on to squiroid's answer and explain to you why it works. I'll provide a very simple way for you to understand, as this was a very common problem for me too when I started.
Angular has something called a Digest cycle. It is a series of functions that are called successively to make sure that if a variable A is two-way bound to another variable B (using ng-model for example), they always stay in sync (that is, always have the same value). Functions that angular provides (or services, that usually start with $, like $timeout etc), automatically start the digest cycle
So let's say that you have a variable A and you've bound it to the $scope variable B, which is equal to a variable C. You would expect C to be bound to A too, right? Because you think when you change C, B should change, and so, A should change too. But that is only possible when you start the digest cycle. The cycle will look for variables that need to be updated, and will update them. However, by simply changing the value of C, you will never trigger the digest cycle, and so your change will never propagate to A.
What you do when you put a variable in $watch is (as the $ suggests), you forcefully start a digest cycle. So now when you change C, you start the cycle, it tracks down that it needs to update A too, and that's why it works.
In your example, $scope.showButton is bound to document.is_provided. Now you're using a non-angular function to change the value of document.is_provided. This means that you do not trigger the digest cycle, and that means your change will never be propagated to $scope.showButton. Therefore, showButton always remains false.
And of course, an example to help you understand:
http://jsbin.com/vojoso/edit?js,console,output
Watch the console. I've added logs to help you understand.
Comment out the $watch statement and see what happens.
Try understanding it and then doing it yourself, you'll get it.
Hope I helped :)

I am not sure but you can watch on it:-
$scope.$watch(function(){
return Users.is_admin() && !Document.is_provided;
},fuction(newVal){
$scope.showButton =newVal;
});
Hope it helps :)

Here is good article about $scope.$watch, it should help to understand how to solve your problem.
It you need bigger answer add code which will explain Document variable, and your REST service.
UPD: I see you changed you original question. I suppose you have trouble with controller. Try do not use $scope implicitly, use data attribute as sad angular code style
Also show the part in you template where you connect controller.
UPD 2: You have some misunderstanding of my words, so I modified your plunker example
index.html
<html ng-app="plunker">
<body ng-controller="MainController as main">
<div ng-show="main.data.document.is_provided">visible</div>
<button ng-click="main.hide()">Hide</button>
<script data-require="angular.js#1.4.x"
src="https://code.angularjs.org/1.4.1/angular.js"
data-semver="1.4.1"></script>
<script src="app.js"></script>
</body>
</html>
app.js
angular
.module('plunker', [])
.controller('MainController', MainController);
function MainController() {
var main = this;
this.data = {
document: {
is_provided: true
}
};
this.hide = hide;
function hide() {
main.data.document.is_provided = false;
}
}
You have one problem inside ng-click, as there should be function execution. Also you are using ng-if instead of ng-show.

The best way (imho) to deal with this kind of problems:
Use {{ anyObject | json }} in your view!
This way you know if the reference you are using and the object exist in your scope. using | json gives you accolades {} when your object exists but when it is empty. If your object isn't available in your scope noting is 'printed'.
In your case, change the body tag to:
<body ng-controller="MainCtrl as mainCtrl">
Then, inside the html of this controller add:
DATA: {{mainCtrl | json}}
So now you see your variables! And you also see clicking on the button doesn't change the boolean variable.
With some extra debugging and rewriting, this works:
app.controller('MainCtrl', function($scope) {
var main = this;
this.data = {
document: {
is_provided: true
}
};
$scope.hide = function() {
console.log(main);
main.data.document.is_provided = !main.data.document.is_provided;
}
});
And:
<button ng-click="hide()">Hide</button> (note the brackets)

Related

Wait for $scope to really apply changes in html

I'm having a problem trying to detect when the $scope variable really affect the html.
I need to access to the element after the $scope really apply the changes to the html, but the $scope.$watch is not doing the trick.
JS:
$scope.variable = "<p id='myElement'>Hello world!</p>";
console.log(jQuery('#myElement')[0]);
HTML:
<div ng-bind-html="varible | trustAsHtml"></div>
The problem is that, even if the $scope is applying the new content, the element still doesn't exist and jQuery returns undefined, so i need to wait untill the $scope really apply the changes to the html.
I know that a solution could be the $timeout but it's not really reliable, there is something that could help me?
Dunno why you need jQuery - there is probably an easier way with angular but there you go: angular has private method called $$postDigest, since all the DOM and model changes happens during $digest cycle it should do what you want, the other method would be to use $timeout, I've made working plunker for you
http://plnkr.co/edit/oZ2xfoUQxcOzNw7eXCmT?p=preview
$scope.htmlvar = $sce.trustAsHtml('<strong>Test</strong>');
$timeout(function () {
console.log('timeout', jQuery('strong').text())
}, 0, false) //false to not run another digest
$scope.$$postDigest(function () {
console.log('postDigest', jQuery('strong').text())
})

Basic Scope Connection

I just started AngularJS a few days ago and I've read that scopes are immediately updated whenever the value of their linked element changes. I have this HTML code with my controller:
<div ng-controller="lyricsMod">
<textarea ng-model="valueB"></textarea>
{{valueA}}
And my AngularJS controller:
myMod.controller('lyricsMod', function($scope) {
$scope.valueA = $scope.valueB;
});
However, this outputs nothing. But, changing the HTML code to:
<div ng-controller="lyricsMod">
<textarea ng-model="valueA"></textarea>
{{valueA}}
Produces the wanted result. Pretty sure it has nothing to do with the AngularJS and that its just linking two things together in HTML. I don't understand, if the scope is immediately updated, why is this not working?
try it:
u need to make use of $watch if you want change scope variable on change of other scope variable
myMod.controller('lyricsMod', function ($scope) {
$scope.$watch('valueB',function(){
$scope.valueA=$scope.valueB;
});
$scope.valueA=$scope.valueB;
});
scopes are immediately updated whenever the value of their linked
element changes
Yes but only scope variables bound using ng-model. In your fist example ValueB is bound using ng-model not valueA.If you want to change value of another scope variable when another changes(and its bound using ng-model). Use ng-change directive.
<div ng-controller="lyricsMod">
<textarea ng-model="valueB" ng-change="changeA()"></textarea>
{{valueA}}
$scope.changeA = function () {
$scope.valueA = $scope.valueB;
}

Is only one $watch created regardless of how many DOM elements are bound to the watched $scope?

In the following code, is there only one $watch created even though the <input> element and interpolation of the double curlies {{customer.name}} create two different bindings to $scope.customer.name?
<html ng-app>
<body>
<input ng-model="customer.name" />
{{customer.name}}
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.2.12/angular.min.js"></script>
</body>
</html>
As a follow-up, does the resulting callback to the listener defined in the $watch update the DOM and re-render those changed elements?
Answer to question 1:
There will be two watchers added.
Slightly simplified, a watcher is an object that is added to the scopes $$watchers array. This watcher object has a watchFunction and a listenerFunction.
Even if you used $watch to register the same pair of watch- and listenerFunction twice, there would be two watcher objects added.
Answer to question 2:
It depends on the watcher.
Consider the interpolation in your example: {{ customer.name }}
The listenerFunction (the function that is executed when the watched expression has changed) of the associated watch object will look like this:
function interpolateFnWatchAction(value) {
node[0].nodeValue = value;
}
Pretty straightforward. The listenerFunction will update the node's nodeValue with the new value. When Angular's digest loop is finished the execution leaves Angular and the JavaScript context, which is followed by the browser re-rendering the DOM to reflect changes. Again a bit simplified.
When it comes to the ng-model directive, it gets more complex, since it has a two-way data binding ($scope --> view and view --> $scope) while the previous case has a one-way data binding ($scope --> view).
The watcher added from ng-model doesn't even register a listenerFunction. Instead it performs work in the watchFunction.
I will not go deeper into the matter, but might as well put the code below (which can be found here):
$scope.$watch(function ngModelWatch() {
var value = ngModelGet($scope);
// if scope model value and ngModel value are out of sync
if (ctrl.$modelValue !== value) {
var formatters = ctrl.$formatters,
idx = formatters.length;
ctrl.$modelValue = value;
while (idx--) {
value = formatters[idx](value);
}
if (ctrl.$viewValue !== value) {
ctrl.$viewValue = ctrl.$$lastCommittedViewValue = value;
ctrl.$render();
}
}
return value;
});
Every ng-* and angular expression create internally a separate watcher.
If you want to check how many watchers are created you could check for angular private scope property scope.$$watchers
Sometimes there could be a lot of this watchers for a complex app, with some performance issue, that's why library as Bindonce was born (in the demo you could find interesting code to check watchers)

How to wait till angularjs is done with everything?

Similar to this question, I want to set focus on the last <select> whenever it gets added. As there's a single method doing it, I need no directive and no watch and no events. My function
$scope.addNew = function() {
$scope.items.push({});
$timeout(function() {
$("select").focus();
});
};
works nicely, except when called directly from the controller function definition like
angular.module('myModule').controller('MyCtrl', function($scope, $timeout) {
$scope.items = {};
...
$scope.addNew();
}
It looks like the timeout happens before the DOM gets constructed and $("select") is empty. With a delay of some 100 ms it works again, but this is a bad hack.
Contrary to what's said in the answer to the linked question, timeout doesn't suffice.
So what's a reliable way to wait for angularjs being really done with the DOM and everything?
Update:
It probably doesn't work because of the select to be focused being embedded in directives (including ng-repeat and some own ones) That's why there initially was no DOM element to focus on.
According to the comments, I need a directive. What's unclear is how exactly to do it. I tried and failed and found out a simpler solution.
What I need
I wasn't very explicit with this, so let me clarify.
I'm working with a table where each row contains some editable fields.
In addNew, I want to set focus on the first editable field of the new row.
In my case this happens to be the very last select.
It worked except at the very beginning, when I was adding the very first row from the controller body.
Why I'm opposed to using a directive
To my limited understanding, it's completely backwards:
A directive modifies the look, behavior, or structure of a given element. But there's no element which should be modified. I tried to put a directive on everything from the select itself to the whole body.
It needs to watch something or listen to an event, but I only want to invoke a function manually.
It didn't work (for me and others as the comments to the linked question shows).
I am going to try and influence you to use a directive here, just to perform the behavior.
Here is a fiddle.
Basic premise is adding the behavioral directive to the element inside repeater:
<table>
<tr ng-repeat="item in items">
<td>{{item}}: <input type="text" auto-focus/></td>
</tr>
</table>
Then your directive would put focus on the last added element:
app.directive('autoFocus', function(){
return function link(scope, elem){
elem[0].focus();
}
});
No watchers or events needed unless I am missing something that you require.
Code that manipulates the DOM should go in a directive, but if you switch to a directive and still have reason to wait until Angular is finished updating the scope and the dom, use $scope.$evalAsync:
$scope.$evalAsync( function() {
// This will wait until Angular is done updating the scope
// Do some stuff here
//
});
The solution was very trivial: Instead of calling $scope.addNew(); directly, I put it in $scope.init invoked from <form ng-init="init()">.
According to the documentation
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.
this seems to be wrong (or maybe not, as ngRepeat si involved). I'm only using it to postpone the call to $scope.addNew();, where neither timeout nor posting events worked.

Changes to scope on click are not being updated across my app

Started using Angular last week, read/watched many tutorials and I'm currently trying to build a newsfeed type application.
Here's the skinny: I have a service that gets data from the server. On the newsfeed itself I have two controllers: one that has the entire newsfeed in its scope and another that has an instance for each newsfeed article. If the user clicks an icon on an individual post it should call a service that has been injected into both controllers and then broadcasts a message that the main controller picks up. The main controller then updates a variable in a filter, filtering the newsfeed content based on the user's selection.
Here's the problem: Everything works fine except that the main controller doesn't update the bound variable in the HTML. I have read close to every SO article on two-way binding within an ng-repeat and the related struggles, but in my case the bound variable falls outside an ng-repeat, hence why I'm posting.
The code:
services.factory('filterService', function() {
var filterService = {};
filterService.filterKey = '';
filterService.getFilter = function() {
return filterService.filterKey;
};
filterService.setFilter = function(name) {
filterService.filterKey = name;
$rootScope.$broadcast('changeFilter');
};
return filterService;
});
app.controller('CommentCtrl', function($scope, $timeout, $http, filterService) {
$scope.setSearchParam = function(param) {
alert('clicked: ' + param)
filterService.setFilter(param);
}
app.controller('FeedCtrl', function($scope, articles, filterService, $timeout) {
$scope.articles = articles;
$scope.model = {
value: ''
};
$scope.$on('changeFilter', function() {
console.log(filterService.filterKey);
$scope.model.value = filterService.filterKey
}
});
});
<div class="articles">
<div class="articleStub" ng-repeat="article in articles|filter:model.value">
<div ng-controller="CommentCtrl">
<div class="{{article.sort}}">
<div class="leftBlock">
<a href="#" ng-click="setSearchParam(article.sort)">
<div class="typeIcon">
<i ng-class="{'icon-comments':article.question, 'icon-star':article.create, 'icon-ok-sign':article.notype}"></i>
</div>
</a>
Note: the FeedCtrl controller is called in the app.config $routeprovider function thing whatever its called
Edited to add: the alert and console checks both work, so I'm assuming the issue is not in the filterService or CommentCtrl.
Here's the Plnkr: http://plnkr.co/edit/bTit7m9b04ADwkzWHv88?p=preview
I'm adding another answer as the other is still valid, but is not the only problem!
Having looked at your code, your problems were two fold:
You had a link to href="#"
This was causing the route code to be re-run, and it was creating a new instance of the controller on the same page, but using a different scope. The way I found this out was by adding the debug line: console.log("running controller init code for $scope.$id:" + $scope.$id); into script.js under the line that blanks the model.value. You'll notice it runs on every click, and the $id of the scope is different every time. I don't fully understand what was happening after that, but having two of the same controller looking after the same bit of the page can't be a good thing!
So, with that in mind, I set href="". This ruins the rendering of the button a bit, but it does cure the problem of multiple controllers being instantiated. However, this doesn't fix the problem... what's the other issue?
angular.element.bind('click', ....) is running 'outside the angular world'
This one is a bit more complicated, but basically for angular data-bindings to work, angular needs to know when the scope gets changed. Most of the time it's handled automagically by angular functions (e.g. inside controllers, inside ng-* directives, etc.), but in some cases, when events are triggered from the browser (e.g. XHR, clicks, touches, etc.), you have to tell angular something has changed. You can do this with $scope.$apply(). There are a few good articles on the subject so I'd recommend a bit of reading (try here to begin with).
There are two solutions to this - one is to use the ng-click directive which wraps the native click event with $scope.$apply (and has the added advantage that your markup is more semantic), or the other is to do it yourself. To minimise the changes to your code, I just wrapped your click code in scope.$apply for you:
element.bind('click', function() {
// tell angular that it needs to 'digest' the changes you're about to make.
scope.$apply(function(){
var param = scope.article.sort;
filterService.setFilter(param);
})
});
Here's a working version of your code: http://plnkr.co/edit/X1AK0Bc4NZyChrJEknkN?p=preview
Note I also set up a filter on the list. You could easily ad a button to clear it that is hidden when there's no filter set:
<button ng-click="model.value=''" ng-show="model.value">Clear filter</button>
Hope this helps :)
I actually think the problem is not that your model.value isn't getting updated - all that code looks fine.
I think the problem lies in your filter.
<div class="articleStub" ng-repeat="article in articles|filter:model.value">
This filter will match any object with any field that contains model.value. What you actually want to do is the following:
<div class="articleStub"
ng-repeat="article in articles|filter:{sort: model.value}:true">
To specify that you only want to match against the sort property of each article. The final true parameter means that it'll only allow strict matches as well, so ed wouldn't match edward.
Note that | filter:{sort: model.value}:true is an angular expression, the :s are like JavaScript commas. If you were to imagine it in JavaScript it would be more like: |('filter',{sort:model.value}, true) where | is a special 'inject a filter here' function..
EDIT:
I'm finding it hard to debug your example without having the working code in front of me. If you can make it into a plunker I can help more, but in the meantime, I think you should try to make your code less complicated by using a different approach.
I have created a plunker that shows an easy way to filter a list by the item that you click. I've used very little code so hopefully it's quite easy to understand?
I would also recommend making your feed items into a directive. The directives can have their own controller so it would prevent you having to do the rather ugly repeating of a ng-controller.

Resources