$watch not firing on data change - angularjs

I have a watch setup against the model of a ui-select2 dropdown (from ui-bootstrap). The watch fires on load but not on data changes and I can't figure out why.
It isn't the usual problem of not $apply'ing the model change or not using the third parameter for equality comparison (at least from my code).
What do I need to do to get it to fire?
Here is a plunk demonstrating the issue.

Try passing true as a 3rd argument to .$watch()
$rootScope.Scope documentation
$watch(watchExpression, listener, objectEquality)
objectEquality(optional) – {boolean=} – Compare object for equality rather than for reference.

I fixed some stuff.
http://plnkr.co/edit/5Zaln7QT2gETVcGiMdoW?p=preview
The JS
var myMod = angular.module("myApp",[]).controller("MainController", function($scope){
$scope.myModel = {selectedId:null};
}).controller("DetailController",function($scope){
$scope.items = [1,2,3,4];
$scope.watchHitCount = 0;
$scope.$watch('myModel.selectedId', function(newVal, oldVal){
console.log(newVal + " " + oldVal);
$scope.watchHitCount++;
},true);
});
The index.html
<body ng-app="myApp">
<div ng-controller="MainController">
<ng-include src="'detail.html'" ng-controller="DetailController"></ng-include>
</div>
</body>
The detail.html
<pre>watch hit: {{watchHitCount}}</pre>
<pre>selected value: {{myModel.selectedId}}</pre>
<select ng-model="myModel.selectedId" ui-select2="">
<option></option>
<option ng-repeat="item in items" value="{{item}}">{{item}}</option>
</select>
It was complaining about not finding the controller so I set it up the way I normally would with a named ng-app and a module declared that has controllers defined on it.
I also added an object to hold the value in your model. It is bad practice to use the $scope object as your model, instead your scope should refer to an object that is your model.

