Why does the ngModelCtrl.$valid not update? - angularjs

I'm trying to create a directive that contains an inputfield with a ng-model and knows if the inputcontrol is valid. (I want to change a class on a label within the directive based on this state.) I want to use the ngModelController.$valid to check this, but it always returns true.
formcontroller.$valid or formcontroller.inputfieldname.$valid do work as exprected, but since im trying to build a reusable component using a formcontroller is not very handy because then i have to determine what field of the form corresponds with the current directive.
I dont understand why one works and one doesnt, because in de angular source it seems to be the same code that should manage these states: The ngModelController.$setValidity function.
I created a test directive that contains a numeric field with required and a min value. As you can see in the fiddle below, the model controller is only triggered during page load and after that never changes.
jsfiddle with example directive
Directive code:
angular.module('ui.directives', []).directive('textboxValid',
function() {
return {
restrict: 'E',
require: ['ngModel', '^form'],
scope: {
ngModel: '='
},
template: '<input type="number" required name="somefield" min="3" ng-model="ngModel" /> '+
'<br>Form controller $valid: {{formfieldvalid}} <br> ' +
'Model controller $valid: {{modelvalid}} <br>'+
'Form controller $valid: {{formvalid}} <br>',
link: function (scope, element, attrs, controllers) {
var ngModelCtrl = controllers[0];
var formCtrl = controllers[1];
function modelvalid(){
return ngModelCtrl.$valid;
}
function formvalid(){
return formCtrl.$valid;
}
scope.$watch(formvalid, function(newVal,oldVal)
{
scope.modelvalid = ngModelCtrl.$valid;
scope.formvalid = formCtrl.$valid;
scope.formfieldvalid = formCtrl.somefield.$valid;
});
scope.$watch(modelvalid, function(newVal,oldVal)
{
scope.modelvalid = ngModelCtrl.$valid;
scope.formvalid = formCtrl.$valid;
scope.formfieldvalid = formCtrl.somefield.$valid;
//This one only gets triggered on pageload
alert('modelvalid ' + newVal );
});
}
};
}
);
Can someone help me understand this behaviour?

I think because you're watching a function and the $watch is only execute when this function is called !!
Watch the model instead like that :
scope.$watch('ngModel', function(newVal,oldVal)
{
scope.modelvalid = ngModelCtrl.$valid;
scope.formvalid = formCtrl.$valid;
scope.formfieldvalid = formCtrl.somefield.$valid;
//This one triggered each time the model changes
alert('modelvalid ' + ngModelCtrl.$valid );
});

I figured it out..
The textboxValid directive has a ng-model directive, and so does the input that gets created by the directive template. However, these are two different directives, both with their own seperate controller.
So, i changed my solution to use an attribute directive like below. This works as expected.
.directive('attributetest',
function() {
return {
restrict: 'A',
require: 'ngModel',
scope: {
ngModel: '='
},
link: function (scope, element, attrs, ngModelCtrl) {
function modelvalid(){
return ngModelCtrl.$valid;
}
scope.$watch(modelvalid, function(newVal,oldVal){
console.log('scope.modelvalid = ' + ngModelCtrl.$valid );
});
}
};
});

Related

Angular filter not working

