I'm trying to extend ngModel so that I have $empty flag that works even when the validation is failing. We're using it to show a "clear text" button.
The model override works fine, but I'm noticing that the flag changes don't apply immediately. Calling $digest throws an "in progress" error.
myApp.directive('ngModel', function() {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, element, attrs, ngModel) {
var checkEmpty = function() {
ngModel.$empty = (ngModel.$$rawModelValue === undefined ||
ngModel.$$rawModelValue.length === 0);
};
checkEmpty();
element.on('keyup', checkEmpty);
}
}
});
http://jsfiddle.net/sxg7nnwm/2/
You could just simply $watch for changes and set the variable:
scope.$watch(function(){
return ngModel.$isEmpty(ngModel.$viewValue);
},
function(v){
ngModel.$empty = v;
})
EDIT:
Of course, this is inefficient as it registers a $watch.
As correctly pointed out in comments, $viewChangeListeners do not fire for invalid entry. Binding to element events is also bad, since ng-model should be DOM-agnostic.
The approach that could work is to register a $parser that runs first. This would work in most cases - but does not guarantee to work in all, since any other directive is free to register their own parsers (at any time) ahead of our parser and invalidate the value before our parser has a chance to run and re-set $empty.
priority: -100,
require: "ngModel",
link: function(scope, element, attrs, ngModel) {
ngModel.$parsers.unshift(function(v) {
ngModel.$empty = isEmpty();
return v;
});
// needed for first time
var unwatch = scope.$watch(function(){
return isEmpty();
}, function(v){
ngModel.$empty = v;
unwatch();
});
function isEmpty(){
return ngModel.$isEmpty(ngModel.$viewValue);
}
}
Are you forced to use the keyup event? To avoid issues with digest timings you should hook into ngModel's pipeline by using $viewChangeListeners instead.
link: function(scope, element, attrs, ngModel) {
var checkEmpty = function() {
ngModel.$empty = (ngModel.$$rawModelValue === undefined || ngModel.$$rawModelValue.length === 0);
console.log(ngModel.$empty);
};
checkEmpty();
ngModel.$viewChangeListeners.push(checkEmpty);
// element.on('keyup', checkEmpty);
}
Fiddle: http://jsfiddle.net/mwup0s9j/1/
Well, I've found that $timeout works, it sets up my code in the next cycle:
element.on('keyup', function() {
$timeout(checkEmpty);
});
Related
I was developing a module where I need to create some text input manually (on enter or button clicking ) and auto focus on that input right after it's appended to the list. So far the function seems to work but when I open the console log, the $digest already in progress error appears. Kind of weird but if I remove some $eval or $apply the code won't work.
Here's my plnk demo for your reference: Demo
function keyEnter($document) {
return {
restrict: "A",
scope: false,
link: function(scope, ele, attrs) {
ele.bind("keydown keypress", function(event) {
if (event.which === 13) {
scope.$apply(function() {
scope.$eval(attrs.keyEnter);
});
event.preventDefault();
}
});
}
}
}
function customAutofocus() {
return {
restrict: 'A',
link: function(scope, element, attrs) {
scope.$watch(function() {
return scope.$eval(attrs.customAutofocus);
}, function(newValue) {
if (newValue === true) {
element[0].focus();
}
});
}
};
}
I followed the auto focus from this thread, it doesn't show any error even when I applied the same logic. The only difference is I'm using angular 1.3 while his is 1.2
What should I do to improve the code to avoid those $digest error ? Any help is really appreciate, thanks in advance
I adapted your plunk, so it works.
have a look at the new directive:
function customAutofocus($timeout) {
return {
restrict: 'A',
link: function(scope, element, attrs) {
//rember this gets run only only
//once just after creating the element, so I just neet to focus once, when
// this digest cycle is done!
$timeout(function() {
// use a timout to foucus outside this digest cycle!
element[0].focus(); //use focus function instead of autofocus attribute to avoid cross browser problem. And autofocus should only be used to mark an element to be focused when page loads.
}, 0);
}
};
}
This makes use of how angular works.
I'm building an application using AngularJS and UniformJS. I'd like to have a reset button on the view that would reset my select's to their default value. If I use uniform.js, it isn't working.
You can examine it here:
http://plnkr.co/edit/QYZRzlRf1qqAYgi8VbO6?p=preview
If you click the reset button continuously, nothing happens.
If you remove the attribute, therefore no longer using uniform.js, everything behaves correctly.
Thanks
UPDATE:
Required the use of timeout.
app.controller('MainCtrl', function($scope, $timeout) {
$scope.reset = function() {
$scope.test = "";
$timeout(jQuery.uniform.update, 0);
};
});
Found it. For the sake of completeness, I'm copying my comment here:
It looks like Uniform is really hacky. It covers up the actual select element, and displays span instead. Angular is working. The actual select element's value is changing, but the span that Uniform displays is not changing.
So you need to tell Uniform that your values have changed with jQuery.uniform.update. Uniform reads the value from the actual element to place in the span, and angular doesn't update the actual element until after the digest loop, so you need to wait a little bit before calling update:
app.controller('MainCtrl', function($scope, $timeout) {
$scope.reset = function() {
$scope.test = "";
$timeout(jQuery.uniform.update, 0);
};
});
Alternatively, you can put this in your directive:
app.directive('applyUniform',function($timeout){
return {
restrict:'A',
require: 'ngModel',
link: function(scope, element, attr, ngModel) {
element.uniform({useID: false});
scope.$watch(function() {return ngModel.$modelValue}, function() {
$timeout(jQuery.uniform.update, 0);
} );
}
};
});
Just a slightly different take on #john-tseng's answer. I didn't want to apply a new attribute to all my check-boxes as we had quite a few in the application already. This also gives you the option to opt out of applying uniform to certain check-boxes by applying the no-uniform attribute.
/*
* Used to make sure that uniform.js works with angular by calling it's update method when the angular model value updates.
*/
app.directive('input', function ($timeout) {
return {
restrict: 'E',
require: '?ngModel',
link: function (scope, element, attr, ngModel) {
if (attr.type === 'checkbox' && attr.ngModel && attr.noUniform === undefined) {
element.uniform({ useID: false });
scope.$watch(function () { return ngModel.$modelValue }, function () {
$timeout(jQuery.uniform.update, 0);
});
}
}
};
});
Please try blow code.
app.directive('applyUniform', function () {
return {
restrict: 'A',
link: function (scope, element, attrs) {
if (!element.parents(".checker").length) {
element.show().uniform();
// update selected item check mark
setTimeout(function () { $.uniform.update(); }, 300);
}
}
};
});
<input apply-uniform type="checkbox" ng-checked="vm.Message.Followers.indexOf(item.usrID) > -1" ng-click="vm.toggleSelection(item.usrID)" />
I have an ng form on a page. Inside the form I have several controls which need to display a save dialog when the form is dirty, ie form.$dirty = true. However there are some navigation controls in the form I don't want to dirty the form. Assume I can't move the control out of the form.
see: http://plnkr.co/edit/bfig4B
How do I make the select box not dirty the form?
Here's a version of #acacia's answer using a directive and not using $timeout. This will keep your controllers cleaner.
.directive('noDirtyCheck', function() {
// Interacting with input elements having this directive won't cause the
// form to be marked dirty.
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, elm, attrs, ctrl) {
ctrl.$pristine = false;
}
}
});
Then use it in your form like so:
<input type="text" name="foo" ng-model="x.foo" no-dirty-check>
I used #overthink's solution, but ran into the problem mentioned by #dmitankin. However, I didn't want to attach a handler to the focus event. So instead, I endeavored to override the $pristine property itself and force it to return false always. I ended up using Object.defineProperty which is not supported in IE8 and below. There are workarounds to do this in those legacy browsers, but I didn't need them, so they are not part of my solution below:
(function () {
angular
.module("myapp")
.directive("noDirtyCheck", noDirtyCheck);
function noDirtyCheck() {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, elem, attrs, ctrl) {
var alwaysFalse = {
get: function () { return false; },
set: function () { }
};
Object.defineProperty(ctrl, '$pristine', alwaysFalse);
Object.defineProperty(ctrl, '$dirty', alwaysFalse);
}
};
}
})();
I am also overriding $dirty so it can't be set as dirty either.
Setting the $pristine property to false, only when initializing, works until you call $setPristine() on the form. Then your control has its $pristine back to true and changing the input's value would make your form dirty.
To avoid that, set the $pristine on focus:
link: function(scope, elm, attrs, ctrl) {
elm.focus(function () {
ctrl.$pristine = false;
});
}
Angular only sets the form dirty if the control is pristine. So the trick here is to set $pristine on the control to false. You can do it in a timeout in the controller.
see: http://plnkr.co/edit/by3qTM
This is my final answer. Basically angular internally calls the $setDirty() function of the NgModelController when the input is interacted with, so just override that!
app.directive('noDirtyCheck', function() {
return {
restrict: 'A',
require: 'ngModel',
link: postLink
};
function postLink(scope, iElem, iAttrs, ngModelCtrl) {
ngModelCtrl.$setDirty = angular.noop;
}
})
A variation on #overthink's answer with some additional validation, and inline bracket notation to protect against minification.
"use strict";
angular.module("lantern").directive("noDirtyCheck", [function () {
return {
restrict: "A",
require: "ngModel",
link: function (scope, elem, attrs, ngModelCtrl) {
if (!ngModelCtrl) {
return;
}
var clean = (ngModelCtrl.$pristine && !ngModelCtrl.$dirty);
if (clean) {
ngModelCtrl.$pristine = false;
ngModelCtrl.$dirty = true;
}
}
};
}]);
I ran into some problems with that implementation, so here is mine (more complex):
app.directive('noDirtyCheck', [function () {
// Interacting with input elements having this directive won't cause the
// form to be marked dirty.
// http://stackoverflow.com/questions/17089090/prevent-input-from-setting-form-dirty-angularjs
return {
restrict: 'A',
require: ['^form', '^ngModel'],
link: function (scope, element, attrs, controllers) {
var form = controllers[0];
var currentControl = controllers[1];
var formDirtyState = false;
var manualFocus = false;
element.bind('focus',function () {
manualFocus = true;
if (form) {
window.console && console.log('Saving current form ' + form.$name + ' dirty status: ' + form.$dirty);
formDirtyState = form.$dirty; // save form's dirty state
}
});
element.bind('blur', function () {
if (currentControl) {
window.console && console.log('Resetting current control (' + currentControl.$name + ') dirty status to false (called from blur)');
currentControl.$dirty = false; // Remove dirty state but keep the value
if (!formDirtyState && form && manualFocus) {
window.console && console.log('Resetting ' + form.$name + ' form pristine state...');
form.$setPristine();
}
manualFocus = false;
// scope.$apply();
}
});
}
};
}]);
I'm writing a custom directive to validate some value in the scope. It should work like the required attribute, but instead of validating the input text, it's going to validate a value in the scope. My problem is that this value is set in a $scope.$watch function and this function runs after my directive. So when my directive tries to validate the value it has not been set yet. Is it possible to run the $watch code before running my custom directive?
Here is the code:
var app = angular.module('angularjs-starter', []);
app.controller('MainCtrl', function($scope) {
var keys = {
a: {},
b: {}
};
$scope.data = {};
// I need to execute this before the directive below
$scope.$watch('data.objectId', function(newValue) {
$scope.data.object = keys[newValue];
});
});
app.directive('requiredAttribute', function (){
return {
require: 'ngModel',
link: function(scope, elem, attr, ngModel) {
var requiredAttribute = attr.requiredAttribute;
ngModel.$parsers.unshift(function (value) {
ngModel.$setValidity('requiredAttribute', scope[attr.requiredAttribute] != null);
return value;
});
}
};
});
<input type="text" name="objectId" ng-model="data.objectId" required-attribute="object" />
<span class="invalid" ng-show="myForm.objectId.$error.requiredAttribute">Key "{{data.objectId}}" not found</span>
And here is a plunker: http://plnkr.co/edit/S2NrYj2AbxPqDrl5C8kQ?p=preview
Thanks.
You can schedule the $watch to happen before the directive link function directly. You need to change your link function.
link: function(scope, elem, attr, ngModel) {
var unwatch = scope.$watch(attr.requiredAttribute, function(requiredAttrValue) {
if (requiredAttribute=== undefined) return;
unwatch();
ngModel.$parsers.unshift(function (value) {
ngModel.$setValidity('requiredAttribute', requiredAttrValue != null);
return value;
});
});
}
This approach will activate the $watch function inside the directive only once and will remove the watcher the first time your required scope variable is set.
There is also another approach where you parse the value and check it this way:
link: function(scope, elem, attr, ngModel) {
var parsedAttr = $parse(attr.requiredAttribute);
ngModel.$parsers.unshift(function (value) {
ngModel.$setValidity('requiredAttribute', parsedAttr(scope) != null);
return value;
});
}
Here you will need to use $parse AngularJS service. The difference here is that this will mark the input field as invalid without waiting for first value set on the required scope variable.
Both variants allow you to pass an expression instead of a simple variable name. This makes it possible to write something as required-attribute="object.var1.var2".
It really depends on what you need.
I've defined a directive like so:
angular.module('MyModule', [])
.directive('datePicker', function($filter) {
return {
require: 'ngModel',
link: function(scope, elem, attrs, ctrl) {
ctrl.$formatters.unshift(function(modelValue) {
console.log('formatting',modelValue,scope,elem,attrs,ctrl);
return $filter('date')(modelValue, 'MM/dd/yyyy');
});
ctrl.$parsers.unshift(function(viewValue) {
console.log('parsing',viewValue);
var date = new Date(viewValue);
return isNaN(date) ? '' : date;
});
}
}
});
The parser seems to fire every time I type a key in my textbox though -- what exactly is the default event, is it keyup, or input? And how do I change it to only fire onchange? It really isn't necessary to fire anymore often than that.
Furthermore, I'm actually manipulating the content of this input using jQuery UI's datepicker. When clicking on the calendar it doesn't seem to trigger the appropriate event that causes the model to be updated/parser to be triggered. I think I can force an event to be fired but I need to know which one.
Trying to use scope.$apply() but that doesn't seem to help any:
.directive('datepicker', function($filter) {
return {
require: 'ngModel',
link: function(scope, elem, attrs, ctrl) {
$(elem).datepicker({
onSelect: function(dateText, inst) {
console.log(dateText, inst);
scope.$apply();
}
});
ctrl.$formatters.unshift(function(modelValue) {
console.log('formatting',modelValue);
return $filter('date')(modelValue, attrs.datePicker || 'MM/dd/yyyy');
});
ctrl.$parsers.unshift(function(viewValue) {
console.log('parsing',viewValue);
return new Date(viewValue);
});
}
}
})
I don't think the solution given here works for me because (a) I want to use the datepicker attribute value for choosing a date format or other options, but more importantly, (b) it seems to be passing back a string to the model when I want an actual date object... so some form of parsing has to be done and applied to the ng-model.
Here I created a mo-change-proxy directive, It works with ng-model and it updates proxy variable only on change.
In this demo I have even included improved directive for date-input. Have a look.
Demo: http://plnkr.co/edit/DBs4jX9alyCZXt3LaLnF?p=preview
angModule.directive('moChangeProxy', function ($parse) {
return {
require:'^ngModel',
restrict:'A',
link:function (scope, elm, attrs, ctrl) {
var proxyExp = attrs.moChangeProxy;
var modelExp = attrs.ngModel;
scope.$watch(proxyExp, function (nVal) {
if (nVal != ctrl.$modelValue)
$parse(modelExp).assign(scope, nVal);
});
elm.bind('blur', function () {
var proxyVal = scope.$eval(proxyExp);
if(ctrl.$modelValue != proxyVal) {
scope.$apply(function(){
$parse(proxyExp).assign(scope, ctrl.$modelValue);
});
}
});
}
};
});