Set value in $scope and use $parsers on it - angularjs

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.

Related

Why does the ngModel validator code seem to run before the scope.$watch changes?

I am attempting to create an AngularJS directive with a custom validator so that I can show error messages based on the validator. However, I am running into an error because it seems that the validator is running prior to the scope.$watch() per the console.log() messages I've input.
Here is the directive:
angular
.module('app')
.directive('validateRefundAmount', validateRefundAmount);
validateRefundAmount.$inject = [ 'AmountConversionService', '$q' ];
function validateRefundAmount(AmountConversionService, $q) {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attrs, control) {
scope.$watch('orderDetails.refundAmount', function(newValue, oldValue) {
if (newValue === oldValue) {
console.log(newValue, oldValue);
return;
}
// Get Already Refunded Amount
var refundedAmount = scope.orderDetails.refundedAmount;
// Converts Amount To Pure Integers, Removing Decimal
var totalPaymentAmount = AmountConversionService.prepareAmountForCalculations(scope.orderDetails.paymentAmount, 10);
var totalRefundAmount = AmountConversionService.prepareAmountForCalculations(newValue || 0);
// Add Already Refunded Amount to Previously Refunded Amount to Get Total Refund
if (refundedAmount) {
totalRefundAmount += AmountConversionService.prepareAmountForCalculations(refundedAmount);
}
control.$validators.refundAmountTooHigh = function() {
if (totalRefundAmount > totalPaymentAmount) {
return false
}
return true;
}
});
}
};
}
The element that this is applied to is an text input box that starts with no value. When i type '1' into the field the validator doesn't run. When I add a '2' to the field, making '12', the validator runs with '1' as the input. The same thing occurs when I add a '3' to the field making '123' -- it runs with '12', instead of the new value of '123'.
When I've inserted console.log() statements to see what is occurring when, it looks like the validator code runs and then the scope.watch() runs creating the new value after the validator has run but I don't know why or what to search for to find out.
There may be another way to do what I'm trying to do -- what I want to do is run the validator everytime the element value changes. I can't use bind on keypress due to keypress not detecting backspaces and may be able to use bind on keydown -- not sure.
Please check doc for $setViewValue, apparently $validators is used before ngModelValue update.
And your scope.$watch will only be invoked after $modelValue is updated.
https://docs.angularjs.org/api/ng/type/ngModel.NgModelController#$setViewValue
When $setViewValue is called, the new value will be staged for committing through the $parsers and $validators pipelines. If there are no special ngModelOptions specified then the staged value is sent directly for processing through the $parsers pipeline. After this, the $validators and $asyncValidators are called and the value is applied to $modelValue. Finally, the value is set to the expression specified in the ng-model attribute and all the registered change listeners, in the $viewChangeListeners list are called.

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

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

ngModel and How it is Used

