ngChange fires before value makes it out of isolate scope - angularjs

//main controller
angular.module('myApp')
.controller('mainCtrl', function ($scope){
$scope.loadResults = function (){
console.log($scope.searchFilter);
};
});
// directive
angular.module('myApp')
.directive('customSearch', function () {
return {
scope: {
searchModel: '=ngModel',
searchChange: '&ngChange',
},
require: 'ngModel',
template: '<input type="text" ng-model="searchModel" ng-change="searchChange()"/>',
restrict: 'E'
};
});
// html
<custom-search ng-model="searchFilter" ng-change="loadResults()"></custom-search>
Here is a simplified directive to illustrate. When I type into the input, I expect the console.log in loadResults to log out exactly what I have already typed. It actually logs one character behind because loadResults is running just before the searchFilter var in the main controller is receiving the new value from the directive. Logging inside the directive however, everything works as expected. Why is this happening?
My Solution
After getting an understanding of what was happening with ngChange in my simple example, I realized my actual problem was complicated a bit more by the fact that the ngModel I am actually passing in is an object, whose properties i am changing, and also that I am using form validation with this directive as one of the inputs. I found that using $timeout and $eval inside the directive solved all of my problems:
//main controller
angular.module('myApp')
.controller('mainCtrl', function ($scope){
$scope.loadResults = function (){
console.log($scope.searchFilter);
};
});
// directive
angular.module('myApp')
.directive('customSearch', function ($timeout) {
return {
scope: {
searchModel: '=ngModel'
},
require: 'ngModel',
template: '<input type="text" ng-model="searchModel.subProp" ng-change="valueChange()"/>',
restrict: 'E',
link: function ($scope, $element, $attrs, ngModel)
{
$scope.valueChange = function()
{
$timeout(function()
{
if ($attrs.ngChange) $scope.$parent.$eval($attrs.ngChange);
}, 0);
};
}
};
});
// html
<custom-search ng-model="searchFilter" ng-change="loadResults()"></custom-search>

The reason for the behavior, as rightly pointed out in another answer, is because the two-way binding hasn't had a chance to change the outer searchFilter by the time searchChange(), and consequently, loadResults() was invoked.
The solution, however, is very hacky for two reasons.
One, the caller (the user of the directive), should not need to know about these workarounds with $timeout. If nothing else, the $timeout should have been done in the directive rather than in the View controller.
And two - a mistake also made by the OP - is that using ng-model comes with other "expectations" by users of such directives. Having ng-model means that other directives, like validators, parsers, formatters and view-change-listeners (like ng-change) could be used alongside it. To support it properly, one needs to require: "ngModel", rather than bind to its expression via scope: {}. Otherwise, things would not work as expected.
Here's how it's done - for another example, see the official documentation for creating a custom input control.
scope: true, // could also be {}, but I would avoid scope: false here
template: '<input ng-model="innerModel" ng-change="onChange()">',
require: "ngModel",
link: function(scope, element, attrs, ctrls){
var ngModel = ctrls; // ngModelController
// from model -> view
ngModel.$render = function(){
scope.innerModel = ngModel.$viewValue;
}
// from view -> model
scope.onChange = function(){
ngModel.$setViewValue(scope.innerModel);
}
}
Then, ng-change just automatically works, and so do other directives that support ngModel, like ng-required.

