Angularjs call a controller method every time custom directive select changes - angularjs

I have written a custom directive for select component. The problem I face is simpleComboSelectionChanged() prints the previous selected value and not the current value. Please let me know what is the problem.
directive & Controller:
.directive('simpleSelect', [function($compile) {
return {
restrict: 'E',
transclude: true,
require: '^ngModel',
scope:{
id: '#',
ngModel: '=',
items: '=',
ngChange: '&'
},
// linking method
link: function(scope, element, attrs) {
scope.updateModel = function()
{
scope.ngChange();
};
},
template:'<select class="form-control" id="id" ng-model="ngModel" ng-selected="ngModel" ng-options="Type.type as Type.name | translate for Type in items"'+
'ng-change="updateModel()"></select>'
};
}])
.controller('ComboTemplateCtrl', ['$scope', function($scope) {
$scope.ComboItems = [{type:1, name:"Combo.Item1", isSet:false},
{type:2, name:"Combo.Item2", isSet:false},
{type:3, name:"Combo.Item3", isSet:false},
{type:4, name:"Combo.Item4", isSet:false},
{type:5, name:"Combo.Item5", isSet:false}
];
$scope.simpleSelectValue = $scope.ComboItems[0].type;
$scope.simpleComboSelectionChanged = function(){
console.log("Selected Item is :", $scope.simpleSelectValue);
};
}])
<simple-select id="simpleSelectTest"
ng-model="simpleSelectValue" items="ComboItems"
ng-change="simpleComboSelectionChanged()"></simple-select>

