How to use the last valid modelValue if a model becomes invalid? - angularjs

I'm working on an application that saves changes automatically when the user changes something, for example the value of an input field. I have written a autosave directive that is added to all form fields that should trigger save events automatically.
template:
<input ng-model="fooCtrl.name" autosave>
<input ng-model="fooCtrl.email" autosave>
directive:
.directive('autosave', ['$parse', function ($parse) {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attrs, ngModel) {
function saveIfModelChanged () {
// save object containing name and email to server ...
}
ngModel.$viewChangeListeners.push(function () {
saveIfModelChanged();
});
}
};
}]);
So far, this all works fine for me. However, when I add validation into the mix, for example validating the input field to be a valid email address, the modelValue is set to undefined as soon as the viewValue is changed to an invalid email address.
What I would like to do is this: Remember the last valid modelValue and use this when autosaving. If the user changes the email address to be invalid, the object containing name and email should still be saved to the server. Using the current valid name and the last valid email.
I started out by saving the last valid modelValue like this:
template with validation added:
<input type="email" ng-model="fooCtrl.name" autosave required>
<input ng-model="fooCtrl.email" autosave required>
directive with saving lastModelValue:
.directive('autosave', ['$parse', function ($parse) {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attrs, ngModel) {
var lastModelValue;
function saveIfModelChanged () {
// remeber last valid modelValue
if (ngModel.$valid) {
lastModelValue = ngModel.$modelValue;
}
// save object containing current or last valid
// name and email to server ...
}
ngModel.$viewChangeListeners.push(function () {
saveIfModelChanged();
});
}
};
}]);
My question is, how to use lastModelValue while saving, but preserving the invalid value in the view?
EDIT:
Another possibility, as suggested by Jugnu below, would be wrapping and manipulating the build in validators.
I tried to following: wrap all existing validators and remember the last valid value, to restore it if validations fails:
Object.keys(ngModel.$validators).forEach(function(validatorName, index) {
var validator = ngModel.$validators[validatorName];
ngModel.$validators[validatorName] = createWrapper(validatorName, validator, ngModel);
});
function createWrapper(validatorName, validator, ngModel){
var lastValid;
return function (modelValue){
var result = validator(modelValue);
if(result) {
lastValid = modelValue;
}else{
// what to do here? maybe asign the value like this:
// $parse(attrs.ngModel).assign(scope, lastValid);
}
return result;
};
}
But I'm not sure how to continue with this approach either. Can I set the model value without AngularJS kicking in and try to validate that newly set value?

I have created a simple directive that serves as a wrapper on the ng-model directive and will keep always the latest valid model value. It's called valid-ng-model and should replace the usage of ng-model on places where you want to have the latest valid value.
I've created an example use case here, I hope you will like it. Any ideas for improvements are welcomed.
This is the implementation code for valid-ng-model directive.
app.directive('validNgModel', function ($compile) {
return {
terminal: true,
priority: 1000,
scope: {
validNgModel: '=validNgModel'
},
link: function link(scope, element, attrs) {
// NOTE: add ngModel directive with custom model defined on the isolate scope
scope.customNgModel = angular.copy(scope.validNgModel);
element.attr('ng-model', 'customNgModel');
element.removeAttr('valid-ng-model');
// NOTE: recompile the element without this directive
var compiledElement = $compile(element)(scope);
var ngModelCtrl = compiledElement.controller('ngModel');
// NOTE: Synchronizing (inner ngModel -> outside valid model)
scope.$watch('customNgModel', function (newModelValue) {
if (ngModelCtrl.$valid) {
scope.validNgModel = newModelValue;
}
});
// NOTE: Synchronizing (outside model -> inner ngModel)
scope.$watch('validNgModel', function (newOutsideModelValue) {
scope.customNgModel = newOutsideModelValue;
});
}
};
});
Edit: directive implementation without isolate scope: Plunker.

