Angular parser called on blur - angularjs

I have a type of input mask using an angular directive. I'm using a formatters and the blur event to format the model value for display, and I'm using parsers and the focus event to remove the formatting when the user edits the textbox.
I'm getting strange behaviour in Internet Explorer where if you use the Tab key to lose focus, the parser event is (incorrectly) firing so the model value is being updated incorrectly.
Is this an angular bug? Or is there something I'm doing wrong?
Here is a fiddle: https://jsfiddle.net/capesean/htorwgs5/3/
Note that in IE, with your console window open, you will see the events logging out.
Also, testing this on an earlier Angular version, seems to work fine:
https://jsfiddle.net/htorwgs5/4/
The directive code is:
.directive("test", function () {
return {
restrict: "A",
require: 'ngModel',
link: function (scope, element, attr, ngModel) {
// for DOM -> model validation
ngModel.$parsers.unshift(function (value) {
console.log("parser");
ngModel.$setValidity('test', true);
return +value;
});
ngModel.$formatters.unshift(function (value) {
console.log("formatter");
ngModel.$setValidity('test', true);
return (value === undefined ? "" : value) + "!";
});
element.val(scope.minutes);
element.bind("blur", function () {
scope.$apply(function () {
console.log("blur");
element.val((scope.minutes === undefined ? "" : scope.minutes) + "#");
});
});
element.bind("focus", function () {
scope.$apply(function () {
console.log("focus");
element.val(scope.minutes);
});
});
}
};
})

This is known behaviour. I've posted a bug report here:
https://github.com/angular/angular.js/issues/14987
The solution was to use $timeout to delay the setting of the element value, as suggested in the reply to the bug report.

Related

Using element.bind in validation directive

I trying to validate input with custom directive:
.directive('customValidation', function () {
return {
require: 'ngModel',
link: function (scope, element, attr, ngModelCtrl) {
function fromUser(text) {
element.bind("keydown keypress", function (event) {
if (!(event.keyCode >= 48 && event.keyCode <= 57)) {
return undefined;
}
})
}
ngModelCtrl.$parsers.push(fromUser);
}
};
});
but it doesn't work. Any character is passes validation. What I doing wrong?
So basically what you are trying to achieve is to check if an input contains only numbers. At least that is what I can understand from your explanation and the sample code.
First, you are using a parser, which are used for sanitizing and converting values passed from the DOM to the model. Validation comes afterwards. If you just want to check if only numbers are written, then you need something like this:
ngModel.$validators.validCharacters = function(modelValue, viewValue) {
var value = modelValue || viewValue;
return /[0-9]+/.test(value);
};
I suggest reading the API docs as they explain all ngModelController functionality very thoroughly: click here for thorough docs
Second, you are binding to a event everytime your parser is called. The parser is called each time you change the contents of your input element. If you type the word everytime into your input, you end up binding the event nine times! Apart from the fact that binding to the event after you changed something is too late as your first event was already fired.

Directive click getting called twice

