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();
}
});
}
};
}]);
Related
Need help with several issues. Here is simplified code:
HTML
<input type="text" title="{{e.Name}}" ng-model="e.modelName" ng-required="true" typeahead-editable="false" ng-blur="vm.isUnchanged(i)" focus-me="vm.Event"
own-typeahead typeahead-on-select="vm.changeValue($item, $model, $label, i)"
uib-typeahead="$Event, $viewValue)" typeahead-min-length="0"/>
JS
app.directive("ownTypeahead", function() {
var directive = {
link: link,
scope: true,
restrict: 'A',
require: ["ngModel"]
};
return directive;
function link(scope, element, attrs, ctrls) {
element.bind('click', function () {
if (ctrls[0].$viewValue && ctrls[0].$viewValue == ' ') {
ctrls[0].$setViewValue('');
}
ctrls[0].$setViewValue(' ');
});
element.bind('focus', function () {
if (!ctrls[0].$viewValue || ctrls[0].$viewValue == '') {
ctrls[0].$setViewValue(' ');
}
});
}
});
/**
* Directive that places focus on the element it is applied to when the
* expression it binds to evaluates to true
*/
app.directive('focusMe', ['$timeout', function focusMe($timeout) {
return function (scope, elem, attrs) {
scope.$watch(attrs.focusMe, function (newVal) {
if (newVal) {
$timeout(function () {
elem[0].focus();
}, 0, false);
}
});
};
}]);
The problems/questions are:
1) The main one. Focus after clicking on certain items triggers typeahead ddl in the input field almost always, but there are several items that move focus but don't trigger the list to be opened. Any ideas where is the issue? (the code above works in about 90% of the cases and in 100% with a click on input field)
2) Not ideal solution, but an ok workaround could be trigger a click event on the focused input field to open the list. Can't manage to get that right the angular way. How this could be done?
Got it working by adding a 200ms timeout for the focusMe:
app.directive('focusMe', ['$timeout', function focusMe($timeout) {
return function (scope, elem, attrs) {
scope.$watch(attrs.focusMe, function (newVal) {
if (newVal) {
$timeout(function () {
elem[0].focus();
}, 200, false);
}
});
};
}]);
If anyone has better suggestions would accept the answer.
I have created a complex form element to avoid code duplication.
However I can't make it to behave same as normal input field.
HTML
<input name="first" ng-model="ctrl.first" type="text" required is-number />
<complex-input name="second" ng-model="ctrl.second" required></complex-input>
JS/ng
// main input directive
app.directive("complexInput", function(){
return {
require: "^?ngModel",
scope: {
passedModel: '=ngModel'
},
template: "<div><input ng-model='passedModel' is-number type='text' child-element /></div>",
link: function(scope, elem, attr, modelCtrl) {
angular.extend(modelCtrl.$validators, scope.childValidators || {});
}
}
});
// is number validator
app.directive('isNumber', function(){
return {
require: "^?ngModel",
link: function(scope, elem, attr, modelCtrl) {
modelCtrl.$validators.isNumber = function (modelValue, viewValue) {
var value = modelValue || viewValue;
return !isNaN(value);
};
}
}
});
// hacky directive to pass back validators from child field
app.directive('childElement', function(){
return {
require: "^?ngModel",
priority: 10,
link: function(scope, elem, attr, modelCtrl) {
if (!modelCtrl) return;
scope.childValidators = modelCtrl.$validators;
}
}
});
When I run it content of both fields errors is following.
On init:
First: {"required":true}
Second: {"required":true}
If I enter string:
First: {"isNumber":true}
Second: {**"required":true**,"isNumber":true}
If I enter number:
First: {}
Second: {}
I would expect both input and complex-inputto behave same. Problem is obviously that is-number validation on inner input is blocking model on outer complex-input so it's value is not set, unless you enter number.
What am I doing wrong?
Is there a nicer/cleaner way to do this and possibly avoid the ugly childElement directive?
Please find test plnkr here: https://plnkr.co/edit/Flw03Je1O45wpY0wf8om
UPDATE: Complex input is not a simple wrapper for input. In reality in can have multiple inputs that together compile a single value.
You can solve both problems (the childElement and the correct validation) by letting complex-element be only a wrapper around the real input field.
To do this :
The complex-element directive has to use something else than name, for example "input-name"
The input in the complex-element directive template need to use that name
You need to pass from complex-element to the input field whatever else you need (validations, events etc..)
For example, the following is your code modified and works as you expect :
var app = angular.module("myApp", []);
app.controller("MyCtrl", function($scope){
var vm = this;
vm.first = "";
vm.second = "";
});
app.directive("complexInput", function(){
return {
require: "^?ngModel",
scope: {
passedModel: '=ngModel',
name: '#inputName'
},
template: "<div><input ng-model='passedModel' name='{{name}}' is-number type='text'/ required></div>",
link: function(scope, elem, attr, modelCtrl) {
angular.extend(modelCtrl.$validators, scope.childValidators || {});
}
}
});
app.directive('isNumber', function(){
return {
require: "^?ngModel",
link: function(scope, elem, attr, modelCtrl) {
modelCtrl.$validators.isNumber = function (modelValue, viewValue) {
var value = modelValue || viewValue;
return !isNaN(value);
};
}
}
});
HTML
<p>COMPLEX:<br/><complex-input required input-name="second" ng-model="ctrl.second"></complex-input></p>
See the plunker here : https://plnkr.co/edit/R8rJr53Cdo2kWAA7zwJA
Solution was to add ng-model-options='{ allowInvalid: true }' on inner input.
This forces inner input to update model even if it's invalid.
However more nicer solution would be to pass entire model from child elements to parent directive and then iterate through their $validators.
app.directive('childElement', function(){
return {
require: "^?ngModel",
priority: 10,
link: function(scope, elem, attr, modelCtrl) {
if (!modelCtrl) return;
scope.childModels.push(modelCtrl);
}
}
});
Complex input here has two inputs that combined give a final value.
First one has to be number but it's not required, while the second one is required.
app.directive("complexInput", function(){
function preLink(scope, elem, attr, modelCtrl) {
// create local scope value
scope.inner1 = "";
scope.inner2 = "";
scope.childModels = [];
}
// do some nice mapping of inner errors
function mapKeys(index, validator) {
return validator + "." + index;
}
function postLink(scope, elem, attr, modelCtrl) {
// copy value on change to passedModel
// could be complex state
scope.$watch('inner1', function(value){
scope.passedModel = scope.inner1 + scope.inner2;
});
scope.$watch('inner2', function(value){
scope.passedModel = scope.inner1 + scope.inner2;
});
scope.childModels.forEach(function(childModel, index){
childModel.$viewChangeListeners.push(function(){
Object.keys(childModel.$validators).forEach(function(validatorKey){
modelCtrl.$setValidity(mapKeys(index, validatorKey), !childModel.$error[validatorKey]);
});
});
});
}
return {
require: "^?ngModel",
scope: {
passedModel: '=ngModel'
},
template: "<div><input ng-model='inner1' is-number type='text' child-element /><br/><input ng-model='inner2' required type='text' child-element /></div>",
compile: function(element, attributes){
return { pre: preLink, post: postLink };
}
}
});
I built a toggle switch using css where clicking on the control simply adds and removes a CSS class which causes the toggle switch to animate. I want to get this toggle switch in a form with validation and specifically want to set the control to $dirty = true whenever they click on the control.
I set up my directive by setting require: 'ngModel' and then settings the controller in the link function to dirty like this:
link: function(scope, elem, attrs, ctrl){
var hasCallback = angular.isDefined(attrs.callback);
scope.toggle = function(e){
if(hasCallback){
scope.skcallback({event:e});
}else{
scope.ngModel = !scope.ngModel;
}
//ctrl.$setDirty(); // This didn't work and didn't even appear to be on the ctrl object
ctrl.$dirty = true;
ctrl.$pristine = false;
}
scope.$watch('disabled', function(value){
if(value){
elem.addClass('disabled');
}else{
elem.removeClass('disabled');
}
})
}
I tried a number of different things to get the validation to work but when I inspect the DOM the control is always set to ng-pristine and doesn't appear to ever get set to dirty. What am I doing wrong?
The solution is to create a custom validator directive (tested with Angular 1.3.8) :
mod.directive('cvDirtySet', function () {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, elem, attrs, ctrl) {
var more = false;
ctrl.$validators.dummy = function (modelValue, viewValue) {
if (more) {
ctrl.$setDirty();
}
more = true;
return true;
};
}
};
});
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'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);
});
}
});
}
};
});