Since you are sending the entire object for each field modification, you have to keep the last valid state of that entire object somewhere. Use case I have in mind:
You have a valid object { name: 'Valid', email: 'Valid' }.
You change the name to invalid; the autosave directive placed at the name input knows its own last valid value, so the correct object gets sent.
You change the email to invalid too. The autosave directive placed at the email input knows its own last valid value but NOT that of name. If the last known good values are not centralized, an object like { name: 'inalid', email: 'Valid' } will be sent.
So the suggestion:
Keep a sanitized copy of the object you are editing. By sanitized I mean that any invalid initial values should be replaced by valid pristine ones (e.g. zeros, nulls etc). Expose that copy as a controller member, e.g. fooCtrl.lastKnowngood.
Let autosave know the last known good state, e.g. as:
<input ng-model="fooCtrl.email" autosave="fooCtrl.lastKnowngood" required />
Keep the last known good local value in that object; utilize the ng-model expression, e.g. as:
var lastKnownGoodExpr = $parse(attrs.autosave);
var modelExpr = $parse(attrs.ngModel);
function saveIfModelChanged () {
var lastKnownGood = lastKnownGoodExpr(scope);
if (ngModel.$valid) {
// trick here; explanation later
modelExpr.assign({fooCtrl: lastKnownGood}, ngModel.$modelValue);
}
// send the lastKnownGood object to the server!!!
}
Send the lastKnownGood object.
The trick, its shortcomings and how can it be improved: When setting the local model value to the lastKnownGood object you use a context object different than the current scope; this object assumes that the controller is called fooCtrl (see the line modelExpr.assign({fooCtrl: lastKnownGood}, ...)). If you want a more general directive, you may want to pass the root as a different attribute, e.g.:
<input ng-model="fooCtrl.email" autosave="fooCtrl.lastKnowngood" required
autosave-fake-root="fooCtrl" />
You may also do some parsing of the ng-model expression yourself to determine the first component, e.g. substring 0 → 1st occurence of the dot (again simplistic).
Another shortcoming is how you handle more complex paths (in the general case), e.g. fooCtrl.persons[13].address['home'].street - but that seems not to be your use case.
By the way, this:
ngModel.$viewChangeListeners.push(function () {
saveIfModelChanged();
});
can be simplified as:
ngModel.$viewChangeListeners.push(saveIfModelChanged);

Angular default validators will only assign value to model if its valid email address.To overcome that you will need to override default validators.
For more reference see : https://docs.angularjs.org/guide/forms#modifying-built-in-validators
You can create a directive that will assign invalide model value to some scope variable and then you can use it.
I have created a small demo for email validation but you can extend it to cover all other validator.
Here is fiddle : http://plnkr.co/edit/EwuyRI5uGlrGfyGxOibl?p=preview

Related

Set value in $scope and use $parsers on it

I have a field in $scope (i'm new in angularjs terminology), that is linked in html:
<input type="text" ng-model="phone" format-phone/>
Format-phone is a custom directive that adds a parser for a view value:
.directive('formatPhone', () => ({
require: '?ngModel',
link: (scope, elem, attrs, ctrl) => {
if (!ctrl) return;
ctrl.$parsers.unshift(viewValue => {
var phone = viewValue.substring(0, 12).replace(/* some custom replace */);
elem.val(phone.replace(/* another custom replace */)));
return phone;
});
}
}))
That works perfectly and modify model and view values as intended. I want to add default value to the model in controller code, so i added this line:
$scope.phone = '1112223344';
But this value is not handled by parser in directive and stays raw until i edit it first time. Can i explicitly call parser on view/model value from controller without writing another initialization directive? Or maybe i can set view value that will be automatically handled by parser?
UPD: Thanks to #potatopeelings i made it better, but this solution looks real bad. Do i have any alternative for calling formatter from parser?
ctrl.$formatters.unshift(modelValue => {
modelValue; // 123456
return modelValue.replace(...); // View value is now (123)45-6
});
ctrl.$parsers.unshift(viewValue => {
var phone = viewValue.replace(...); // (123)456 -> 123456
elem.val(ctrl.$formatters[0](phone)); // Element value is now (123)45-6
return phone; // Model value is now 123456
});
parsers are meant to convert view values to model values and formatters are used to convert model values to view values.
You need to insert a formatter that converts your (initialized) model value to the view value you need.

