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

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.

Related

Value in ngModel not updated in Angular input view

I need to format the input values so I create a directive that use a template with require: 'ngModel' because I have to use ngModelController functions ($parsers, $formatters, etc.).
This is my HTML:
<div ng-model="myInputValue" amount-input-currency=""></div>
{{myInputValue}}
This is my directive:
.directive('amountInputCurrency', [function(){
return {
templateUrl: '../amountInputCurrency.tmpl.html',
require: 'ngModel',
restrict: 'A',
link: function(scope, elem, attrs, model) {
// ...
}
}
}
And this is my template:
<input type="text" ng-model="myInputValue">
The problem is that I can't updated the view after formatting the inserted value. For example if I write '1' I want change the value in this way:
model.$formatters.push(function(value) {
return value + '00';
}
Alternative I try to set an event in this other way:
<input type="text" ng-model="myInputValue" ng-blur="onBlur()">
scope.onBlur = function() {
model.$viewValue = model.$viewValue + '00';
// or model.$setViewValue(model.$viewValue + '00';);
model.$render();
};
The model.$viewValue changes, myInputValue (in the HTML with {{myInputValue}}) changes but not the value showed in the input box... which is the problem? Thanks!
----------------UPDATE----------------
Probably the problem is because I have 2 ng-model (one in the HTML and one in the template): https://github.com/angular/angular.js/issues/9296
How can I do? Both model refer to the same model...
Formatters change how model values will appear in the view.
Parsers change how view values will be saved in the model.
//format text going to user (model to view)
ngModel.$formatters.push(function(value) {
return value.toUpperCase();
});
//format text from the user (view to model)
ngModel.$parsers.push(function(value) {
return value.toLowerCase();
});
Try using $parsers to change the view to your desired value.
I hope this will help you.
Update:
angular.module('components', []).directive('formatParse', function() {
return {
restrict: 'E',
require: 'ngModel',
scope: { model: "=ngModel" },
template: '<input type="text" data-ng-model="model"></input><button type="button" data-ng-click="clickedView()">SetView</button><button type"button" data-ng-click="clickedModel()">SetModel</button>',
link: function ($scope, el, attrs, ngModelCtrl) {
format = "MMM Do, YYYY H:mm";
console.log($scope.model);
console.log(ngModelCtrl);
$scope.clickedView = function () {
ngModelCtrl.$setViewValue(ngModelCtrl.$viewValue);
};
$scope.clickedModel = function () {
$scope.model = 12; // Put here whatever you want
};
ngModelCtrl.$parsers.push(function (date) {
console.log("Parsing", date)
return date; // Put here the value you want to be in $scope.model
});
ngModelCtrl.$formatters.push(function (date) {
console.log("Formatting", date);
console.log($scope.model);
console.log(ngModelCtrl);
return +date * 2; // Put here what you want to be displayed when clicking setView (This will be in ngModelCtrl.$viewValue)
});
}
}
});
angular.module('someapp', ['components']);
Try using this code and tell if this helped to get the result you wanted.
If it does I suggest, to console.log the ngModelCtrl that you way you will understand more about the inner flow of angular.
In addition, just so you have some more information,
When you edit the input in the view the formatters function are fired to change the model accordingly.
If the value that has been entered is not valid you can return in your formatters function the ngModelCtrl.$viewValue to keep $scope.model with his old and true information.
When you change your scope variable (in your case $scope.model) the parsers functions will be fired to change the view value. (You don't need to use $render, you just need to decide when you want to change your $scope.model),
I suggest instead of using $setViewValue put the value you want in your scope variable and the parsers will act accordingly.

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 call a controller method every time custom directive select changes

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.

Angularjs: how to update my model only once

I need to update my model after is has loaded its data. So i tried to write a directive which can do that. I didn't thought it would be this hard :(
I first tried a filter, which should be more simple, but got this error.
Error: [ngModel:nonassign] Expression 'editpage.url | addUrl' is non-assignable.
So now i try the directive way. This is my html code in the view:
<input ng-model="editpage.url" add-url type="text" class="light_txtbox" readonly>
And this is my directive:
app.directive('addUrl', [function() {
return {
restrict: 'A',
require: '?ngModel',
replace: true,
link: function(scope, element, attrs, ngModel) {
if (!ngModel) return;
// what to do next?
}
};
}]);
In the "what to do next" part i tried a watch like this:
scope.$watch(attrs.ngModel, function(site) {
if (typeof site !== undefined) {
ngModel.$setViewValue('www.mysite.com/' + site);
ngModel.$render();
}
});
But of course now the model is updated and "hey, i am changed so update again!" and again and again...
I only need the update to take place once. I think i need another approach, but can not figure out what to do.
You can unregister a $watch
var unregister = scope.$watch(attrs.ngModel, function() {
if (shouldStopWatching) {
unregister();
}
});
where shouldStopWatching is whatever condition you need (i.e. stop on second call of callback etc)

Angular.js rewrite ngShow function to directive

I'm looking to change the functionality defined in the ng-show directive I have on the form fields to display issues with a form field.
For example:
<span class="help-inline" ng-show="showError(schoolSignup.last_name, 'required')">This field is required</span>
Where schoolSignup.last_name is the reference to the model controller for the field, and 'required' is the validation property I'm looking for.
Defined in a controller as
$scope.showError = function(ngModelController, error) {
return ngModelController.$dirty && ngModelController.$error[error];
};
I've been banging my head against a wall trying to work out how to move this to a directive so I don't have to re-define this in every controller. I was thinking of defining it like...
<span class="help-inline" show-error="required" field="schoolSignup.last_name">This field is required</span>
This is as far as I've got
.directive('showError', function () {
return {
restrict: 'A',
scope:{
field: "="
},
link: function(scope, elem, attrs, ctrl)
{
var errorType = attrs.showError;
scope.errors = scope.field.$error[errorType];
// NOT WORKING YET
}
};
});
How can I do this??
You're on the right track; however, the dot in formName.fieldName will give you trouble using the object[field] syntax. Instead, you could do this pretty nicely with the $parse service (docs):
app.directive("showError", function($parse) {
return {
link: function(scope, elem, attrs) {
// Returns a function that, when called with a scope,
// evaluates the expression in `attrs.field` (e.g.
// "schoolSignup.last_name") on the scope.
var getter = $parse(attrs.field);
// Every time a digest cycle fires...
scope.$watch(function() {
// ...get the input field specified in the `field` attribute...
var field = getter(scope);
// ...and check to see if the error specified by the
// `show-error` attribute is set.
if (field.$dirty && field.$error[attrs.showError]) {
elem.show();
} else {
elem.hide();
}
});
}
};
});
Here's a working example: http://jsfiddle.net/BinaryMuse/Lh7YY/

Resources