ng-disabled doesn't update even if form validity is populated - angularjs

I created a directive that check if an input is valid based on some criteria. In this form I have a button that is ng-disabled="form.$invalid". The problem is that, even if it seems like the valid state is populated, my button is not enabled when my custom directive change the validity state of the input.
Here is a simple example:
<div ng-app="app">
<div ng-controller="fooController">
<form name="fooForm">
<input type="text" ng-model="foo" foo>
<input type="submit" value="send" ng-disabled="fooForm.$invalid">
</form>
</div>
</div>
JS (CoffeeScript):
app = angular.module 'app', []
app.directive 'foo', ->
restrict: 'A'
require: 'ngModel'
link: (scope, element, attrs, controller) ->
element.bind 'keyup', ->
if controller.$viewValue isnt 'foo'
controller.$setValidity 'foo', false
else
controller.$setValidity 'foo', true
app.controller 'fooController', ($scope) ->
$scope.foo = 'bar'
In short, this directive check if the input's value === 'foo'. If it's not it sets the validity 'foo' to false, otherwise to true.
Here is a jsfiddle (javascript) : http://jsfiddle.net/owwLwqbk/
I found a solution involving $apply: http://jsfiddle.net/owwLwqbk/1/
But I wonder if there's not an other, a better way of doing it? Isn't the state supposed to populate?

The jqLite event handler runs outside the context of Angular, that's why you needed the scope.$apply() before it would work.
Another option is to use a watch...
link: function(scope, element, attrs, controller) {
scope.$watch(function () {
return controller.$viewValue;
}, function (newValue) {
controller.$setValidity('foo', newValue === 'foo');
});
}
Fiddle