The reason this happens is because you are binding to ngModel as opposed to require: "ngModel" and using the ngModelController API to modify it. (You are, in fact, use require, but you aren't actually using. Neither are you using transclude which is not needed here).
What happens is the inner ngModel-bound variable - scope.ngModel - is changed to the currently selected item, then the inner ng-change is fired, which invokes the outer ng-change, which tries to read the outer variable bound to ng-model attribute - $scope.simpleSelectValue. But $scope.simpleSelectValue hasn't yet changed - this will happen on a $watch that will occur later.
The main point is - this is not how ngModel is meant to be used.
ngModel is a directive that custom input control authors (like yourself) can use to integrate with other ngModel-compatible directives (such as form, ng-required, ng-change and other custom directives) that can validate, transform, or just listen to changes of input values.
Since you are building a custom input control essentially (even if you are using a built-in control under the covers), you need to support your own ngModel.
Here's a conceptual overview of how this can be done:
.directive('simpleSelect',
function($compile) {
return {
restrict: 'E',
require: 'ngModel',
scope: {
id: '#',
items: '=',
},
link: function(scope, element, attrs, ngModel) {
ngModel.$render = function(){
scope.selectedValue = ngModel.$viewValue;
};
scope.onChange = function(){
ngModel.$setViewValue(scope.selectedValue);
};
},
template: '<select class="form-control" id="id" ng-model="selectedValue"' +
'ng-options="Type.type as Type.name for Type in items"' +
'ng-change="onChange()">' +
'</select>'
};
});
ngModel.$render is fired when rendering is required (for example, when model value changes) - all that you need to do here is to set the value bound to inner ng-model - no need even to do direct DOM manipulation.
ngModel.$setViewValue is called when the input directive receives input from the user. Again, since you are using an existing input directive <select>, then you could just rely on its ng-change.
You will also notice that there is no longer a need to have your own ng-change. That is the value of ngModel support - a consumer of your directive can just treat your input control like any other, include support for ng-change.
Read more about custom input controls.

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>

Angular - Cannot get custom directive to cooperate with ng-change

I have a very simple directive (named cooperate-with-ng-change) which requires ng-model and I'd like it to cooperate with ng-change.
Restated, I'd like to be able to do <cooperate-with-ng-change ng-model="model" ng-change="changeHandler()"></cooperate-with-ng-change>
However I'm noticing when changeHandler() is fired, model is the old value and not the new one.
Here's the directive definition object:
return {
restrict: "E",
require: ["ngModel"],
scope: {
"value": "=ngModel"
},
template: "<label>Cooperate with NgChange: <input type='text' ng-model='value' ng-model-options='{debounce: 500}' /></label>",
link: function(scope, element, attrs, ctrls) {
var inputNgModelCtrl = element.find("input").controller("ngModel");
var parentNgModelCtrl = ctrls[0];
Array.prototype.push.apply(inputNgModelCtrl.$viewChangeListeners, parentNgModelCtrl.$viewChangeListeners)
//if I wrap all the parentNgModelCtrl.$viewChangeListeners in a timeout and digest in changeHandler
//then message matches afterDebounce
/*Array.prototype.push.apply(inputNgModelCtrl.$viewChangeListeners,
parentNgModelCtrl.$viewChangeListeners.map(
function(listener) {
return function() {
setTimeout(listener, 1)
}
}))
*/
}
}
Here's a plunker link if you want to play with it
My guess is $viewChangeListeners gets called before the model value is set, however I can't find what might correspond for after the model value is set.

AngularJS nested directive $pristine and $dirty settings

I'm having issues with a directive I am writing.
Within the directive's template there is also another element directive.
Essentially the outer directive is a decorator for the inner, adding more functionality..
The issue that I am having is that the $pristine and $dirty values are not being set as I would have expected.
I have amended the fiddle below to demonstrate a similar scenario..
(Code follows:)
HTML
<body ng-app="demo" ng-controller="DemoController">
<h3>rn-stepper demo (3/5)</h3>
Model value : {{ rating }}<br>
<hr>
<div ng-model="rating" rn-stepper></div>
</body>
JS
angular.module('demo', [])
.controller('DemoController', function($scope) {
$scope.rating = 42;
})
.directive('test', function() {
return {
restrict: 'E',
scope: {
ngModel: '=ngModel'
},
template: '<input type="text" ng-model="ngModel"></input>'
};
})
.directive('rnStepper', function() {
return {
restrict: 'AE',
scope: {
value: '=ngModel'
},
template: '<button ng-click="decrement()">-</button>' +
'<div>{{ value }}</div>' +
'<button ng-click="increment()">+</button>' +
'<test ng-model="value"></test>',
link: function(scope, iElement, iAttrs) {
scope.increment = function() {
scope.value++;
}
scope.decrement = function() {
scope.value--;
}
}
};
});
http://jsfiddle.net/qqqspj7o/
The model is shared as expected and when I change the value in either the text input or using the slider, the binding works - however if I update the value in the text input, only the text input is marked as ng-dirty - the element directive itself remains as ng-pristine as does the outer div.
I don't understand why this is and the values are not propagated to the element? Is that expected behaviour - if so, how do I propagate the ng-dirty etc values to the element directive and the outer div..
Note: I can only use Angular v 1.2.x as the code needs to be compatible with IE8.
Thanks in advance..
Generally in directives you should avoid =value binding, and work directly with ngModelController.
This topic is a bit complicated for discussion here, but there are many great tutorias on the web I point you to this one:
using ngModelController it explains basics of working with ngModel and also tells bit about decorators.
When you work directly with ngModel you can set validity and state (dirty/touched/pristine) directly in your code, you can also set model value via $setViewValue().

ngChange fires before value makes it out of isolate scope

//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);
});
};
});

Should I use isolate scope in this case?