How to implement an ng-change for a custom directive

I have a directive with a template like
<div>
<div ng-repeat="item in items" ng-click="updateModel(item)">
<div>
My directive is declared as:
return {
templateUrl: '...',
restrict: 'E',
require: '^ngModel',
scope: {
items: '=',
ngModel: '=',
ngChange: '&'
},
link: function postLink(scope, element, attrs)
{
scope.updateModel = function(item)
{
scope.ngModel = item;
scope.ngChange();
}
}
}
I would like to have ng-change called when an item is clicked and the value of foo has been changed already.
That is, if my directive is implemented as:
<my-directive items=items ng-model="foo" ng-change="bar(foo)"></my-directive>
I would expect to call bar when the value of foo has been updated.
With code given above, ngChange is successfully called, but it is called with the old value of foo instead of the new updated value.
One way to solve the problem is to call ngChange inside a timeout to execute it at some point in the future, when the value of foo has been already changed. But this solution make me loose control over the order in which things are supposed to be executed and I assume that there should be a more elegant solution.
I could also use a watcher over foo in the parent scope, but this solution doesn't really give an ngChange method to be implmented and I have been told that watchers are great memory consumers.
Is there a way to make ngChange be executed synchronously without a timeout or a watcher?
Example: http://plnkr.co/edit/8H6QDO8OYiOyOx8efhyJ?p=preview
If you require ngModel you can just call $setViewValue on the ngModelController, which implicitly evaluates ng-change. The fourth parameter to the linking function should be the ngModelCtrl. The following code will make ng-change work for your directive.
link : function(scope, element, attrs, ngModelCtrl){
scope.updateModel = function(item) {
ngModelCtrl.$setViewValue(item);
}
}
In order for your solution to work, please remove ngChange and ngModel from isolate scope of myDirective.
Here's a plunk: http://plnkr.co/edit/UefUzOo88MwOMkpgeX07?p=preview
tl;dr
In my experience you just need to inherit from the ngModelCtrl. the ng-change expression will be automatically evaluated when you use the method ngModelCtrl.$setViewValue
angular.module("myApp").directive("myDirective", function(){
return {
require:"^ngModel", // this is important,
scope:{
... // put the variables you need here but DO NOT have a variable named ngModel or ngChange
},
link: function(scope, elt, attrs, ctrl){ // ctrl here is the ngModelCtrl
scope.setValue = function(value){
ctrl.$setViewValue(value); // this line will automatically eval your ng-change
};
}
};
});
More precisely
ng-change is evaluated during the ngModelCtrl.$commitViewValue() IF the object reference of your ngModel has changed. the method $commitViewValue() is called automatically by $setViewValue(value, trigger) if you do not use the trigger argument or have not precised any ngModelOptions.
I specified that the ng-change would be automatically triggered if the reference of the $viewValue changed. When your ngModel is a string or an int, you don't have to worry about it. If your ngModel is an object and your just changing some of its properties, then $setViewValue will not eval ngChange.
If we take the code example from the start of the post
scope.setValue = function(value){
ctrl.$setViewValue(value); // this line will automatically evalyour ng-change
};
scope.updateValue = function(prop1Value){
var vv = ctrl.$viewValue;
vv.prop1 = prop1Value;
ctrl.$setViewValue(vv); // this line won't eval the ng-change expression
};
After some research, it seems that the best approach is to use $timeout(callback, 0).
It automatically launches a $digest cycle just after the callback is executed.
So, in my case, the solution was to use
$timeout(scope.ngChange, 0);
This way, it doesn't matter what is the signature of your callback, it will be executed just as you defined it in the parent scope.
Here is the plunkr with such changes: http://plnkr.co/edit/9MGptJpSQslk8g8tD2bZ?p=preview
Samuli Ulmanen and lucienBertin's answers nail it, although a bit of further reading in the AngularJS documentation provides further advise on how to handle this (see https://docs.angularjs.org/api/ng/type/ngModel.NgModelController).
Specifically in the cases where you are passing objects to $setViewValue(myObj). AngularJS Documentatation states:
When used with standard inputs, the view value will always be a string (which is in some cases parsed into another type, such as a Date object for input[date].) However, custom controls might also pass objects to this method. In this case, we should make a copy of the object before passing it to $setViewValue. This is because ngModel does not perform a deep watch of objects, it only looks for a change of identity. If you only change the property of the object then ngModel will not realize that the object has changed and will not invoke the $parsers and $validators pipelines. For this reason, you should not change properties of the copy once it has been passed to $setViewValue. Otherwise you may cause the model value on the scope to change incorrectly.
For my specific case, my model is a moment date object, so I must clone the object first before then calling setViewValue. I am lucky here as moment provides a simple clone method: var b = moment(a);
link : function(scope, elements, attrs, ctrl) {
scope.updateModel = function (value) {
if (ctrl.$viewValue == value) {
var copyOfObject = moment(value);
ctrl.$setViewValue(copyOfObject);
}
else
{
ctrl.$setViewValue(value);
}
};
}
The fundamental issue here is that the underlying model does not get updated until the digest cycle that happens after scope.updateModel has finished executing. If the ngChange function requires details of the update that is being made then those details can be made available explicitly to ngChange, rather than relying on the model updating having been previously applied.
This can be done by providing a map of local variable names to values when calling ngChange. In this scenario, you can mapping the new value of the model to a name which can be referenced in the ng-change expression.
For example:
scope.updateModel = function(item)
{
scope.ngModel = item;
scope.ngChange({newValue: item});
}
In the HTML:
<my-directive ng-model="foo" items=items ng-change="bar(newValue)"></my-directive>
See: http://plnkr.co/edit/4CQBEV1S2wFFwKWbWec3?p=preview