I am just getting started with angular and ran into the directive below. I read a few tutorials already and am reading some now, but I really don't understand what "require: ngModel" does, mainly because I have no idea what ngModel does overall. Now, if I am not insane, it's the same directive that provides two way binding (the whole $scope.blah = "blah blah" inside ctrl, and then {{blah}} to show 'blah blah' inside an html element controlled by directive.
That doesn't help me here. Furthermore, I don't understand what "model: '#ngModel' does. #ngModel implies a variable on the parents scope, but ngModel isn't a variable there.
tl;dr:
What does "require: ngModel" do?
What does "model : '#ngModel'" do?
*auth is a service that passes profile's dateFormat property (irrelevant to q)
Thanks in advance for any help.
angular.module('app').directive('directiveDate', function($filter, auth) {
return {
require: 'ngModel',
scope: {
model : '#ngModel',
search: '=?search'
},
restrict: 'E',
replace: true,
template: '<span>{{ search }}</span>',
link: function($scope) {
$scope.set = function () {
$scope.text = $filter('date')($scope.model, auth.profile.dateFormat );
$scope.search = $scope.text;
};
$scope.$watch( function(){ return $scope.model; }, function () {
$scope.set();
}, true );
//update if locale changes
$scope.$on('$localeChangeSuccess', function () {
$scope.set();
});
}
};
});
ngModel is an Angular directive responsible for data-binding. Through its controller, ngModelController, it's possible to create directives that render and/or update the model.
Take a look at the following code. It's a very simple numeric up and down control. Its job is to render the model and update it when the user clicks on the + and - buttons.
app.directive('numberInput', function() {
return {
require: 'ngModel',
restrict: 'E',
template: '<span></span><button>+</button><button>-</button>',
link: function(scope, element, attrs, ngModelCtrl) {
var span = element.find('span'),
plusButton = element.find('button').eq(0),
minusButton = element.find('button').eq(1);
ngModelCtrl.$render = function(value) {
updateValue();
};
plusButton.on('click', function() {
ngModelCtrl.$setViewValue(ngModelCtrl.$modelValue + 1);
updateValue();
});
minusButton.on('click', function() {
ngModelCtrl.$setViewValue(ngModelCtrl.$modelValue - 1);
updateValue();
});
function updateValue(value) {
span.html(ngModelCtrl.$modelValue);
}
}
};
});
Working Plunker
Since it interacts with the model, we can use ngModelController. To do that, we use the require option to tell Angular we want it to inject that controller into the link function as its fourth argument. Now, ngModelController has a vast API and I won't get into much detail here. All we need for this example are two methods, $render and $setViewValue, and one property, $modelValue.
$render and $setViewValue are two ways of the same road. $render is called by Angular every time the model changes elsewhere so the directive can (re)render it, and $setViewValue should be called by the directive every time the user does something that should change the model's value. And $modelValue is the current value of the model. The rest of the code is pretty much self-explanatory.
Finally, ngModelController has an arguably shortcoming: it doesn't work well with "reference" types (arrays, objects, etc). So if you have a directive that binds to, say, an array, and that array later changes (for instance, an item is added), Angular won't call $render and the directive won't know it should update the model representation. The same is true if your directive adds/removes an item to/from the array and call $setViewValue: Angular won't update the model because it'll think nothing has changed (although the array's content has changed, its reference remains the same).
This should get you started. I suggest that you read the ngModelController documentation and the official guide on directives so you can understand better how this all works.
P.S: The directive you have posted above isn't using ngModelController at all, so the require: 'ngModel' line is useless. It's simply accessing the ng-model attribute to get its value.

How do I retrieve the unevaluated value of an attribute from a directive in Angular?

I have a directive which is using $observe to watch when the value of one of the attributes changes. When this fires, I need to be able to retrieve the unevaluated value of the attribute not the evaluated value.
So my HTML would look like this:
<div my-attrib="{{scopeVar}}"></div>
Then the link function in my directive:
attrib.$observe('myAttrib', function(val) {
// Both val and attrib.myAttrib contain "ABC"
// I would like the uncompiled value instead
var evaluatedValue = attrib.myAttrib;
});
If the controller had done this:
$scope.myAttrib = "ABC";
When $observe first, evalutedValue returns "ABC". I actually need it to return "{{scopeVar}}".
EDIT: Per the comment below from François Wahl I ended up moving this into a ng-repeat element which is bound to an array of one item. Then I just remove/add the new item in the controller which updates $scope. This eliminates the need to retrieve the uncompiled attribute value and actually cleans things up quite a bit. It's definitely odd when looking at the view since it's not immediately clear as to why it's in a repeater, but it's worth it since it cleans up the code quite a bit.
You have to grab it in the compile: function of the directive link this:
.directive('myAttrib', function() {
return {
compile: function(tElement, tAttrs) {
var unevaluatedValue = tAttrs.myAttrib;
return function postLink(scope, element, attrs) {
attrs.$observe('myAttrib', function(val) {
// Both val and attrib.myAttrib contain "ABC"
// I would like the uncompiled value instead
var evaluatedValue = attrs.myAttrib;
console.log(unevaluatedValue);
console.log(evaluatedValue);
});
}
}
}
})
Example Plunker: http://plnkr.co/edit/bHDmxJvnENqz8Mpg1qpd?p=preview
Hope this helps.