There are a simple fix for that, to use watch with complex object instead of simple variable
For example (DON"T USE)
$scope.selectedType=1;//default
$scope.$watch(
function () {
return $scope.selectedType;
},
function (newValue, oldValue) {
if (!angular.equals(oldValue, newValue)) {
$scope.DoWork();
}
},
true);
But Use below
$scope.selecteditem={selectedType:1};
$scope.$watch(
function () {
return $scope.selecteditem.selectedType;
},
function (newValue, oldValue) {
if (!angular.equals(oldValue, newValue)) {
$scope.DoWork();
}
},
true);
note that "slectedTypes" in the second example located inside object, not just scope variable. this will work even in old Angular versions.

If you're using the controller-as approach some reading might suggest a syntax like this:
var myController = {
myValue: 1
};
$scope.$watch('$ctrl.myValue', function () {
...
}, true);
Instead just wrap the field in a function like this:
var myController = {
myValue: 1
};
$scope.$watch(function () {
return myController.myValue;
}, function () {
...
}, true);

The only thing that worked for me:
var ctrl = this;
this.$onInit = function () {
console.log("id " + ctrl.event.id);
};
from https://docs.angularjs.org/guide/component#!

Related

scope.$watch(attrs. is not working with a custom directive

I am using scope.$watch to look for changes on an attribute-based directive. These changes are initiated by an ng-model binding on an input element in the view. The directive attribute on the view is being watched using scope.$watch in the directive. Yet the change event never seems to fire in the directive. What is causing my code to break?
The portions highlighted in the code below, where I log to the console (in the directive code, with stars), never fire. The change to the controller scope, via ng-model on the input, is not being propagated to the directive.
If I change the attribute value to a static string, rather than binding it via ng-model, it works.
This code is taken from a working example in the AngularJS documentation here. I cannot 'spot the difference' as the code in the documentation is very similar to mine.
angular.module('myApp.advancedDirectives', [
'myApp.advancedDirectives.advancedDirectives-directive'
])
.value('data', { name: 'John', surname: 'Smith' })
angular.module('myApp.advancedDirectives.advancedDirectives-directive', [])
.directive('advancedDirectives', ['$interval', 'dateFilter', 'data', function ($interval, dateFilter, data) {
console.log(data.length);
function link(scope, element, attrs) {
var format,
timeoutId;
scope.$watch(attrs.advancedDirectives, function (theFormat) {
format = theFormat;
**console.log(theFormat);
updateTime();**
});
element.on('$destroy', function () {
$interval.cancel(timeoutId);
});
// start the UI update process; save the timeoutId for canceling
timeoutId = $interval(function () {
updateTime(); // update DOM
}, 1000);
function updateTime() {
scope.directiveScope2 = dateFilter(new Date(), format);
}
}
var theScope = {
directiveScope1: '=info'
}
return {
templateUrl: 'components/advancedDirectives/advancedDirectivesTemplate.html',
scope: theScope,
link: link
}
}]);
<div ng-controller="viewAdvancedDirectivesCtrl">
<div>
<div><input ng-model="theViewFormat"/></div>
<div>Data from the directive scope: <span advanced-directives="theViewFormat" info='data'></span></div>
</div>
</div>
<span style='background:yellow'>Advanced Directive. Here is some data: {{directiveScope1.name}} {{directiveScope1.surname}}, alive at {{directiveScope2}}</span>
As you are using isolated scope for your directive, theViewFormat value wouldn't be available inside your directive context.
Either you need to use $parent.+ attrs.advancedDirectives while placing $watch
OR
More preferred way would be pass advancedDirectives via attribute to isolated scope like you did for passing info data
var theScope = {
directiveScope1: '=info',
advanced: '=advancedDirectives'
}
Then simply your watch would be on 'advanced' string.
scope.$watch('advanced', function (theFormat) {
format = theFormat;
console.log(theFormat);
updateTime();**
});
Here is my answer:
Below, please see the view code.
Note: I used curly-braces for {{theViewFormat}}
<div ng-controller="viewAdvancedDirectivesCtrl">
<div>
<div>Data from the directive scope: <span advanced-directives="{{theViewFormat}}" info='data'></span></div>
</div>
</div>
The link code looks like below.
Note: Use of $observe (as recommended by #Anik)
Note: I am observing on the attrs, rather than on the directive scope object, therefore avoiding recursive calls to updateTime, and I am now observing the attribute change, rather than the scope change, which matches my requirement.
function link(scope, element, attrs) {
var format,
timeoutId;
attrs.$observe('advancedDirectives', function (theFormat) {
format = theFormat;
console.log('format changed: ' + format);
updateTime();
});
element.on('$destroy', function () {
$interval.cancel(timeoutId);
});
// start the UI update process; save the timeoutId for canceling
timeoutId = $interval(function () {
updateTime(); // update DOM
}, 1000);
function updateTime() {
scope.directiveScope2 = dateFilter(new Date(), format);
console.log('updateTime: ' + format + dateFilter(new Date(), format));
}
}
The updateTime() code, which updates the directive scope is as follows:
function updateTime() {
scope.directiveScope2 = dateFilter(new Date(), format);
console.log('updateTime: ' + format + dateFilter(new Date(), format));
}
Thanks to #Pankaj and #Anik for getting me a long way ahead.

update angularjs model from jquery [duplicate]

I have this simple scenario:
Input element which value is changed by jQuery's val() method.
I am trying to update the angular model with the value that jQuery set. I tried to write a simple directive, but it's not doing what I want.
Here's the directive:
var myApp = angular.module('myApp', []);
myApp.directive('testChange', function() {
return function(scope, element, attrs) {
element.bind('change', function() {
console.log('value changed');
})
}
})
this is the jQuery part:
$(function(){
$('button').click(function(){
$('input').val('xxx');
})
})
and html:
<div ng-app="myApp">
<div ng-controller="MyCtrl">
<input test-change ng-model="foo" />
<span>{{foo}}</span>
</div>
</div>
<button>clickme</button>
Here is the fiddle with my try:
http://jsfiddle.net/U3pVM/743/
Can someone please point me in the right direction?
ngModel listens for "input" event, so to "fix" your code you'd need to trigger that event after setting the value:
$('button').click(function(){
var input = $('input');
input.val('xxx');
input.trigger('input'); // Use for Chrome/Firefox/Edge
input.trigger('change'); // Use for Chrome/Firefox/Edge + IE11
});
For the explanation of this particular behaviour check out this answer that I gave a while ago: "How does AngularJS internally catch events like 'onclick', 'onchange'?"
But unfortunately, this is not the only problem you have. As pointed out with other post comments, your jQuery-centric approach is plain wrong. For more info take a look at this post: How do I “think in AngularJS” if I have a jQuery background?).
Hope this is useful for someone.
I was unable to get the jQuery('#myInputElement').trigger('input') event to be picked up my angular app.
I was however, able to get angular.element(jQuery('#myInputElement')).triggerHandler('input') to be picked up.
The accepted answer which was triggering input event with jQuery didn't work for me. Creating an event and dispatching with native JavaScript did the trick.
$("input")[0].dispatchEvent(new Event("input", { bubbles: true }));
I don't think jQuery is required here.
You can use $watch and ng-click instead
<div ng-app="myApp">
<div ng-controller="MyCtrl">
<input test-change ng-model="foo" />
<span>{{foo}}</span>
<button ng-click=" foo= 'xxx' ">click me</button>
<!-- this changes foo value, you can also call a function from your controller -->
</div>
</div>
In your controller :
$scope.$watch('foo', function(newValue, oldValue) {
console.log(newValue);
console.log(oldValue);
});
You have to use the following code in order to update the scope of the specific input model as follows
$('button').on('click', function(){
var newVal = $(this).data('val');
$('select').val(newVal).change();
var scope = angular.element($("select")).scope();
scope.$apply(function(){
scope.selectValue = newVal;
});
});
I made modifications on only controller initialization by adding listener on action button:
$(document).on('click', '#action-button', function () {
$timeout(function () {
angular.element($('#input')).triggerHandler('input');
});
});
Other solutions did not work in my case.
I know it's a bit late to answer here but maybe I may save some once's day.
I have been dealing with the same problem. A model will not populate once you update the value of input from jQuery. I tried using trigger events but no result.
Here is what I did that may save your day.
Declare a variable within your script tag in HTML.
Like:
<script>
var inputValue="";
// update that variable using your jQuery function with appropriate value, you want...
</script>
Once you did that by using below service of angular.
$window
Now below getData function called from the same controller scope will give you the value you want.
var myApp = angular.module('myApp', []);
app.controller('imageManagerCtrl',['$scope','$window',function($scope,$window) {
$scope.getData = function () {
console.log("Window value " + $window.inputValue);
}}]);
I've written this little plugin for jQuery which will make all calls to .val(value) update the angular element if present:
(function($, ng) {
'use strict';
var $val = $.fn.val; // save original jQuery function
// override jQuery function
$.fn.val = function (value) {
// if getter, just return original
if (!arguments.length) {
return $val.call(this);
}
// get result of original function
var result = $val.call(this, value);
// trigger angular input (this[0] is the DOM object)
ng.element(this[0]).triggerHandler('input');
// return the original result
return result;
}
})(window.jQuery, window.angular);
Just pop this script in after jQuery and angular.js and val(value) updates should now play nice.
Minified version:
!function(n,t){"use strict";var r=n.fn.val;n.fn.val=function(n){if(!arguments.length)return r.call(this);var e=r.call(this,n);return t.element(this[0]).triggerHandler("input"),e}}(window.jQuery,window.angular);
Example:
// the function
(function($, ng) {
'use strict';
var $val = $.fn.val;
$.fn.val = function (value) {
if (!arguments.length) {
return $val.call(this);
}
var result = $val.call(this, value);
ng.element(this[0]).triggerHandler('input');
return result;
}
})(window.jQuery, window.angular);
(function(ng){
ng.module('example', [])
.controller('ExampleController', function($scope) {
$scope.output = "output";
$scope.change = function() {
$scope.output = "" + $scope.input;
}
});
})(window.angular);
(function($){
$(function() {
var button = $('#button');
if (button.length)
console.log('hello, button');
button.click(function() {
var input = $('#input');
var value = parseInt(input.val());
value = isNaN(value) ? 0 : value;
input.val(value + 1);
});
});
})(window.jQuery);
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div ng-app="example" ng-controller="ExampleController">
<input type="number" id="input" ng-model="input" ng-change="change()" />
<span>{{output}}</span>
<button id="button">+</button>
</div>
If you are using IE, you have to use: input.trigger("change");
add .change() after setting the value.
example:('id').val.('value').change();
also don't forget to add onchange or ng-change tag in html
I did this to be able to update the value of ngModel from the outside with Vanilla/jQuery:
function getScope(fieldElement) {
var $scope = angular.element(fieldElement).scope();
var nameScope;
var name = fieldElement.getAttribute('name');
if($scope) {
if($scope.form) {
nameScope = $scope.form[name];
} else if($scope[name]) {
nameScope = $scope[name];
}
}
return nameScope;
}
function setScopeValue(fieldElement, newValue) {
var $scope = getScope(fieldElement);
if($scope) {
$scope.$setViewValue(newValue);
$scope.$validate();
$scope.$render();
}
}
setScopeValue(document.getElementById("fieldId"), "new value");
Not what OP asked, but for any soul that might be as well writing an userscript that goes through input fields and fills the required details. Nothing (fully) worked for me, but finally managed to get it done this way:
var el = $('#sp_formfield_fw_ip');
el.val("some value");
angular.element(el).triggerHandler('focus');
angular.element(el).triggerHandler('input');
angular.element(el).triggerHandler('change');
angular.element(el).triggerHandler('blur');
Open developer tools, and inspect input field for added events. There I found all of them (in my case): focus, input, change and blur.

Angular Bootstrap-Select timing issue to refresh

I love Bootstrap-Select and I am currently using it through the help of a directive made by another user joaoneto/angular-bootstrap-select and it works as intended except when I try to fill my <select> element with an $http or in my case a dataService wrapper. I seem to get some timing issue, the data comes after the selectpicker got displayed/refreshed and then I end up having an empty Bootstrap-Select list.. though with Firebug, I do see the list of values in the now hidden <select>. If I then go in console and manually execute a $('.selectpicker').selectpicker('refresh') it then works. I got it temporarily working by doing a patch and adding a .selectpicker('refresh') inside a $timeout but as you know it's not ideal since we're using jQuery directly in an ngController...ouch!So I believe the directive is possibly missing a watcher or at least something to trigger that the ngModel got changed or updated. Html sample code:
<div class="col-sm-5">
<select name="language" class="form-control show-tick"
ng-model="vm.profile.language"
selectpicker data-live-search="true"
ng-options="language.value as language.name for language in vm.languages">
</select>
<!-- also tried with an ng-repeat, which has the same effect -->
</div>
then inside my Angular Controller:
// get list of languages from DB
dataService
.getLanguages()
.then(function(data) {
vm.languages = data;
// need a timeout patch to properly refresh the Bootstrap-Select selectpicker
// not so good to use this inside an ngController but it's the only working way I have found
$timeout(function() {
$('.selectpicker, select[selectpicker]').selectpicker('refresh');
}, 1);
});
and here is the directive made by (joaoneto) on GitHub for Angular-Bootstrap-Select
function selectpickerDirective($parse, $timeout) {
return {
restrict: 'A',
priority: 1000,
link: function (scope, element, attrs) {
function refresh(newVal) {
scope.$applyAsync(function () {
if (attrs.ngOptions && /track by/.test(attrs.ngOptions)) element.val(newVal);
element.selectpicker('refresh');
});
}
attrs.$observe('spTheme', function (val) {
$timeout(function () {
element.data('selectpicker').$button.removeClass(function (i, c) {
return (c.match(/(^|\s)?btn-\S+/g) || []).join(' ');
});
element.selectpicker('setStyle', val);
});
});
$timeout(function () {
element.selectpicker($parse(attrs.selectpicker)());
element.selectpicker('refresh');
});
if (attrs.ngModel) {
scope.$watch(attrs.ngModel, refresh, true);
}
if (attrs.ngDisabled) {
scope.$watch(attrs.ngDisabled, refresh, true);
}
scope.$on('$destroy', function () {
$timeout(function () {
element.selectpicker('destroy');
});
});
}
};
}
One problem with the angular-bootstrap-select directive, is that it only watches ngModel, and not the object that's actually populating the options in the select. For example, if vm.profile.language is set to '' by default, and vm.languages has a '' option, the select won't update with the new options, because ngModel stays the same. I added a selectModel attribute to the select, and modified the angular-bootstrap-select code slightly.
<div class="col-sm-5">
<select name="language" class="form-control show-tick"
ng-model="vm.profile.language"
select-model="vm.languages"
selectpicker data-live-search="true"
ng-options="language.value as language.name for language in vm.languages">
</select>
</div>
Then, in the angular-bootstrap-select code, I added
if (attrs.selectModel) {
scope.$watch(attrs.selectModel, refresh, true);
}
Now, when vm.languages is updated, the select will be updated too. A better method would probably be to simply detect which object should be watched by using ngOptions, but using this method allows for use of ngRepeat within a select as well.
Edit:
An alternative to using selectModel is automatically detecting the object to watch from ngOptions.
if (attrs.ngOptions && / in /.test(attrs.ngOptions)) {
scope.$watch(attrs.ngOptions.split(' in ')[1], refresh, true);
}
Edit 2:
Rather than using the refresh function, you'd probably be better off just calling element.selectpicker('refresh'); again, as you only want to actually update the value of the select when ngModel changes. I ran into a scenario where the list of options were being updated, and the value of the select changed, but the model didn't change, and as a result it didn't match the selectpicker. This resolved it for me:
if (attrs.ngOptions && / in /.test(attrs.ngOptions)) {
scope.$watch(attrs.ngOptions.split(' in ')[1], function() {
scope.$applyAsync(function () {
element.selectpicker('refresh');
});
}, true);
}
Well, this is an old one... But I had to use it. This is what I added in the link(..) function of the directive:
scope.$watch(
_ => element[0].innerHTML,
(newVal, oldVal) => {
if (newVal !== oldVal)
{
element.selectpicker('refresh');
}
}
)

Difference between & and = for passing functions to isolate scope

& is always described as the way to call a function on the parent scope inside a directive's isolated scope.
However, since = creates two-way binding and a function is just another value, shouldn't it be just as effective for this purpose?
The only difference I see is that using &, you can modify the passed function without affecting the parent, since it's one-way binding.
So why is & usually recommended over = for this use case?
There is also some weird behavior that I've come across. Using & gives you a function wrapper. If you unwrap it in the controller and call it, it will resolve differently than if you unwrap it as the result of an ng-click inside the directive.
I've set up an experiment in this fiddle:
app.directive('myDir', function() {
return {
restrict: 'E',
template: '<button ng-click="parentFunc1()(1); parentFunc2(1)">Directive</button>',
scope: {
parentFunc1: '&func1',
parentFunc2: '=func2',
},
controller: Ctrl2,
}
});
function Ctrl2($scope) {
//Step 1
console.log($scope.parentFunc1);
$scope.parentFunc1()(1);
$scope.parentFunc2(1);
//Step 2
$scope.oldParent1 = $scope.parentFunc1;
$scope.parentFunc1 = function (value) {
console.log(value+1);
};
$scope.parentFunc1(1);
$scope.parentFunc2(1);
//Step 3
$scope.parentFunc1 = $scope.oldParent1;
$scope.parentFunc2 = function (value) {
console.log(value+2);
};
console.log($scope.parentFunc1);
$scope.parentFunc1()(1);
$scope.parentFunc2(1);
//Step 4 -> Click the directive button
}
function Ctrl($scope){
$scope.foo = function (value) {
console.log(value);
};
}
This logs "1,1; 2,1; 1,2; 2,2". The last two pairs of values leave me puzzled because they seem to execute the same code.
Very good question!
See the difference between & and = is simple.
When you are declaring a directive scope, and you add to it & it means that you are declaring a function within the scope rather if it was = it was for a regular property.
WAIT WAIT, those two examples above just worked and they are both functions!
Well that's true but hold on,
You just used them incorrectly.
Using the :"&func" means that you are adding a function that will be evaluated soon.
Confused?
I'll type a perfect example:
<script type="text/javascript">
angular.module("exampleApp", [])
.directive("scopeDemo", function (){
return {
template: "<div><p>Name: {{local}}, City: {{cityFn()}}</p></div>",
scope:{
local: "=nameprop",
cityFn: "&city"
}
}
}
}).controller("scopeCtrl, function($scope){
$scope.data = {
name: "Shahar",
defaultCity: "London"
};
$scope.getCity = function(name){
return name == 'Shahar' ? $scope.data.defaultCity : "unknown";
}
});
</script>
<body ng-controller="scopeCtrl">
<div>
Direct Binding: <input ng-model="data.name" />
</div>
<div scope-demo city="getCity(data.name)" nameprop="data.name"></div> //Reference 1.
</body>
As you can see I've written two attributes to my scope's directive.
one accepts a PROPERTY and one accepts a FUNCTION.
As you can see the result of this directive is rather dull, but it explains the whole point.
You will not succeed doing so if you try to make a function with the '=' since Angular will just ignore that.
I hope it clears it up.
Good luck!
The difference between & and = binding strategies takes place when you want to call function on parent scope with parameters also passed from parent scope.
Let's say you have following controller:
angular.module('myApp').controller('myCtrl', function() {
$scope.mans = [{name: 'Peter'}, {name: 'Alex'}]
$scope.someMethod = function(par) {
console.log(par);
}
});
And HTML:
<div ng-repeat="man in mans">
<button my-dir="someMethod(man.name)">Click me</button>
</div>
In this case myDir directive should only use & binding strategy, because the directive knows nothing aboout passed parameters.