AngularJS - Calling function in directive when a 2-way bound value changes

I've got a directive all setup with 2-way data binding on the attributes using = and I can see everything is working well with that. Now I'm stuck at the need to call a function within the directive whenever one of my bound attributes changes in the parent scope, and I can't figure out how to pull that off.
I'm basically creating a version of the ui checkbox button that works with arrays of objects. You pass the directive an allowed array (which contains all the different options) and an applied array (which contains the same objects from allowed). For checking if an object is in the allowed array I have another array that is the just the id properties. Within the directive this is working great, but if the applied array changes outside of the directive the id array never gets updated.
The Directive:
angular.module('MyApp',[])
.directive('btnCheckboxGroup', function(){
return {
restrict: 'E',
// controller: DirCtrl,
templateUrl: 'btnCheckboxGroup.html',
scope: {
allowed: '=',
applied: '=',
id: '=',
title: '='
},
link: function(scope, elem, attrs){
scope.abp = [];
// this works right away, but how do I run it when the parent scope updates it?
angular.forEach(scope.applied, function(obj){
scope.abp.push( obj[scope.id] );
});
scope.addRemove = function(a){
var index = scope.abp.indexOf(a[scope.id]);
// doesn't exist, add it
if(index === -1){
scope.abp.push(a[scope.id]);
scope.applied.push(a);
// does exist, remove it
} else {
scope.abp.splice(index, 1);
for(var i in scope.applied){
if(scope.applied[i][scope.id]==a[scope.id]){
scope.applied.splice(i,1);
break;
}
}
}
}// end addRemove()
}
};
});
JSFiddle
I've tried lots of variations of things like scope.$watch, attrs.$observe, and attempted at one point to try one-way data-binding with # and that made lots of things crash.
So whats the magic I'm missing here?
You can pass a third parameter to $watch to change the way it compares the old and the new value. See the Angular docs
$watch(watchExpression, [listener], [objectEquality]);
If you set this to true, it will pick up changes in the content of the array and not only when the array reference changes. This does have a performance impact (depending on the length of the array). Checking only the length of the array does not cover the case where the number of elements stay the same but the elements themselves do change.
In your case you would need to do something like this:
scope.$watch(
"applied",
function() {
scope.abp = [];
angular.forEach(scope.applied, function(obj){
scope.abp.push( obj[scope.id] );
});
},
true);
Is this what you're looking for?
scope.$watch(function() {
return scope.applied.length;
}, function(val) {
console.log(val);
console.log(scope.applied);
});
The array on scope doesn't change but its length does, so if you were using the string-variant of $watch it won't fire, but using a function and looking at the length of the array will. More on this in the docs