I have the following directive:
mod.directive('mdCheckbox', function(){
var link = function(scope, element, attr, ngModel){
var inp = element.find('input');
scope.$watch(function(){
return ngModel.$viewValue;
}, function(newVal){
console.log('newVal: ' + newVal);
if(newVal){
inp.attr('checked', 'checked');
}else{
inp.removeAttr('checked');
}
});
scope.clickT = function(evt){
// ** why the need for stopPropagation ?
evt.stopPropagation();
console.log('click called');
if(inp.is(':checked')){
ngModel.$setViewValue(false);
console.log('set to false');
}else{
ngModel.$setViewValue(true);
console.log('set to true');
}
};
};
var directive = {
restrict: 'EA',
require: 'ngModel',
scope: {
text: '#mdText'
},
template: '<div ng-click="clickT($event)" class="checkbox"><input type="checkbox"/>' +
'<span>{{text}}</span></div>',
replace: true,
link: link
};
return directive;
});
and the html :
<md-checkbox md-text="Is Awesome" ng-model="user.isAwesome"></md-checkbox>
Now the clickT handler is called twice, and I have to stop propagation on it. But it's not clear to me why that happens. And the second problem is that even though this seems like it works, after a couple of clicks it stops working - the value doesn't change true/false anymore.
Here's a plunkr for testing
Thanks.
You shouldn't change the DOM manually because you can use data binding for that. But I won't go into that since it isn't the answer to question you asked.
Problem in your example is code that toggles Checking/Unchecking checkbox. It's not an attribute, it's a property(it can be true/false, not contain a value).
The checked attribute doesn't update the checked property after initial load. The attribute is in fact related to defaultChecked property.
Change it to:
if (newVal) {
inp.prop('checked', true);
} else {
inp.prop('checked', false);
}
Working Plnkr
Also, you can remove stopPropagation call.
There was something weird about the way you were adding and removing the checked attribute. Keep it simple and just set the input to true or false.
Working Demo
scope.clickT = function(){
console.log('click called');
if(inp){
ngModel.$setViewValue(false);
console.log('set to false');
}else{
ngModel.$setViewValue(true);
console.log('set to true');
}
};
It works if you query ngModel instead of input.
if (ngModel.$viewValue) ...
Although I'm not sure what this directive buys you...

Model not updating in Angular.js directive for bootstrap-multiselect

I have the following directive:
app.directive('scMultiselect', [function() {
return {
link: function(scope, element, attrs) {
element = $(element[0]);
element.multiselect({
enableFiltering: true,
// Replicate the native functionality on the elements so
// that Angular can handle the changes for us
onChange: function(optionElement, checked) {
optionElement.prop('selected', false);
if (checked)
optionElement.prop('selected', true);
element.change();
}
});
scope.$watch(function () {
return element[0].length;
}, function () {
element.multiselect('rebuild');
});
scope.$watch(attrs.ngModel, function() {
element.multiselect('refresh');
});
}
};
}]);
And the following element in my partial:
<select
id="level_teachers"
class="multiselect col-sm-10"
multiple="multiple"
ng-model="level.teachers"
ng-options="teacher.id as teacher.name for teacher in teachers"
sc-multiselect>
</select>
The bootstrap-multiselect control initializes and displays correctly, however when I select entries in it, my model (level.teachers) remains empty.
Had same problem and this worked for me :
First you add ngModel as 4th parameter of link function. Its very useful - more about it here: https://docs.angularjs.org/api/ng/type/ngModel.NgModelController
Then you basically have to add/delete 'by hand' the option in onChange method to/from your ngModel.
ngModel.$setViewValue() updates the value, ngModel.$render and scope.$apply() are making it visible and spread new model further :)
If you have only single selection then its much easier - less code because of no array control - just use $setViewValue(), $render() and $apply().
app.directive('scMultiselect', function() {
return {
require: 'ngModel',
link: function(scope, element, attrs, ngModel) {
element = $(element[0]);
element.multiselect({
enableFiltering: true,
onChange: function(optionElement, checked) {
optionElement.prop('selected', false);
var modelValue = ngModel.$modelValue; // current model value - array of selected items
var optionText = optionElement[0].text; // text of current option
var optionIndex = modelValue.indexOf(optionText);
if (checked) {
if ( optionIndex == -1) { // current option value is not in model - add it
modelValue.push(optionText)
}
optionElement.prop('selected', true);
} else if ( optionIndex > -1 ) { // if it is - delete it
modelValue.splice(optionIndex,1);
}
ngModel.$setViewValue(modelValue);
ngModel.$render();
scope.$apply();
}
});
scope.$watch(element[0].length, function () {
element.multiselect('rebuild');
});
}
};
});
Hope it will work for you too :)
IT's probably because Bootstrap components aren't built in a way that allow them to be used by Angularjs. They don't really have a way to update themselves after you instantiate them, and more importantly they don't participate in Angularjs's update process. Anyway the good news is someone has taken the initiative to rewrite those bootstrap components in Angularjs so we can use them.
http://angular-ui.github.io/bootstrap/
If you are using Bootstrap 3.x then get the latest. If you're stuck on 2.3.x v0.8 is the last version that supported 2.3.x