What's the difference between ngModel.$modelValue and ngModel.$viewValue

I have the following ckEditor directive. At the bottom are two variations that I have seen from examples on how to set the data in the editor:
app.directive('ckEditor', [function () {
return {
require: '?ngModel',
link: function ($scope, elm, attr, ngModel) {
var ck = null;
var config = attr.editorSize;
if (config == 'wide') {
ck = CKEDITOR.replace(elm[0], { customConfig: 'config-wide.js' });
} else {
ck = CKEDITOR.replace(elm[0], { customConfig: 'config-narrow.js' });
}
function updateModel() {
$scope.$apply(function () {
ngModel.$setViewValue(ck.getData());
});
}
$scope.$on('modalObjectSet', function (e, modalData) {
// force a call to render
ngModel.$render();
});
ck.on('change', updateModel);
ck.on('mode', updateModel);
ck.on('key', updateModel);
ck.on('dataReady', updateModel);
ck.on('instanceReady', function () {
ngModel.$render();
});
ck.on('insertElement', function () {
setTimeout(function () {
$scope.$apply(function () {
ngModel.$setViewValue(ck.getData());
});
}, 1000);
});
ngModel.$render = function (value) {
ck.setData(ngModel.$modelValue);
};
ngModel.$render = function (value) {
ck.setData(ngModel.$viewValue);
};
}
};
}])
Can someone tell me what is the difference between:
ck.setData(ngModel.$modelValue);
ck.setData(ngModel.$viewValue);
And which should I use. I looked at the angular documentation and it says:
$viewValue
Actual string value in the view.
$modelValue
The value in the model, that the control is bound to.
I have no idea what the author meant when he wrote this in the document :-(
You are looking at the correct documentation, but it might just be that you're a little confused. The $modelValue and $viewValue have one distinct difference. It is this:
As you already noted above:
$viewValue: Actual string (or Object) value in the view.
$modelValue: The value in the model, that the control is bound to.
I'm going to assume that your ngModel is referring to an <input /> element...? So your <input> has a string value that it displays to the user, right? But the actual model might be some other version of that string. For example, the input might be showing the string '200' but the <input type="number"> (for example) will actually contain a model value of 200 as an integer. So the string representation that you "view" in the <input> is the ngModel.$viewValue and the numeric representation will be the ngModel.$modelValue.
Another example would be a <input type="date"> where the $viewValue would be something like Jan 01, 2000 and the $modelValue would be an actual javascript Date object that represents that date string. Does that make sense?
I hope that answers your question.
You can see things like this :
$modelValue is your external API, that is to say, something exposed to your controller.
$viewValue is your internal API, you should use it only internally.
When editing $viewValue, the render method won't be called, because it is the "rendered model". You will have to do it manually, whereas the rendering method will be called automatically upon $modelValue modifications.
However, the information will remain consistent, thanks to $formatters and $parsers :
If you change $viewValue, $parsers will translate it back to
$modelValue.
If you change $modelValue, $formatters will
convert it to $viewValue.
Angular has to keep track of two views of ngModel data- there's the data as seen by the DOM(browser) and then there's Angular's processed representation of those values. The $viewValue is the DOM side value. So, for example, in an <input> the $viewValue is what the user typed in to their browser.
Once someone types something in <input> then $viewValue is processed by $parsers and turned into Angular's view of the value which is called $modelValue.
So you can think of $modelValue being angular's processed version of the value, the value you see in the model, while $viewValue is the raw version.
To take this one step further imagine we do something that changes the $modelValue. Angular sees this change and calls $formatters to create an updated $viewValue (based on the new $modelValue) to be sent to the DOM.

Resources