Check duplicate values for a text filed in Angular form

Folks,
I have a text-field in a form. When a user enters a value in this text field,
I would like to check it against a array of values to ensure this value doesn't already exists.
I started off writing a directive and since I am new to this I am quiet lost
<input type="text" ng-model="user.name" placeholder="Enter Name" required checkDuplicateName existing-names="allUsers" tooltip/>
I the above code allUsers looks like this
var allUsers = ['john','james','Tim'];
My directive is as follows:
angular.module('formInputs').directive('checkDuplicateName', function() {
return {
restrict : 'A',
require : 'ngModel',
link: function(scope, element, attrs, ctrl) {
scope.$watch(attrs.ngModel,function(){
for (var i =0; i<attrs.existing-names.length;i++) {
if (attrs.existing-names[i] === attrs.ngModel) {
attrs.$set('tooltip','Name already exsists');
return;
}
}
});
}
}
});
The problem I am having is that the directive is not getting fired. What am i missing here ?
I've created a fiddle here that has a working version of your idea. It was just a couple small things, it doesn't look like you are passing your data correctly. When you access attrs it just gives you the string that is in the markup so attrs.ngModel will just give you "users.name" as a string.
I created an isolate scope so that you can see the scoping.
scope: {
model: '=ngModel',
users: '=existingNames'
},
Also, angular turns this-case-in-html into camel case in the js like: thisCaseInHtml
Edit: if you don't want to isolate scope, that's fine too. You just need to be evaluating the strings on the scope instead.
Here's an updated fiddle http://jsfiddle.net/ddJ4Z/10/ with the changes. Notice how the values are evaluated on the scope.
scope.$watch(attrs.ngModel,function(newVal){
var users = scope.$eval(attrs.existingNames) || [];
for (var i =0; i<users.length;i++) {
if (users[i] === newVal) {
window.alert(newVal);
return;
}
}
});
Hope this helps!

Unable to get the resolved attributes within custom directive

I have been trying to write a custom directive for an input field with dynamic id, in the directive am unable to get the correct id.
<input id="myInput{{$index}}" my-dir="fn()"/>
myApp.directive('myDir', function ($parse) {
var obj = {
require: "ngModel",
link: {
post: function (scope, element, attrs) {
var fn = $parse(attrs.myDir);
var elementId = element.attr('id');
console.log(elementId); // Here I see myInput{{$index}} instead of myInput0, by this time angular is not resolving the value
}
}
};
return obj;
});
My question would be, how can I get the resolved value in the directive. Also I cannot use any isolated scope here due to other reasons.
Thanks in advance
You can use $observe to observe the value changes of attributes that contain interpolation (e.g. src="{{bar}}"). Not only is this very efficient but it's also the only way to easily get the actual value because during the linking phase the interpolation hasn't been evaluated yet and so the value is at this time set to undefined.
post: function (scope, element, attrs) {
attrs.$observe('id', function (id) {
console.log(id)
})
}
If you only want to evaluate the value once in the link function, you can use $interpolate (remember to inject it into your directive):
console.log($interpolate(element.attr('id'))(scope));
However, since you are likely using ng-repeat (because of the use of $index) I prefer #sza's answer, since your list may change, hence you may need to react to changes to your list.

Resources