Please see demo below
var app;
app = angular.module('app', []);
app.directive('foo', function() {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, element, attrs, ctrl) {
ctrl.$parsers.unshift(function(val) {
console.log(val);
if (val == "bar") {
ctrl.$setValidity('foo', true);
} else {
ctrl.$setValidity('foo', false);
}
});
}
};
});
app.controller('fooController', function($scope) {
$scope.foo = 'bar';
});
.ng-invalid-foo {
outline: none;
border: 1px solid red;
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="app">
<div ng-controller="fooController">
[Only "bar" is valid value] <br/>
<form name="fooForm">
<input type="text" ng-model="foo" foo="">
<input type="submit" value="send" ng-disabled="fooForm.$invalid" />
</form>
</div>
</div>

Related

Deselectable radio not tracking previous value correctly

I'm trying to create a simple directive that I can apply to an existing radio input to make it deselectable...it doesn't quite work as the directive is not tracking the previously clicked value correctly. The goal is to have the radio set to 'true', 'false', or null if the currently selected value is the same as the previous (clicking true twice for example will unset the radio and set the model value to null). It seems to continually lose track of the previous value which gets set to undefined. I think I'm maybe making some kind of scoping issue but I'm not sure.
Here is the directive:
angular.module('App').directive('deselectableRadio', function() {
return {
restrict: 'A',
scope: {
model: '=ngModel'
},
link: function(scope, element, attr) {
var previousValue = angular.copy(scope.model);
element.bind('click', function(e) {
determineState(element[0]);
});
function determineState(elem) {
if (elem.checked && elem.value == previousValue) {
elem.checked = false;
scope.model = null;
}
previousValue = angular.copy(scope.model);
}
}
}
});
And here is the HTML:
<div class="app-radio">
<input type="radio" name="toggle" id="toggle-yes" value="true"
ng-model="props['toggle']" deselectable-radio>
<label for="toggle-yes">Yes</label>
</div>
<div class="app-radio">
<input type="radio" name="toggle" id="toggle-no" value="false"
ng-model="props['toggle']" deselectable-radio>
<label for="toggle-no">No</label>
</div>
The directive is fighting the ng-model controller.
Instead of creating an isolate scope and a two-way binding to the ng-model attribute, use the ngModelController API.
The DEMO
angular.module('app',[])
.directive('deselectableRadio', function() {
return {
restrict: 'A',
scope: false,
//scope: {
// model: '=ngModel'
//},
require: "ngModel",
link: function(scope, element, attrs, ctrl) {
element.on('click', function(e) {
if (attrs.value == ctrl.$viewValue) {
ctrl.$setViewValue(null);
ctrl.$render();
}
});
}
};
});
<script src="//unpkg.com/angular/angular.js"></script>
<body ng-app="app">
<div class="app-radio">
<input type="radio" name="toggle" id="toggle-yes" value="true"
ng-model="props['toggle']" deselectable-radio>
<label for="toggle-yes">Yes</label>
</div>
<div class="app-radio">
<input type="radio" name="toggle" id="toggle-no" value="false"
ng-model="props['toggle']" deselectable-radio>
<label for="toggle-no">No</label>
</div>
<br>
model = {{props.toggle || 'null'}}
</body>

AngularJS - input type="number" not clearable for non-number (NaN)

Clear number input type does not work for 'e' number
When I clear the input field with input eee in number type, it does not get cleared. Any other input numbers get cleared. Check the JSFiddle. Any hints would be appreciated.
http://jsfiddle.net/2ankx9up/
<div ng-app="app">
<div ng-controller="MainCtrl">
<input type="number" class="form-control" data-ng-model="searchAll">
</input>
<a class="clear" href="" data-ng-click="clearSearch()">X</a>
</div>
</div>
var app = angular.module("app", []);
app.controller("MainCtrl", function ($scope) {
$scope.searchAll = "";
$scope.clearSearch = function () {
$scope.searchAll = "";
};
});
The ng-model directive is unable to clear the content of an <input type="number"> element when that content parses to NaN (not a number). This can happen when a user pastes invalid content or simply types "eee".
One fix is to add a custom directive:
app.directive('clearNan', function() {
return {
require: 'ngModel',
link: function(scope, elem, attrs, ngModel) {
ngModel.$formatters.push(function(value) {
if (!value) elem.val(null);
return value;
});
}
};
})
Usage:
<input type="number" clear-nan ng-model="x" />
The DEMO
angular.module('numfmt-error-module', [])
.directive('clearNan', function() {
return {
require: 'ngModel',
link: function(scope, elem, attrs, ngModel) {
ngModel.$formatters.push(function(value) {
if (!value) elem.val(null);
return value;
});
}
};
})
.run(function($rootScope) {
$rootScope.typeOf = function(value) {
return typeof value;
};
})
<script src="//unpkg.com/angular/angular.js"></script>
<body ng-app="numfmt-error-module">
<input clear-nan type="number" ng-model="x" />
<br>
{{ x }} : {{ typeOf(x) }}
<br>
<button ng-click="x=''">Clear input</button>
</body>

Get access to form controller validation errors in a custom directive

I have a directive that wraps a form element with some inputs. One of the options is passing in a formName. Usually, with a form with the example name of myForm, to show an error you would do something like myForm.firstName.$error.required.
But, how do I get access to the errors when the form name is dynamically being passed in to the directive?
example usage
<my-custom-form formName='myForm' formSubmit='parentCtrl.foo()'></my-custom-form>
directive
angular.module('example')
.directive('myCustomForm', [
function() {
return {
restrict: 'E',
replace: true,
templateUrl: 'myCustomForm.directive.html',
scope: {
fornName: '#',
formSubmit: '&'
},
require: ['myCustomForm', 'form'],
link: function(scope, element, attrs, ctrls) {
var directiveCtrl = ctrls[0];
var formCtrl = ctrls[1];
scope.data = {};
scope.hasError = function(field) {
// how do i show the errors here?
};
scope.onSubmit = function() {
scope.formSubmit();
};
}
};
}]);
template
<form name="{{ formName }}" ng-submit="onSubmit()" novalidate>
<div class="form-group" ng-class="{'is-invalid': hasError('fullName') }">
<input type="text" name="fullName" ng-model="data.full_name" required />
<div ng-show="hasError('fullName')">
<p>How do I show this error?</p>
</div>
</div>
<div class="form-group" ng-class="{'is-invalid': hasError('email') }">
<input type="text" name="email" ng-model="data.email" ng-minlength="4" required />
<div ng-show="hasError('email')">
<p>How do I show this error?</p>
</div>
</div>
<button type="submit">Submit</button>
</form>
I think the only problem with your code is that the directive requires itself, I don't think that will work. Just removing the myCustomForm from the require works fine.
To check if the field has errors, you just need to check if the $error object in the form controller is empty.
require: ['form'],
link: function(scope, element, attrs, ctrls) {
var formCtrl = ctrls[0];
scope.data = {};
scope.hasError = function(field) {
// Field has errors if $error is not an empty object
return !angular.equals({}, formCtrl[field].$error);
};
Plunker

Validating form when directive creates form inputs dynamically

I am trying to validate form while using directive. The directive creates form inputs by compiling and replacing the element.
If I use simple template version of directive it works fine. If the input created by directive is invalid form is also invalid. It even works when created in a loop as well : http://codepen.io/kuasha/pen/gbreKP
<div ng-app="app" ng-controller="MainCtrl">
<form name="myform" ng-submit="sendForm()" novalidate>
<div class="form-group">
<myinput />
</div>
<div class="form-group">
<button ng-show="myform.$valid" type="submit">Submit!</button>
</div>
</form>
</div>
The app and controller:
var app = angular.module('app', []);
app.controller('MainCtrl', function ($scope) {
$scope.sendForm = function () {
if ($scope.myform.$valid) {
alert('form valid');
}
};
});
Here is the simple working version of the directive:
app.directive('myinput', function () {
return {
restrict: 'E',
template: '<label>Test</label><input ng-model="value" type="text" required ng-minlength="5" />'
};
});
But if I change the directive to use a link function and compile then, even when the input inside the directive is invalid, forms $valid is still true: http://codepen.io/kuasha/pen/qEZoKv?editors=101
app.directive('myinput', function ($compile) {
return {
restrict: 'E',
link: function (scope, element, attributes) {
var template = '<div><input type="text" required ng-minlength=5 /></div>'
var newElement = angular.element(template);
$compile(newElement)(scope);
element.replaceWith(newElement);
}
};
});
Edit:
Directive could create its own validator-
app.directive('myinput', function ($compile) {
return {
require:'^form',
restrict: 'E',
scope: {
},
controller: ['$scope', function($scope){
}],
link: function (scope, element, attributes, ctrl) {
var template = '<label>Test</label><input name="myctrl" ng-model="value" type="text" required ng-minlength="5" />'
var newElement = angular.element(template);
$compile(newElement)(scope);
element.replaceWith(newElement);
scope.$watch("value", function(){
ctrl.$setValidity('myctrl', scope.value && scope.value.length > 0);
console.log("Validity set");
});
}
};
});
But its probably not right approach for this particular problem.
You should switch the replaceWith and $compile (DEMO):
element.replaceWith(newElement);
$compile(newElement)(scope);
I think it is because you can't really compile an element that is not yet in the DOM. But I'm not sure.

ng-model is not bind with controller's $scope when I am creating a form dynamically using directive.

view code:- mydir is my custom directive
<div ng-model="vdmodel" mydir="dataValue">
</div>
my directive :-
app.directive('mydir',['$translate',function($translate){
return {
restrict: 'A',
transclude: true,
scope: {dir:'=mydir'},
compile: function(element, attrs) {
return function(scope, element, attrs, controller){
var setTemplate = '';
var setOpt = '';
if(scope.dir.itemtype== 'NUMBER'){
setTemplate = '<input type="number" class="form-control form-font ng-animate ng-dirty"';
setTemplate +='" ng-model="dir[somevalue]" value="'+scope.sizing.somevalue+'" >';
element.html(setTemplate);
}
}
}
}
});
There are many more form element in directive, but when I am trying to submit and collect value in my controller function I get nothing.
What I am doing wrong and what is the best way to collect form values ?
there are quiet a few changes that you will need to do
1.as you are using isolate scope, pass ngModel as well to the directive
scope: {dir:'=mydir', ngModel: '='},
2.as per the best practise ngModel must always have a dot
ng-model="params.vdmodel"
3.make sure to initialize the params object in controller
$scope.params = {}
Usually, a directive would share the same scope as the parent controller but since you are defining a scope in your directive, it sets up it's own isolate scope. Now since the controller and directive have their seperate scope, you need a way to share the data between them which is now done by using data: "=" in scope.
The app code
var myApp = angular.module('myApp', []);
myApp.controller('myController', function ($scope, $http) {
$scope.vdmodel = {};
})
.directive("mydir", function () {
return {
restrict: "A",
scope:{
data:"=model",
dir:'=mydir'
},
templateUrl: 'test/form.html'
};
});
The form.html
<form>
Name : <input type="text" ng-model="data.modelName" /><br><br>
Age : <input type="number" ng-model="data.modelAge" /><br><br>
Place : <input type="text" ng-model="data.modelPlace" /><br><br>
Gender:
<input type="radio" ng-model="data.modelGender" value="male"/>Male<br>
<input type="radio" ng-model="data.modelGender" value="female"/>Female<br><br><br>
</form>
The page.html
<div ng-app="myApp" >
<div ng-controller="myController" >
<div model="vdmodel" mydir="dataValue"></div>
<h3>Display:</h3>
<div>
<div>Name : {{myData.modelName}} </div><br>
<div>Age : {{myData.modelAge}}</div><br>
<div>Place : {{myData.modelPlace}}</div><br>
<div>Gender : {{myData.modelGender}}</div><br>
</div>
</div>
</div>
You have to use $compile service to compile a template and link with the current scope before put it into the element.
.directive('mydir', function($compile) {
return {
restrict: 'A',
transclude: true,
scope: {
dir: '=mydir'
},
link: function(scope, element, attrs, controller) {
var setTemplate = '';
var setOpt = '';
if (scope.dir.itemtype == 'NUMBER') {
setTemplate = '<input type="number" class="form-control form-font ng-animate ng-dirty"';
setTemplate += '" ng-model="dir.somevalue" value="' + scope.dir.somevalue + '" >';
element.html($compile(setTemplate)(scope));
}
}
}
});
See the plunker below for the full working example.
Plunker: http://plnkr.co/edit/7i9bYmd8blPNHch5jze4?p=preview

Resources