Value in ngModel not updated in Angular input view - angularjs

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.

Related

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.

$viewValue in ngModelController displays different on IE and Chrome/Firefox

I have created directive myInput that require ngModel controller and then alert ngModelCtrl.$viewValue in input-event trigger catch when I stoke the key. I get different result between IE and Chrome/Firefox
As follow:
http://jsfiddle.net/southbridge/zgyv14g0/
In IE It displays previous value of Input before I've stroked the keyboard ,but in Chrome/Firefox It displays current value .
app.directive('myInput',function(){
return {
restrict:'A',
require:'ngModel',
link:function(scope,element,attrs,ngModelCtrl){
element.on('input',function(){
var x=ngModelCtrl.$viewValue;
alert(x);
});
}
}
});
I am not sure if the input event will trigger after the $viewvalue property has been updated by angular (most possibly not), because angular updates viewvalue property by listening to input/change etc.. events itself.
However one more criteria is the priority of the directive (if you are angular 1.3 and less then provide your directive a priority greater than ngModels i.e ex:1, priority of ngModel has been revised to 1 starting 1.3) and which handler runs first. But if you really just need the value of the input element you could just try accessing it with value property inside the handler i.e this.value
However if you really need the current $viewvalue best bet would be to use $viewChangeListeners of the ngModelController.
.directive('myInput',function(){
return {
restrict:'A',
require:'ngModel',
priority: 1, //if required
link:function(scope,element,attrs,ngModelCtrl){
ngModelCtrl.$viewChangeListeners.push(handleViewValueChange);
function handleViewValueChange(){
alert(ngModelCtrl.$viewValue);
}
//element.on('input', handleViewValueChange); //Or just use this.value in the handler
}
}
});
Array of functions to execute whenever the view value has changed. It is called with no arguments, and its return value is ignored. This can be used in place of additional $watches against the model value.
angular.module('app', []).controller('ctrl', function($scope) {
}).directive('myInput', function() {
return {
restrict: 'A',
require: 'ngModel',
priority: 1, //If needed
link: function(scope, element, attrs, ngModelCtrl) {
ngModelCtrl.$viewChangeListeners.push(handleViewValueChange);
function handleViewValueChange() {
var x = ngModelCtrl.$viewValue;
alert(x);
}
element.on('input', function() {
alert(ngModelCtrl.$viewValue); //or hust this.value
});
}
}
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.3/angular.min.js"></script>
<div ng-app="app" ng-controller="ctrl">
<input my-input ng-model="model" />{{model}}
</div>

AngularJs Bootstrap Datepicker Validation

I am using the Bootstrap Datepicker (found here: http://mgcrea.github.io/angular-strap/#/datepicker) on an input field. By default, the datepicker allows a date to be entered. If it is valid, then it sets the date. Otherwise, it sets the date to the current date.
I wrote a directive that uses a regex to only characters for a date to be input.:
maskModelCtrl.$parsers.push(function(inputValue) {
var val = maskRenderLogic(inputValue, mask, maxVal);
setAndRender(maskModelCtrl, val);
return maskReturnLogic(val);
});
And I wrote a directive to standardize the input field for the datepicker with a template:
angular.module('form.validation').directive('dateMask', ['$parse', function($parse) {
return {
restrict: 'E',
scope: {
date: '='
},
template: '<input ng-model="date" regex-mask="[^0-9 /]" max-length="10" bs-datepicker data-date-format="mm/dd/yyyy" placeholder="mm/dd/yyyy" >',
replace: true
};
}]);
The problem is, the datepicker translates any keyboard input or date selections to the current date. (See plnkr: http://plnkr.co/edit/oCZnq0UOmaxC83Rv7YSs?p=preview) I don't think that these directives should be incompatible with each other.
I realize you've probably already considered this, but I'm writing an application that deals a lot with dates (inputs, modifications, etc) and I tested a lot of different date pickers.
The Angular-UI Bootstrap datepicker seems to be the most malleable and usable.
That being said, your directive should leverage $formatters and $parsers:
Markup:
<input date-validator type="text" ng-model="date"/>
Directive
app.directive('dateValidator', function() {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, elem, attrs, ctrl) {
var validate = function(value) {
//.. do validation logic ../
modifiedValue = value / 2;
// set the validity if needed
ctrl.$setValidity('customDate', false);
//return the modified value
return modifiedValue;
}
// formatters fire when the model directly changes
ctrl.$formatters.unshift(function(value) {
return validate(value);
});
// parsers fire when the model changes via input
ctrl.$parser.unshift(function(value) {
return validate(value);
});
}
}
});

AngularJS - In a directive that changes the model value, why do I have to call $render?

I made a directive designed to be attached to an element using the ngModel directive. If the model's value matches something the value should then set to the previous value. In my example I'm looking for "foo", and setting it back to the previous if that's what's typed in.
My unit tests passed fine on this because they're only looking at the model value. However in practice the DOM isn't updated when the "put back" triggers. Our best guess here is that setting old == new prevents a dirty check from happening. I stepped through the $setViewValue method and it appears to be doing what it ought to. However it won't update the DOM (and what you see in the browser) until I explicitly call ngModel.$render() after setting the new value. It works fine, but I just want to see if there's a more appropriate way of doing this.
Code is below, here's a fiddle with the same.
angular.module('myDirective', [])
.directive('myDirective', function () {
return {
restrict: 'A',
terminal: true,
require: "?ngModel",
link: function (scope, element, attrs, ngModel) {
scope.$watch(attrs.ngModel, function (newValue, oldValue) {
//ngModel.$setViewValue(newValue + "!");
if (newValue == "foo")
{
ngModel.$setViewValue(oldValue);
/*
I Need this render call in order to update the input box; is that OK?
My best guess is that setting new = old prevents a dirty check which would trigger $render()
*/
ngModel.$render();
}
});
}
};
});
function x($scope) {
$scope.test = 'value here';
}
Our best guess here is that setting old == new prevents a dirty check from happening
A watcher listener is only called when the value of the expression it's listening to changes. But since you changed the model back to its previous value, it won't get called again because it's like the value hasn't changed at all. But, be careful: changing the value of a property inside a watcher monitoring that same property can lead to an infinite loop.
However it won't update the DOM (and what you see in the browser) until I explicitly call ngModel.$render() after setting the new value.
That's correct. $setViewValue sets the model value as if it was updated by the view, but you need to call $render to effectively render the view based on the (new) model value. Check out this discussion for more information.
Finally, I think you should approach your problem a different way. You could use the $parsers property of NgModelController to validate the user input, instead of using a watcher:
link: function (scope, element, attrs, ngModel) {
if (!ngModel) return;
ngModel.$parsers.unshift(function(viewValue) {
if(viewValue === 'foo') {
var currentValue = ngModel.$modelValue;
ngModel.$setViewValue(currentValue);
ngModel.$render();
return currentValue;
}
else
return viewValue;
});
}
I changed your jsFiddle script to use the code above.
angular.module('myDirective', [])
.directive('myDirective', function () {
return {
restrict: 'A',
terminal: true,
require: "?ngModel",
link: function (scope, element, attrs, ngModel) {
if (!ngModel) return;
ngModel.$parsers.unshift(function(viewValue) {
if(viewValue === 'foo') {
var currentValue = ngModel.$modelValue;
ngModel.$setViewValue(currentValue);
ngModel.$render();
return currentValue;
}
else
return viewValue;
});
}
};
});
function x($scope) {
$scope.test = 'value here';
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<h1>Foo Fighter</h1>
I hate "foo", just try and type it in the box.
<div ng-app="myDirective" ng-controller="x">
<input type="text" ng-model="test" my-directive>
<br />
model: {{test}}
</div>

Cannot figure out how to create a directive with $parsers and $formatters

I want to create a directive that will allow a user to input a date in a wide variety of formats. When the underlying model changes (whether via direct user input, or programmatically), I would like it to display the date in a normalized format.
An example of this "in the wild" are the departure and return date inputs on Google Flights.
Here's my code (which does not work at all).
VIEW
<input type="text" ng-model="params.departDate"
date-input display-format="{Weekday} {d} {Month}, {yyyy}">
CONTROLLER
app.controller('MainCtrl', function($scope) {
$scope.params = {
departDate: new Date()
};
$scope.isDate = function() {
return $scope.params.departDate instanceof Date;
}
});
DIRECTIVE
app.directive("dateInput", function() {
return {
require: 'ngModel',
scope: {
displayFormat: '#'
},
link: function (scope, element, attrs, ngModel) {
ngModel.$parsers.push(function (viewVal) {
if (viewVal) {
// Using sugar.js for date parsing
var parsedValue = Date.create(viewVal);
if (parsedValue.isValid()) {
ngModel.$setValidity("date", true);
return parsedValue;
}
}
ngModel.$setValidity("date", false);
return viewVal;
});
ngModel.$formatters.unshift(function (modelVal) {
if (modelVal){
// Using sugar.js for date formatting
var date = Date.create(modelVal);
if (date.isValid()) {
return date.format(scope.displayFormat || '{Weekday} {d} {Month}, {yyyy}')
}
}
return modelVal;
});
}
};
})
This doesn't even come close to working as I would expect. What am I doing wrong?
Here's a PLUNKR: http://plnkr.co/edit/583yOD6aRhRD8Y2bA5gU?p=preview
As mentioned in a comment, ng-model and isolate scopes don't mix well, see Can I use ng-model with isolated scope?.
I suggest not creating any scope in your directive, and access the display-format attribute using attrs:
var displayFormat = attrs.displayFormat;
I also prefer isolated scope with reusable directives, in that case ng-model has to be given proper path to parent scope: ng-model="$parent.params.departDate"
updated plunker:
http://plnkr.co/edit/DuR6Om2kyzWD67hYExSh?p=preview

Resources