I'm implementing an custom input widget. The real code is more complex, but generally it looks like this:
app.directive('inputWidget', function () {
return {
replace:true,
restrict: 'E',
templateUrl:"inputWidget.html",
compile: function (tElement, tAttributes){
//flow the bindings from the parent.
//I can do it dynamically, this is just a demo for the idea
tElement.find("input").attr("placeholder", tAttributes.placeholder);
tElement.find("input").attr("ng-model", tElement.attr("ng-model"));
}
};
});
inputWidget.html:
<div>
<input />
<span>
</span>
</div>
To use it:
<input-widget placeholder="{{name}}" ng-model="someProperty"></input-widget>
The placeholder is displayed correctly with above code because it uses the same scope of the parent: http://plnkr.co/edit/uhUEGBUCB8BcwxqvKRI9?p=preview
I'm wondering if I should use an isolate scope, like this:
app.directive('inputWidget', function () {
return {
replace:true,
restrict: 'E',
templateUrl:"inputWidget.html",
scope : {
placeholder: "#"
//more properties for ng-model,...
}
};
});
With this, the directive does not share the same scope with the parent which could be a good design. But the problem is this isolate scope definition will quickly become messy as we're putting DOM-related properties on it (placeholder, type, required,...) and every time we need to apply a new directive (custom validation on the input-widget), we need to define a property on the isolate scope to act as middle man.
I'm wondering whether it's a good idea to always define isolate scope on directive components.
In this case, I have 3 options:
Use the same scope as the parent.
Use isolate scope as I said above.
Use isolate scope but don't bind DOM-related properties to it, somehow flow the DOM-related properties from the parent directly. I'm not sure if it's a good idea and I don't know how to do it.
Please advice, thanks.
If the input-widget configuration is complex, I would use an options attribute, and also an isolated scope to make the attribute explicit and mandatory:
<input-widget options="{ placeholder: name, max-length: 5, etc }"
ng-model="name"></input-widget>
There is no need to flow any DOM attributes if you have the options model, and the ngModel:
app.directive('inputWidget', function () {
return {
replace:true,
restrict: 'E',
templateUrl:"inputWidget.html",
scope: { options:'=', ngModel: '='}
};
});
And in your template, you can bind attributes to your $scope view model, as you normally would:
<div>
<input placeholder="{{options.placeholder}}" ng-model="ngModel"/>
<span>
{{options}}
</span>
</div>
Demo
Personally, when developing for re-use, I prefer to use attributes as a means of configuring the directive and an isolated scope to make it more modular and readable. It behaves more like a component and usually without any need for outside context.
However, there are times when I find directives with child / inherited scopes useful. In those cases, I usually 'require' a parent directive to provide the context. The pair of directives work together so that less attributes has to flow to the child directive.
This is not a very trivial problem. This is because one could have arbitrary directives on the templated element that are presumably intended for <input>, and a proper solution should ensure that: 1) these directives compile and link only once and 2) compile against the actual <input> - not <input-widget>.
For this reason, I suggest using the actual <input> element, and add inputWidget directive as an attribute - this directive will apply the template, while the actual <input> element would host the other directives (like ng-model, ng-required, custom validators, etc...) that could operate on it.
<input input-widget
ng-model="someProp" placeholder="{{placeholder}}"
ng-required="isRequired"
p1="{{name}}" p2="name">
and inputWidget will use two compilation passes (modeled after ngInclude):
app.directive("inputWidget", function($templateRequest) {
return {
priority: 400,
terminal: true,
transclude: "element",
controller: angular.noop,
link: function(scope, element, attrs, ctrl, transclude) {
$templateRequest("inputWidget.template.html").then(function(templateHtml) {
ctrl.template = templateHtml;
transclude(scope, function(clone) {
element.after(clone);
});
});
}
};
});
app.directive("inputWidget", function($compile) {
return {
priority: -400,
require: "inputWidget",
scope: {
p1: "#", // variables used by the directive itself
p2: "=?" // for example, to augment the template
},
link: function(scope, element, attrs, ctrl, transclude) {
var templateEl = angular.element(ctrl.template);
element.after(templateEl);
$compile(templateEl)(scope);
templateEl.find("placeholder").replaceWith(element);
}
};
});
The template (inputWidget.template.html) has a <placeholder> element to mark where to place the original <input> element:
<div>
<pre>p1: {{p1}}</pre>
<div>
<placeholder></placeholder>
</div>
<pre>p2: {{p2}}</pre>
</div>
Demo
(EDIT) Why 2 compilation passes:
The solution above is a "workaround" that avoids a bug in Angular that was throwing with interpolate values being set on a comment element, which is what is left when transclude: element is used. This was fixed in v1.4.0-beta.6, and with the fix, the solution could be simplified to:
app.directive("inputWidget", function($compile, $templateRequest) {
return {
priority: 50, // has to be lower than 100 to get interpolated values
transclude: "element",
scope: {
p1: "#", // variables used by the directive itself
p2: "=" // for example, to augment the template
},
link: function(scope, element, attrs, ctrl, transclude) {
var dirScope = scope,
outerScope = scope.$parent;
$templateRequest("inputWidget.template.html").then(function(templateHtml) {
transclude(outerScope, function(clone) {
var templateClone = $compile(templateHtml)(dirScope);
templateClone.find("placeholder").replaceWith(clone);
element.after(templateClone);
});
});
}
};
});
Demo 2

Resources