I am coding a filter that will format phone numbers in a contact form I've built however for some reason the value in the input is never being updated and I'm not sure what I'm doing wrong.
Here's my HTML:
<div class='form-group'>
<input name='phone' ng-model='home.contact.phone' placeholder='(Area code) Phone' required ng-bind='home.contact.phone | phone' />
</div>
and here's my filter:
(function () {
'use strict'
angular
.module('Allay.phoneFilter', [])
.filter('phone', function () {
return function (phone) {
if(!phone) return '';
var res = phone + '::' // Since this isn't working, I'm doing something super simple, adding a double colon to the end of the phone number.
return res;
}
});
})();
I'm not sure if you need this, but here's the controller:
(function () {
'use strict'
angular
.module('Allay', [
'Allay.phoneFilter'
])
.controller('HomeController', function () {
var home = this;
});
})();
If I add an alert(res) before 'return res' in the filter I see the value I expect '123::', however the value in the input it's self is still just 123.
You need create directive to change your ngModel, like this:
.directive('phoneFormat', function() {
return {
require: 'ngModel',
link: function(scope, elem, attrs, ctrl) {
var setvalue = function() {
elem.val(ctrl.$modelValue + "::");
};
ctrl.$parsers.push(function(v) {
return v.replace(/::/, '');
})
ctrl.$render = function() {
setvalue();
}
elem.bind('change', function() {
setvalue();
})
}
};
});
Use in html:
<input name='phone' ng-model='contact.phone' placeholder='(Area code) Phone' required phone-format />
JS Fiddle: http://jsfiddle.net/57czd36L/1/
Your usage of ngBind on the input is not quite correct. From the documentation,
The ngBind attribute tells Angular to replace the text content of the specified HTML element with the value of a given expression, and to update the text content when the value of that expression changes
You do not need to replace the text content of the <input> element, that wouldn't make sense. You can instead extend the formatter pipeline of the NgModelController using a directive like
app.directive('phoneFormat', function () {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attrs, ngModel) {
ngModel.$formatters.push(function (value) {
if (value)
return value + '::';
});
}
}
});
Then, in your HTML,
<input ng-model='home.contact.phone' phone-format />
In case you wanted to keep the filter you wrote (for other usages), you can actually re-use it in the directive like
app.directive('phoneFormat', [ '$filter', function ($filter) {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attrs, ngModel) {
ngModel.$formatters.push($filter('phone'));
}
}
}]);
$filter('phone') simply returns the filter function registered under 'phone'. Here is a Plunker.
Note, this solution will only format data when you change the $modelValue of the NgModelController, for example like
$scope.$apply('home.contact.phone = "123-456-7890"');
If you are looking for something to update/format the value of the input as the user is typing, this is a more complicated task. I recommend using something like angular-ui/ui-mask.
Although a filter module is a good approach, I use an 'A' directive to do the dirty work because changing the element value will affect its ng-model.
However, I would only suggest this kind of solution if your actual data manipulation could sum in 3-4 lines of code; otherwise, a more thorough approach is needed.
This is an example that will delete anything which isn't an integer:
(function () {
'use strict'
angular.module('Allay').directive('phoneValidator', function () {
return {
restrict: 'A',
link: function(scope, element, attrs) {
angular.element(element).on('keyup', function() {
element.val(element.val().replace(/[^0-9\.]/, ''));
});
}
}
});
})();
And than in your HTML template :
<input name="phone" ng-model="home.contact.phone" placeholder="(Area code) Phone" phoneValidator required/>`
You should remove your "ng-bind" cause you are filtering it and what is presented is what in the ng-model. use value instead.
<input name='phone' ng-model='home.contact.phone | phone' value="{{contact.phone | phone}}" />
see working example: JsFiddle

Change model binding in Angular JS dynamically using a directive?

Is it possible to change the binding expression on an ng-model directive dynamically?
This is what I'am trying to accomplish:
I have an input field that initially should propose a value (based on a value that the user entered some time ago -> "repetitions.lastTime"). This value should be bound initially. Then, if the user clicks on the input field, the proposed value should be copied to another property ("repetitions.current") on the scope. From now on, the input field should be bound to "repetitions.current".
Edit Plunker: http://plnkr.co/edit/9EbtnEYoJccr02KYUzBN?p=preview
HTML
<mo-repetitions mo-last-time="repetitions.lastTime" mo-current="repetitions.current"></mo-repetitions>
<p>current: {{repetitions.current}}</p>
<p>last time: {{repetitions.lastTime}}</p>
JavaScript
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
$scope.repetitions = {
lastTime : 5,
current : 0
};
});
app.directive('moRepetitions', [function () {
return {
restrict: 'E',
scope: {
moLastTime : "=",
moCurrent : "="
},
link: function (scope, element, attrs, ngModel) {
element.css("color", "gray");
scope.activated = false;
scope.activate = function () {
if (scope.activated)
return;
else
scope.activated = true;
element.css("color", "black");
scope.moCurrent = scope.moLastTime;
//This is not working, because it apparently comes too late:
attrs['ngModel'] = 'moCurrent';
};
},
template: '<input ng-model="moLastTime" ng-click="activate()" type="number" />',
replace: true
};
}]);
Can someone point me on the right track?
Created a plnkr
element.attr('ng-model', 'moCurrent');
$compile(element)(scope);

AngularJS Accessing Aliased Controller Attribute From Directive

OK this is a contrived example, but...
Say I have a controller like this:
app.controller('TestCtrl', function() {
this.testString;
this.otherString;
});
And I have a template like this:
<div ng-controller='TestCtrl as test'>
<input demo type='text' ng-model='test.testString'>
{{test.otherString}}
</div>
And then I have a directive like this:
app.directive('demo', function() {
return {
require:'ngModel',
link: function(scope, elem, attrs, ctrl) {
scope.$watch(attrs.ngModel, function(newVal) {
/* How do I get otherString without knowing the controller alias?
This works but is not good practice */
scope.test.otherString = newVal + ' is cool!';
/* This doesn't work, but would if the property was in scope
instead of the controller */
scope[attrs.demo] = newVal + ' is cool!';
});
}
}
});
How do I get otherString without knowing the controller alias? I could just break apart attrs.ngModel to get it, but is there an 'angular' way to get the property?
EDIT
While this example didn't exactly reflect the issues I was having with my real scenario, I did find out how to get the controller's property in the link function, allowing me to update the model:
link: function(scope, elem, attrs, ctrl) {
var otherString = scope.$eval(attrs.demo);
scope.$watch(attrs.ngModel, function(newVal) {
otherString = newVal + ' is cool!';
}
}
A directive should have zero knowledge of anything outside of itself. If the directive depends on an outside controller having defined some arbitrary property, things will break very easily.
Defining a "scope" property on the directive lets you expose an explicit API for binding data to the directive.
myModule.directive('demo', function() {
return {
scope: {
demoString: '=demo',
},
link: function(scope, element, attrs) {
// You can access demoString here, or in a directive controller.
console.log(scope.demoString);
}
};
});
And the template
<div ng-controller='TestCtrl as test'>
<input demo="test.otherString" ng-model='test.testString'>
{{test.otherString}}
</div>
This isn't the only way to facilitate passing data or setting up bindings to a directive, but it is the most common way and should cover the majority of use-cases.
If you are trying to be more angular-like I would just use the $scope in the controller and pass that to the directive like so:
app.directive('demo', function() {
return {
scope: {strings: '='},
link: function(scope, elem, attrs, ctrl) {
scope.$watch('strings.test', function(newVal) {
/* How do I get otherString without knowing the controller alias? */
scope.strings.other = newVal + ' is cool!';
});
}
}
});
then in the html:
<div ng-controller='TestCtrl as test'>
<input demo type='text' strings="strings" ng-model="strings.test" />
{{strings.other}}
</div>
In the controller you would assign:
$scope.strings = {
test: '',
other: ''
}

How to create a angular input directive that works with form validation

How do I create an angular directive that adds a input in a form but also works with form validation.
For instance the following directive creates a text input:
var template = '\
<div class="control-group" ng-class="{ \'error\': {{formName}}[\'{{fieldName}}\'].$invalid}">\
<label for="{{formName}}-{{fieldName}} class="control-label">{{label}}</label>\
<div class="controls">\
<input id="{{formName}}-{{fieldName}}" name="{{fieldName}}" type="text" ng-model="model" />\
</div>\
</div>\
';
angular
.module('common')
.directive('formTextField', ['$compile', function ($compile) {
return {
replace: true,
restrict: 'E',
scope: {
model: '=iwModel',
formName: '#iwFormName',
fieldName: '#iwFieldName',
label: '#iwLabel'
},
transclude: false,
template: template
};
}])
;
However the input is not added to the form variable ($scope.formName). I've been able to work around that by using the dynamicName directive from the following stackoverflow question Angularjs: form validation and input directive but ng-class will still not work.
Update
It now seems to be working but it feels like a hack. I thought I needed the scope to grab the form and field names however I can read that directly from the attributes. Doing this shows the field in the controllers scope form variable. However the ng-class attribute does not apply. The only way around this is to add the html element a second time once the scope is available.
jsFiddle here
var template = '\
<div class="control-group">\
<label class="control-label"></label>\
<div class="controls">\
<input class="input-xlarge" type="text" />\
</div>\
</div>\
';
angular
.module('common')
.directive('formTextField', ['$compile', function ($compile) {
return {
replace: true,
restrict: 'E',
scope: false,
compile: function compile(tElement, tAttrs, transclude) {
var elem = $(template);
var formName = tAttrs.iwFormName;
var fieldName = tAttrs.iwFieldName;
var label = tAttrs.iwLabel;
var model = tAttrs.iwModel;
elem.attr('ng-class', '{ \'error\': ' + formName + '[\'' + fieldName + '\'].$invalid }');
elem.find('label').attr('for', formName + '-' + fieldName);
elem.find('label').html(label);
elem.find('input').attr('id', formName + '-' + fieldName);
elem.find('input').attr('name', fieldName);
elem.find('input').attr('ng-model', model);
// This one is required so that angular adds the input to the controllers form scope variable
tElement.replaceWith(elem);
return {
pre: function preLink(scope, iElement, iAttrs, controller) {
// This one is required for ng-class to apply correctly
elem.replaceWith($compile(elem)(scope));
}
};
}
};
}])
;
when I do something like this, I use the directive compile function to build my html prior to it being processed. For example:
myApp.directive('specialInput', ['$compile', function($compile){
return {
// create child scope for control
scope: true,
compile: function(elem, attrs) {
// use this area to build your desired dom object tree using angular.element (eg:)
var input = angular.element('<input/>');
// Once you have built and setup your html, replace the provided directive element
elem.replaceWith(input);
// Also note, that if you need the regular link function,
// you can return one from here like so (although optional)
return function(scope, elem, attrs) {
// do link function stuff here
};
}
};
}]);

Compile custom directives

I'm trying to add dynamically a custom validation directive inside other custom directive. It works fine for system angular directive like "required", but not work for custom validate directive.
I have directive 'controlInput' with input, on which i dynamically add directive 'testValidation' (in real application in dependance from data of control-input).
<control-input control-data='var1'></control-input>
Directives:
app.directive('controlInput', function ($compile) {
return {
restrict: 'E',
replace: true,
template: '<div><input type="text" ng-model="var1"></div>',
link: function (scope, elem, attrs) {
var input = elem.find('input');
input.attr('required', true);
input.attr('test-validation', true);
$compile(elem.contents())(scope);
}
};
});
app.directive('testValidation', function () {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attrs, ctrl) {
ctrl.$parsers.unshift(function (value) {
if (value) {
var valid = value.match(/^test$/);
ctrl.$setValidity('invalidTest', valid);
}
return valid ? value : undefined;
});
}
};
});
Full example http://plnkr.co/edit/FylMfTugHrotEMSQyTfT?p=preview
In this example I also add simple input to be sure 'testValidation' directive is working.
Thanks for any answers!
EDIT:
I suggest you fix your original program by changing the template in the controlInput directive to:
template: '<div><input type="text" testdir required ng-model="var1"></div>'
I don't see why not do it as mentioned above, but another way would be to replace the input with a new compiled one:
input.replaceWith($compile(elem.html())(scope));
NOTE:
Change
var valid = value.match(/^test$/);
To
var valid = /^test$/.test(value);
From MDN:
String.prototype.match()
Return value
array An Array containing the matched results or null if there were no
matches.
RegExp.prototype.test() returns what you need, a boolean value.

Resources