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>
Related
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.
I have an usecase where I need to dynamically add directives to an input field, depending on the configuration set in a DB.
It all seemed to work fine, but there were some strange quirks with these input fields.
I discovered that the strange behaviour is caused by the directives calling the formatters when I expect them to call the parsers.
I made a plunker to demonstrate this behaviour.
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
$scope.test1 = 'World1';
$scope.test2 = 'World2';
});
app.directive('test', ['$log', function($log) {
return {
require : 'ngModel',
link : function(scope, elm, attrs, ctrl) {
function parse(viewValue) {
console.log('parsing', viewValue);
return viewValue;
}
function format(viewValue) {
console.log('formatting', viewValue);
return viewValue;
}
ctrl.$formatters.unshift(format);
ctrl.$parsers.unshift(parse);
}
};
}]);
app.directive('variabele', ['$compile', function($compile) {
return {
restrict : 'E',
template : '<div><input ng-model="ngModel" /></div>',
scope : {
ngModel : '='
},
require: ['ngModel'],
link: function(scope, elm, attrs, ctrl) {
console.log('testing');
var input = angular.element(elm.find("input"));
input.attr('test', '');
$compile(input)(scope);
}
};
}]);
plunker
It's a bit simplified from what I have to illustrate the problem. There are two input fields. One of which always has the test directive. The other has the variable directive which in turn adds the test directive dynamically.
In reality one or more directives are added which are defined in the database.
When you change the value of the first input field you can see in tghe console that the parser is called, but when you change the value of the second input field you see that the formatter is being called instead. I'm not sure what I'm doing wrong here.
EDIT: The original plunker was broken, so i fixed it. They now use a different model for each input field and the second input field correctly uses the variabele directive.
It is the expected behaviour,
Formatters change how model values will appear in the view.
Parsers change how view values will be saved in the model.
In your case, you bind the same value in both directive test and variabele. when you change value in test directive parsers are called ( view -> model) and in variabele it is the other way (model -> view) formatters are called.
for more info: How to do two-way filtering in angular.js?
In my Angular app, I defined a custom slider directive that wraps <input type="range">. My custom directive supports ngModel to bind the slider’s value to a variable. The custom directive also requires a fraction-size attribute. It does a calculation on the value and then uses the result to set the step value of the wrapped <input>.
I am seeing a bug when I combine these two features – ngModel and my bound attribute value. They are run in the wrong order.
Here is a demonstration:
angular.module('HelloApp', []);
angular.module('HelloApp').directive('customSlider', function() {
var tpl = "2 <input type='range' min='2' max='3' step='{{stepSize}}' ng-model='theNum' /> 3";
return {
restrict: 'E',
template: tpl,
require: 'ngModel',
scope: {
fractionSize: '='
},
link: function(scope, element, attrs, ngModelCtrl) {
scope.stepSize = 1 / scope.fractionSize;
scope.$watch('theNum', function(newValue, oldValue) {
ngModelCtrl.$setViewValue(newValue);
});
ngModelCtrl.$render = function() {
scope.theNum = ngModelCtrl.$viewValue;
};
}
};
});
angular.module('HelloApp').controller('HelloController', function($scope) {
$scope.someNumber = 2.5;
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.7/angular.min.js"></script>
<div ng-app="HelloApp" ng-controller="HelloController">
<h3>Custom slider</h3>
<custom-slider ng-model="someNumber" fraction-size="10"></custom-slider>
<h3>View/edit the slider’s value</h3>
<input ng-model="someNumber"></input>
</div>
The above slider should start at the middle, which represents 2.5. But it actually starts all the way at the right (representing 3). The slider fixes itself and allows the value 2.5 if you drag it, or if you change its bound value by editing the text field.
I have figured out why this is happening in the demonstration – I just don’t know how to fix it. Currently, when a new custom slider is dynamically added to the page, the wrapped <input>’s step is undefined, and defaults to 1. Then ngModel sets the value to 2.5 – but since step is 1, the value in the input is rounded to 3. Finally, step is set to 0.1 – too late for it to matter.
How can I ensure that the step attribute’s value is bound before ngModel sets the input’s value?
In the example above, the slider is on the page at page load. In my real code, multiple new sliders are added dynamically. They should all bind their step and value in the correct order whenever they are added.
A workaround I don’t like
A workaround is to hard-code the step in the template instead of setting it dynamically. If I were to do this, my custom slider directive would have no use, and I would remove it and just use an <input> directly:
<input type="range" min="2" max="3" step="0.1" ng-model="someNumber">
If I use that, the slider’s value is set correctly, without rounding. But I want to to keep my custom directive, customSlider, so that the calculation of step is abstracted.
Things I tried that didn’t work
Changing the order of the step and ng-model attributes in the template doesn’t have any effect.
I started trying to add a compile function, instead of just having a link function. But I can’t set stepSize in compile because scope isn’t available during the compile phase.
I tried splitting my directive’s link function into pre-link and post-link functions. But whether I set scope.stepSize in pre or in post, the page works as before.
Manually calling scope.$digest() right after setting scope.stepSize just throws Error: [$rootScope:inprog] $digest already in progress.
I don’t think custom directive priorities are a good fit for this, because there is only one custom directive involved. My binding of step’s value isn’t a custom directive, it is just raw {{}}-binding. And I think binding step is a simple enough task that it shouldn’t be wrapped in its own directive.
Taking #pixelbits' answer of direct DOM manipulation further, I wouldn't have another ng-model on the inner input at all, and instead always set/get the properties on the input directly
You have one abstraction level to think about when setting/getting values from the input. Raw DOM events and elements.
As such you are not limited to what Angular allows on such elements (indeed: what it does seems to not be able to handle your use case without work-around). If the browser allows it on the range input, you can do it.
Have 2 ngModelControllers in play on the one UI widget that sets one value can get a bit confusing, at least for me!
You still have access to the outer ngModelController pipeline and all its functionality regarding parsers and validators, if you need/want to use it.
You save from having an extra watcher (but this could be a micro/premature optimization).
See an example below.
angular.module('HelloApp', []);
angular.module('HelloApp').directive('customSlider', function() {
var tpl = "2 <input type='range' min='2' max='3' /> 3";
return {
restrict: 'E',
template: tpl,
require: 'ngModel',
scope: {
fractionSize: '='
},
link: function(scope, element, attrs, ngModelCtrl) {
var input = element.find('input');
input.prop('step', 1 / scope.fractionSize);
input.on('input', function() {
scope.$apply(function() {
ngModelCtrl.$setViewValue(input.prop('value'));
});
});
ngModelCtrl.$render = function(value) {
input.prop('value', ngModelCtrl.$viewValue);
};
}
};
});
angular.module('HelloApp').controller('HelloController', function($scope) {
$scope.someNumber = 2.5;
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.7/angular.min.js"></script>
<div ng-app="HelloApp" ng-controller="HelloController">
<h3>Custom slider</h3>
<custom-slider ng-model="someNumber" fraction-size="10"></custom-slider>
<h3>View/edit the slider’s value</h3>
<input ng-model="someNumber"></input>
</div>
Also available at http://plnkr.co/edit/pMtmNSy6MVuXV5DbE1HI?p=preview
In your link function, manipulate the DOM by adding the step attribute.
You can also simplify your binding with the outer ngModel by putting theNum: '=ngModel' in scope.
var app = angular.module('HelloApp', []);
app.directive('customSlider', function () {
var tpl = "2 <input type='range' min='2' max='3' ng-model='theNum' /> 3";
return {
restrict: 'E',
template: tpl,
require: 'ngModel',
scope: {
fractionSize: '=',
theNum: '=ngModel'
},
link: function (scope, element, attrs, ngModelCtrl) {
var e = element.find('input')[0];
var step = 1 / scope.fractionSize;
e.setAttribute('step', step);
scope.$watch('fractionSize', function (newVal) {
if (newVal) {
var step = 1 / newVal;
e.setAttribute('step', step);
}
});
}
};
});
angular.module('HelloApp').controller('HelloController', function($scope) {
$scope.someNumber = 2.5;
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.7/angular.min.js"></script>
<div ng-app="HelloApp" ng-controller="HelloController">
<h3>Custom slider</h3>
<custom-slider ng-model="someNumber" fraction-size="10"></custom-slider>
<h3>View/edit the slider’s value</h3>
<input ng-model="someNumber"></input> {{ someNumber }}
</div>
I'm not sure if this is the correct way to do it, but adding a timeout to your main controller solves the issue.
$timeout(function() {
$scope.someNumber = 2.5;
});
Edit: While it seems like a dirty hack at first, but note that it is common for (final) $scope variables to be assigned later than templates, because of additional ajax calls to retrieve the values.
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>
This is piggy-backing a little off an earlier question. I have been trying to work with directives and data from a model/array
My model looks something like this:
$scope.testModel = {
inputA:[1,2,3]
};
Then I would have inputs for each.
My directive is checking, on keyup, if the input is greater than a given number (10). If it is, then it sets it as 10.
link: function(scope, element) {
scope.$watch('ngModel',function(val){
element.val(scope.ngModel);
});
element.bind("keyup", function(event) {
if(parseInt(element.val())>10){
element.val(10);
scope.ngModel=element.val();
scope.$apply();
}
});
}
The problem is that I get an error, depending on the input:
TypeError: Cannot set property '0' of undefined
Here is the fiddle to see the error and code I have set up: https://jsfiddle.net/prXm3/3/
NOTE
I would prefer not to change the data set as I receive it directly from the server. I know I can change the model to inputA0:1,inputA1:2,inputA2:3 and get it to work, but then I would have to transform the data when my app gets it, and then re-transform it when I send to back to the server. I would prefer to leave the model as I have it set.
Since your directive is interacting with ngModel, you should work along with it in order to update both the model and the view:
angular.module('test', []).directive('myDirective', function() {
return {
restrict: 'A',
require: '?ngModel',
link: function(scope, element, attrs, ngModel) {
if (!ngModel) return;
ngModel.$parsers.unshift(function(viewValue) {
if(parseInt(viewValue) > 10) {
ngModel.$setViewValue(10);
ngModel.$render();
return 10;
}
else
return viewValue;
});
}
}
});
Working jsFiddle.
Perhaps you'd be interested in checking out the following posts for further information:
NgModelController
Developer's Guide on Forms