Angular, two way binding with two variables

I have two variables that are related by a function, and the user should be able to change one or the other in an input field which should automatically change the other.
How can I do that, right now I'm just using $watch for both.
Here's some sample code and a fiddle.
JS,
angular.module("test", [])
.controller("MyController", function ($scope) {
$scope.letter = 'A';
$scope.number = 1;
$scope.map = { 'A': 1, 'B': 2, 'C': 3, 'D': 4 };
$scope.$watch('letter', function(new_val, old_val){
if(new_val != old_val){
$scope.number = $scope.map[new_val];
}
});
$scope.$watch('number', function(new_val, old_val){
...
});
});
HTML,
<div ng-app="test">
<div ng-controller="MyController">
<input ng-model="letter" />
<input type="number" ng-model="number" />
</div>
</div>
There are a number of ways you can do this, and using $watch is certainly one of them. As mentioned by Matt, you could also use the ng-change directive to fire a method on your controller.
The third way that I would like to offer up, is to make use of ES5 properties, and the Controller 'as' syntax that Angular introduced in 1.2+
If you define your controller as a JS object instead of using an anonymous function, you can add properties and methods to the prototype:
myController = function () {
this.map = {'A': 1,'B': 2,'C': 3,'D': 4};
this._letter = 'A';
this._number = 1;
};
Now we can extract the work you have already done for getting your letter and number values into functions:
myController.prototype.getLetterValue = function (num) {
for (var key in this.map) {
if (this.map.hasOwnProperty(key)) {
if (this.map[key] === num) {
return key;
}
}
}
};
myController.prototype.getNumberValue = function (letter) {
return this.map[letter];
};
Lastly, we are going to declare a couple of properties on your controller that encapsulate the desired functionality using Object.defineProperty.
Object.defineProperty(
myController.prototype,
"letter", {
get: function () {
return this._letter;
},
set: function (newValue) {
this._letter = newValue;
this._number = this.getNumberValue(this._letter);
},
enumerable: true,
configurable: true
});
Object.defineProperty(
myController.prototype,
"number", {
get: function () {
return this._number;
},
set: function (newValue) {
this._number = newValue;
this._letter = this.getLetterValue(this._number);
},
enumerable: true,
configurable: true
});
Add this controller to your module:
angular.module("test", [])
.controller("MyController", myController);
And lastly, you just need to modify your binding syntax slightly in order to use the new Controller 'as' syntax. This will allow you to bind directly to properties and methods on your controller instead of having to use $scope
<div ng-app="test">
<div ng-controller="MyController as ctrl">
<input ng-model="ctrl.letter" />
<input type="number" ng-model="ctrl.number" />
</div>
</div>
Live Demo
Summary
This isn't exactly less code, but does have several advantages.
Your controller is decoupled from $scope and $watch making it more portable
The controller code is easier to read because all the functionality isn't nested inside an anonymous function
The code is a little more forward looking because future versions of Angular will probably eliminate $scope and the $digest loop altogether by using native observables.

Resources