Combining ng-grid and ui-select2: unable to select a value

In the editableCellTemplate of an ng-grid, I wanted to use a more user-friendly dropdown, and I found ui-select2 to be a good one.
The thing is however, that any click on this component, when it is used inside the ng-grid, results in the cell juming back to non-editable mode.
This way I can't select another value using the mouse (I can using the arrows on my keyboard).
When I put the dropdown in the cellTemplate, it works, but it should also work in the *editable*CellTemplate.
Some code to see what I mean can be found here.
http://plnkr.co/edit/taPmlwLxZrF10jwwb1FX?p=preview
Does anyone know why that happens, and what could be done to work around it?
I updated the plunker with the solution (last grid shown).
http://plnkr.co/edit/taPmlwLxZrF10jwwb1FX?p=preview
The idea is that you should 'talk to ng-grid' exactly like the other editablCellTemplates do. These 'rules' aren't well defined in the documentation, but they could be deduced by looking at ng-grid's source code for the ng-input directive.
Basically, your own component should respond to the ngGridEventStartCellEdit event by focusing your editor element, and the most important thing is: your component MUST emit the ngGridEventEndCellEdit event only when the cell loses focus (or when you want the editor to disappear, like maybe when pressing enter or something).
So for this specific case I created my own directive that adds the necessary behaviour to a ui-select2 element, but I imagine you could understand what you'd have to do for your own specific case.
So, as an example, here is my ui-select2 specific directive:
app.directive(
'uiSelect2EditableCellTemplate',
[
function() {
return {
restrict: "A",
link: function ( scope, elm, attrs ) {
//make sure the id is set, so we can focus the ui-select2 by ID later on (because its ID will be generated from our id if we have one)
elm.attr( "id", "input-" + scope.col.index + "-" + scope.row.rowIndex );
elm.on( 'click', function( evt ) {
evt.stopPropagation();
} );
elm.on( 'mousedown', function( evt ) {
evt.stopPropagation();
} );
//select2 has its own blur event !
elm.on( 'select2-blur',
function ( event ) {
scope.$emit( 'ngGridEventEndCellEdit' );
}
);
scope.$on( 'ngGridEventStartCellEdit', function () {
//Event is fired BEFORE the new elements are part of the DOM, so try to set the focus after a timeout
setTimeout( function () {
$( "#s2id_" + elm[0].id ).select2( 'open' );
}, 10 );
} );
}
};
}
]
);
and my own editableCellTemplate would have to look something like this:
$scope.cellTemplateDropdownUiSelect3 =
'<select ui-select2-editable-cell-template ui-select2 ng-model="COL_FIELD" style="width: 90%" >' +
'<option ng-repeat="(fkid, fkrow) in fkvalues_country" value="{{fkid}}" ng-selected="COL_FIELD == fkid" >{{fkrow}} ({{fkid}})</option>' +
'</select>' ;
A little bit of 'official' information can be found here: https://github.com/angular-ui/ng-grid/blob/master/CHANGELOG.md#editing-cells
the template for datepicker
<input type="text" datepicker ng-model="COL_FIELD"/>
and the angular directive would look like this.
app.directive('datepicker',
function () {
return {
restrict: 'A',
require: '?ngModel',
scope: {},
link: function (scope, element, attrs, ngModel) {
if (!ngModel) return;
var optionsObj = {};
optionsObj.dateFormat = 'dd/mm/yy';
optionsObj.onClose = function(){
scope.$emit( 'ngGridEventEndCellEdit' );
}
var updateModel = function (date) {
scope.$apply(function () {
ngModel.$setViewValue(date);
});
};
optionsObj.onSelect = function (date, picker) {
updateModel(date);
};
ngModel.$render = function () {
element.datepicker('setDate', ngModel.$viewValue || '');
};
scope.$on('ngGridEventStartCellEdit', function (obj) {
element.datepicker( "show" );
});
scope.$on('ngGridEventStartCellEdit', function (obj) {
element.datepicker( "show" );
});
element.datepicker(optionsObj);
}
};
});

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>

Resources