You answered your own question in the title! '=' is watched while '&' is not
Somewhere outside angular:
input view value changes
next digest cycle:
ng-model value changes and fires ng-change()
ng-change adds a $viewChangeListener and is called this same cycle.
See:
ngModel.js#L714 and ngChange.js implementation.
At that time $scope.searchFilter hasn't been updated. Console.log's old value
next digest cycle:
searchFilter is updated by data binding.
UPDATE: Only as a POC that you need 1 extra cycle for the value to propagate you can do the following. See the other anwser (#NewDev for a cleaner approach).
.controller('mainCtrl', function ($scope, $timeout){
$scope.loadResults = function (){
$timeout(function(){
console.log($scope.searchFilter);
});
};
});

Related

Unable to watch form $dirty (or $pristine) value in Angular 1.x

I have a scenario in an Angular 1.x project where I need to watch a controller form within a directive, to perform a form $dirty check. As soon as the form on a page is dirty, I need to set a flag in an injected service.
Here is the general directive code:
var directiveObject = {
restrict: 'A',
require: '^form',
link: linkerFn,
scope: {
ngConfirm: '&unsavedCallback'
}
};
return directiveObject;
function linkerFn(scope, element, attrs, formCtrl) {
...
scope.$watch('formCtrl.$dirty', function(oldVal, newVal) {
console.log('form property is being watched');
}, true);
...
}
The above only enters the watch during initialization so I've tried other approaches with the same result:
watching scope.$parent[formName].$dirty (in this case I pass formName in attrs and set it to a local var formName = attrs.formName)
watching element.controller()[formName] (same result as the above)
I've looked at other SO posts regarding the issue and tried the listed solutions. It seems like it should work but somehow the form reference (form property references) are out of scope within the directive and therefore not being watched.
Any advice would be appreciated.
Thank you.
I don't know why that watch isn't working, but as an alternative to passing in the entire form, you could simply pass the $dirty flag itself to the directive. That is:
.directive('formWatcher', function() {
restrict: 'A',
scope: {
ngConfirm: '&unsavedCallback', // <-- not sure what you're doing with this
isDirty: '='
},
link: function(scope, element, attrs) {
scope.watch('isDirty', function(newValue, oldValue) {
console.log('was: ', oldValue);
console.log('is: ', newValue);
});
}
})
Using the directive:
<form name="theForm" form-watcher is-dirty="theForm.$dirty">
[...]
</form>

AngularJS $watch controller variable from a directive with scope

From the directive, I want to track changes to a controller variable using $watch.
I have created this jsfiddle. (https://jsfiddle.net/hqz1seqw/7/)
When the page loads, the controller and both directives $watch function gets called but when I change the radio buttons, only the controllers and dir-two $watch function gets called. Why isnt dir-ones $watch function being called?
I want both the directives $watch to fire however, I can only get one of them to (i.e. dir-two). Not sure what I need to change. Does it have something to do with isolated scope? Is there a better way of doing this?
AngularJS Code:
var mod = angular.module("myApp", []);
//Controller
mod.controller("myCtrl", function($scope){
$scope.tempformat = "C";
$scope.one="25 - dir-one";
$scope.$watch('tempformat', function(nv){
alert("nv from controller");
});
$scope.two="35 - dir-two";
});
//dir-one directive
mod.directive("dirOne", function(){
return{
restrict: 'E',
template: "<p>{{info}}</p>",
scope: {info: '='
},
link: function (scope, element, attr) {
scope.$watch('tempformat', function(nv){
alert("nv from directive-one");
if(scope.tempformat === "C"){
element.find("p").append("C");
}
else if(scope.tempformat === "F"){
element.find("p").append("F");
}
});
}
}});
//dir-two directive
mod.directive("dirTwo", function($window){
return{
restrict: "EA",
template: "<p></p>",
link: function (scope, element, attr) {
scope.$watch('tempformat', function(nv){
alert("nv from directive-two");
if(scope.tempformat === "C"){
element.find("p").append("C");
}
else if(scope.tempformat === "F"){
element.find("p").append("F");
}
});
}
}
});
HTML Code:
<div ng-app="myApp" ng-controller="myCtrl">
<h2>Temperature</h2>
<input type="radio" ng-model="tempformat" value="C"/> Celcius
<input type="radio" ng-model="tempformat" value="F"/> Farenheit
<dir-one info="one"></dir-one>
<dir-two info="two"></dir-two>
</div>
Does it have something to do with isolated scope?
The problem is the fact that dir-one separates its scope from the parent. There are some alternatives that can be done in this situation such as:
scope.$watch('$parent.tempformat', function(nv){ //...
which will look to the parent for the specified content.
Another alternative is to bind to the directive itself:
scope: {
info: '=',
tempformat: '='
},
and then in the html:
<dir-one info="one" tempformat="tempformat"></dir-one>
see: the documentation for more information. Particularly the Isolating the Scope of a Directive area.
Is there a better way of doing this?
In general isolate scopes help construct reusable components (as noted in the documentation) so if this is something that is being attempted (from the content noted in the answer) then I would support something along the lines of the second option where you can specify that watch content on the directive itself and consider that the "better" way of doing this.
From my experience, and this is solely my own preference, I would bind it to the directive since I usually isolate my scope(s) for a reason.

ngModel and How it is Used

I am just getting started with angular and ran into the directive below. I read a few tutorials already and am reading some now, but I really don't understand what "require: ngModel" does, mainly because I have no idea what ngModel does overall. Now, if I am not insane, it's the same directive that provides two way binding (the whole $scope.blah = "blah blah" inside ctrl, and then {{blah}} to show 'blah blah' inside an html element controlled by directive.
That doesn't help me here. Furthermore, I don't understand what "model: '#ngModel' does. #ngModel implies a variable on the parents scope, but ngModel isn't a variable there.
tl;dr:
What does "require: ngModel" do?
What does "model : '#ngModel'" do?
*auth is a service that passes profile's dateFormat property (irrelevant to q)
Thanks in advance for any help.
angular.module('app').directive('directiveDate', function($filter, auth) {
return {
require: 'ngModel',
scope: {
model : '#ngModel',
search: '=?search'
},
restrict: 'E',
replace: true,
template: '<span>{{ search }}</span>',
link: function($scope) {
$scope.set = function () {
$scope.text = $filter('date')($scope.model, auth.profile.dateFormat );
$scope.search = $scope.text;
};
$scope.$watch( function(){ return $scope.model; }, function () {
$scope.set();
}, true );
//update if locale changes
$scope.$on('$localeChangeSuccess', function () {
$scope.set();
});
}
};
});
ngModel is an Angular directive responsible for data-binding. Through its controller, ngModelController, it's possible to create directives that render and/or update the model.
Take a look at the following code. It's a very simple numeric up and down control. Its job is to render the model and update it when the user clicks on the + and - buttons.
app.directive('numberInput', function() {
return {
require: 'ngModel',
restrict: 'E',
template: '<span></span><button>+</button><button>-</button>',
link: function(scope, element, attrs, ngModelCtrl) {
var span = element.find('span'),
plusButton = element.find('button').eq(0),
minusButton = element.find('button').eq(1);
ngModelCtrl.$render = function(value) {
updateValue();
};
plusButton.on('click', function() {
ngModelCtrl.$setViewValue(ngModelCtrl.$modelValue + 1);
updateValue();
});
minusButton.on('click', function() {
ngModelCtrl.$setViewValue(ngModelCtrl.$modelValue - 1);
updateValue();
});
function updateValue(value) {
span.html(ngModelCtrl.$modelValue);
}
}
};
});
Working Plunker
Since it interacts with the model, we can use ngModelController. To do that, we use the require option to tell Angular we want it to inject that controller into the link function as its fourth argument. Now, ngModelController has a vast API and I won't get into much detail here. All we need for this example are two methods, $render and $setViewValue, and one property, $modelValue.
$render and $setViewValue are two ways of the same road. $render is called by Angular every time the model changes elsewhere so the directive can (re)render it, and $setViewValue should be called by the directive every time the user does something that should change the model's value. And $modelValue is the current value of the model. The rest of the code is pretty much self-explanatory.
Finally, ngModelController has an arguably shortcoming: it doesn't work well with "reference" types (arrays, objects, etc). So if you have a directive that binds to, say, an array, and that array later changes (for instance, an item is added), Angular won't call $render and the directive won't know it should update the model representation. The same is true if your directive adds/removes an item to/from the array and call $setViewValue: Angular won't update the model because it'll think nothing has changed (although the array's content has changed, its reference remains the same).
This should get you started. I suggest that you read the ngModelController documentation and the official guide on directives so you can understand better how this all works.
P.S: The directive you have posted above isn't using ngModelController at all, so the require: 'ngModel' line is useless. It's simply accessing the ng-model attribute to get its value.

Simple Angular Directive

I'm attempting to write my first Angular directive to add pagination functionality to my app.
I'm keeping it simple for now and want to have the directive just share the scope of the controller.
In my controller, I have a scoped var called:
$scope.pages
Inside my directive's linking function, I can do:
console.dir($scope)
to see the contents of the controller's scope object, but if I try to access the pages property, it comes back as undefined.
What am I missing?
Thanks in advance.
myDirectives.directive("mdPagination", function(){
return {
template: '',
restrict: 'A',
priority: 1000,
scope: false,
link: function($scope, el, attrs){
console.dir($scope.pages); //returns undefined
el.text('hello world');
},
}
});
Edit: I'm aware there's pagination modules out there, but my needs are modest and I thought building my own would be an easy way to start learning how to build directives.
The problem you have is directives linking functions are run before controllers are, so you typically can't access scope values during this linking phase. There are a couple other ways to do this. The most immediate way to solve your problem is with $scope.$watch. So something like this should work:
$scope.watch('pages', function(pages) {
console.dir(pages);
});
This works just fine for lost of circumstances. I would also suggest decoupling your controller from the scope it is declared in a bit by using an attribute to pass in an expression (property name basically) for your watch. A simple example might look like this:
<div my-directive="myProperty">
link: function($scope, el, attrs) {
$scope.watch(attrs.myDirective, function(value) {
//do something you need to do with the value here
});
}
However as you add complexity to your directive you may want to give your directive it's own controller to manage state without directly calling watches. This controller can either use a child scope belonging to the directive itself with scope: true or just use the parent scope with scope: false. Your choice will likely depend upon need. Then you could have:
myDirectives.directive("mdPagination", function(){
return {
template: '',
restrict: 'A',
priority: 1000,
controller: ['$scope', '$element', '$attrs', function($scope, $element, $attrs) {
console.dir($scope.pages);
}],
link: function($scope, el, attrs){
$element.text('hello world');
}
}
});
By default, the directive shares its scope with the controller. To do that, you must not having a "scope" attribute in your object, like that:
myDirectives.directive("mdPagination", function(){
return {
template: '',
restrict: 'A',
priority: 1000,
link: function($scope, el, attrs){
console.dir($scope.pages);
el.text('hello world');
},
}
});
#Sandro is right. You set the scope property in your directive to false which means that the scope will not inherit (or share) its properties with the controller. If you remove the false, it should work as you intend it.
The docs for how to use $scope are here: http://code.angularjs.org/1.2.0-rc.3/docs/api/ng.$rootScope.Scope
The developer guide for creating directives has a lot of good info about how to setup your directive so that it gives you the scope you want. Those docs are here: http://code.angularjs.org/1.2.0-rc.3/docs/guide/directive
Hope that helps.

AngularJS - ngClick, custom directive, and isolated scope issue

Consider the following directive: (Live Demo)
app.directive('spinner', function() {
return {
restrict: 'A',
scope: {
spinner: '=',
doIt: "&doIt"
},
link: function(scope, element, attrs) {
var spinnerButton = angular.element("<button class='btn disabled'><i class='icon-refresh icon-spin'></i> Doing...</button>");
element.after(spinnerButton);
scope.$watch('spinner', function(showSpinner) {
spinnerButton.toggle(showSpinner);
element.toggle(!showSpinner);
});
}
};
});
which is used like this:
<button ng-click="doIt()" spinner="spinIt">Spin It</button>
When spinner's value (i.e. the value of $scope.spinIt in this example) is true, the element should be hidden and spinnerButton should appear instead. When spinner's value is false, the element should be visible and spinnerButton should be hidden.
The problem here is that doIt() is not in the isolated scope, thus not called on click.
What would be the "Angular way" to implement this directive?
My suggestion is to look at what's going on with these spinners. Be a little more API focused.
Relevant part follows. We use a regular callback to indicate when we're done, so the spinner knows to reset the state of the button.
function SpinDemoCtrl($scope, $timeout, $q) {
$scope.spinIt = false;
$scope.longCycle = function(complete) {
$timeout(function() {
complete();
}, 3000);
};
$scope.shortCycle = function(complete) {
$timeout(function() {
complete();
}, 1000);
};
}
app.directive('spinnerClick', function() {
return {
restrict: 'A',
scope: {
spinnerClick: "=",
},
link: function(scope, element, attrs) {
var spinnerButton = angular.element("<button class='btn disabled'><i class='icon-refresh icon-spin'></i> Doing...</button>").hide();
element.after(spinnerButton);
element.click(function() {
spinnerButton.show();
element.hide();
scope.spinnerClick(function() {
spinnerButton.hide();
element.show();
});
});
}
};
});
Here's one that expects use of $q. It'll work better with Angular-style asynchronous operations, and eliminates the callback functions by instead having the spinner reset on fulfilment of the promise.
Here is the polished version of the directive I ended up with (based on Yuki's suggestion), in case it helps someone: (CoffeeScript)
app.directive 'spinnerClick', ->
restrict: 'A'
link: (scope, element, attrs) ->
originalHTML = element.html()
spinnerHTML = "<i class='icon-refresh icon-spin'></i> #{attrs.spinnerText}"
element.click ->
return if element.is('.disabled')
element.html(spinnerHTML).addClass('disabled')
scope.$apply(attrs.spinnerClick).then ->
element.html(originalHTML).removeClass('disabled')
Use it like so:
<button class="btn btn-primary" spinner-click="createNewTask()"
spinner-text="Creating...">
Create
</button>
Controller's code:
TasksNewCtrl = ($scope, $location, $q, Task) ->
$scope.createNewTask = ->
deferred = $q.defer()
Task.save $scope.task, ->
$location.path "/tasks"
, (error) ->
// Handle errors here and then:
deferred.resolve()
deferred.promise
Yes, it will call doIt in your isolated scope.
You can use $parent.doIt in that case
<button ng-click="$parent.doIt()" spinner="spinIt">Spin It</button>
From the AngularJS Documentation (http://docs.angularjs.org/guide/directive):
& or &attr - provides a way to execute an expression in the context of the parent scope. If no attr name is specified then the attribute name is assumed to be the same as the local name. Given and widget definition of scope: { localFn:'&myAttr' }, then isolate scope property localFn will point to a function wrapper for the count = count + value expression. Often it's desirable to pass data from the isolated scope via an expression and to the parent scope, this can be done by passing a map of local variable names and values into the expression wrapper fn. For example, if the expression is increment(amount) then we can specify the amount value by calling the localFn as localFn({amount: 22}).
so inlclude doIt: "&doIt" in your scope declaration, then you can use doIt as a function in your isolated scope.
I'm confused as to why you are not packaging up everything in the directive as if it's a self-contained module. That's at least what I would do. In other words, you have the click-handler in the HTML, some behavior in the directive and some behavior in the external controller. This makes your code much less portable and more decentralized.
Anyway, you may have reasons for this that are not shared, but my suggestion would be to put all the "Spin It" related stuff in the spinner directive. This means the click-handler, the doIt() function and template stuff all within the link function.
That way there's no need to worry about sharing scope and code entanglement. Or, am I just missing something?
I don't know about the 'angular' way of doing things, but i suggest not using an isolated scope but instead just creating a child scope. You then do attrs.$observe to get any properties you need for your directive.
I.E. :
app.directive('spinner', function() {
return {
restrict: 'A',
scope: true, //Create child scope not isolated scope
link: function(scope, element, attrs) {
var spinnerButton = angular.element("<button class='btn disabled'><i class='icon-refresh icon-spin'></i> Doing...</button>");
element.after(spinnerButton);
//Using attrs.$observe
attrs.$observe('spinner', function(showSpinner) {
spinnerButton.toggle(showSpinner);
element.toggle(!showSpinner);
});
}
};
});
I find this way is better than using '$parent' to escape the isolated scope in other directives (eg ngClick or ngModel) as the end user of your directive does not need to know whether or not using your directive requires them to use '$parent' or not on core angularjs directives.
Using CoffeeScript and a FontAwesome icon.
No need to manually specify the spinner-text
It will just add the spinner content left of the text while loading
We must use finally instead of then for the promise otherwise the spinner will stay there on failure?
I must use $compile because the contents of the button is dynamically compiled as I am using https://github.com/angular-translate/angular-translate
app.directive 'spinnerClick', ["$compile", ($compile) ->
restrict: 'A'
link: (scope, element, attrs) ->
originalHTML = element.html()
spinnerHTML = "<i class='fa fa-refresh fa-spin'></i> "
element.click ->
return if element.is('.disabled')
element.html(spinnerHTML + originalHTML).addClass('disabled')
$compile(element.contents())(scope)
scope.$apply(attrs.spinnerClick).finally ->
element.html(originalHTML).removeClass('disabled')
$compile(element.contents())(scope)
]

Resources