I'm trying to create a directive named currency that appends a $ before the text in input. The dollar sign should be shown at all times and shouldn't be possible to remove.
Here's my code:
app.directive('currency', function() {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, elem, attrs, controller) {
// view -> model
controller.$parsers.push(function (viewValue) {
viewValue = viewValue.replace(/^\$/, '');
controller.$viewValue = viewValue;
return viewValue;
});
// model -> view
controller.$formatters.push(function (modelValue) {
modelValue = '$' + modelValue;
controller.$modelValue = modelValue;
return modelValue;
});
}
};
});
Working example: https://jsfiddle.net/U3pVM/29012/
As you can see, the dollar sign is appended initially, but can be deleted and won't be appended after that. It seems that the function I push to $formatters is only being called once. Is it supposed to work like that or am I missing something? How can I implement the desired behavior?
ok, i have tried a workaround, it works but i am not sure if this is the correct way to do it.
updated fiddle : https://jsfiddle.net/U3pVM/29014/
controller.$parsers.push(function (viewValue) {
//console.log(viewValue.substring(0,1));
if(viewValue.substring(0,1) != "$"){
var view_value = "$" + viewValue;
controller.$setViewValue(view_value);
controller.$render();
}
viewValue = viewValue.replace(/^\$/, '');
//controller.$viewValue = viewValue;
console.log(viewValue);
return viewValue;
});
P.S: i am not sure why you are injecting ngModel as controller in your link function. it might be a mistake.
I think you're not quite understanding what the $parsers and $formatters do. Whenever you enter something in the input field, the $parsers are responsible for converting this value into a model value. Formatters are responsible for converting a model value into a display value in your input field.
What you are attempting to do is to change the content of your input field ($formatter feature) when someone enters something into the field ($parser feature).
While I'm sure there are workarounds to making it work this way, you're misusing the concepts of $parsers and $formatters when you do. Instead you should be looking at a custom directive (or extend the one you have) to add to the input that does what you're trying to do, for instance by handing keyups.
Edit
See the following code example for a link function to give you some indication of what I mean:
link: function (scope, elem, attrs, controller) {
elem.bind('keyup', function(evt) {
// Change the displayed value after every keypress
// This function is an example and needs serious work...
// Perhaps you should even put this in a separate directive
var value = elem.val().replace(/[^$0-9]/g, '');
if (value && value.substring(0,1) !== '$') {
value = '$' + value;
}
elem.val(value);
});
// view -> model
controller.$parsers.push(function (viewValue) {
// Any time the view changes, remove the $ sign and interpret the rest as number for the model
var modelValue = viewValue.replace(/^\$/, '');
return parseFloat(modelValue);
});
// model -> view
controller.$formatters.push(function (modelValue) {
// Any time the model (number) changes, append it with a $ sign for the view
var viewValue = '$' + modelValue;
return viewValue;
});
}
Or check the entire fiddle: https://jsfiddle.net/cL0hpvp4/
(I'm sorry if my question title isn't very good, I couldn't think of a better one. Feel free to suggest better options.)
I'm trying to create a reusable "property grid" in Angular, where one can bind an object to the grid, but in such a way that presentation of the object can be customized somewhat.
This is what the directive template looks like (the form-element isn't important to my question, so I'll leave it out):
<div ng-repeat="prop in propertyData({object: propertyObject})">
<div ng-switch on="prop.type">
<div ng-switch-when="text">
<form-element type="text"
label-translation-key="{{prop.key}}"
label="{{prop.key}}"
name="{{prop.key}}"
model="propertyObject[prop.key]"
focus-events-enabled="false">
</form-element>
</div>
</div>
</div>
and, the directive code:
angular.module("app.shared").directive('propertyGrid', ['$log', function($log) {
return {
restrict: 'E',
scope: {
propertyObject: '=',
propertyData: '&'
}
templateUrl: 'views/propertyGrid.html'
};
}]);
Here's an example usage:
<property-grid edit-mode="true"
property-object="selectedSite"
property-data="getSitePropertyData(object)">
</property-grid>
And the getSitePropertyData() function that goes with it:
var lastSite;
var lastSitePropertyData;
$scope.getSitePropertyData = function (site) {
if (site == undefined) return null;
if (site == lastSite)
return lastSitePropertyData;
lastSite = site;
lastSitePropertyData = [
{key:"SiteName", value:site.SiteName, editable: true, type:"text"},
//{key:"Company.CompanyName", value:site.Company.CompanyName, editable: false, type:"text"},
{key:"Address1", value:site.Address1, editable: true, type:"text"},
{key:"Address2", value:site.Address2, editable: true, type:"text"},
{key:"PostalCode", value:site.PostalCode, editable: true, type:"text"},
{key:"City", value:site.City, editable: true, type:"text"},
{key:"Country", value:site.Country, editable: true, type:"text"},
{key:"ContactName", value:site.ContactName, editable: true, type:"text"},
{key: "ContactEmail", value: site.ContactEmail, editable: true, type:"email"},
{key: "ContactPhone", value: site.ContactPhone, editable: true, type:"text"},
{key: "Info", value: site.Info, editable: true, type:"text"}
];
return lastSitePropertyData;
};
The reason I'm going through such a "property data" function and not just binding directly to properties on the object is that I need to control the order of the properties, as well as whether they should even be shown to the user at all, and also what kind of property it is (text, email, number, date, etc.) for the sake of presentation.
At first, as you can tell from the value property remnant in the getSitePropertyData() function, I first tried providing the values directly from this function, but that wouldn't bind to the object, so changes either in the object or form the property grid didn't sync back and forth. Next up, then, was using the key idea, which lets me do this: propertyObject[prop.key]—which works great for direct properties, but as you can see, I had to comment out the "Company" field, because it's a property of a property, and propertyObject["a.b"] doesn't work.
I'm struggling to figure out what to do here. I need the bindings to work, and I need to be able to use arbitrarily deep properties in my bindings. I know this kind of thing is theoretically possible; I've seen it done for instance in UI Grid, but such projects have so much code that I would probably spend days finding out how they do it.
Am I getting close, or am I going about this all wrong?
You want to run an arbitrary Angular expression on an object. That is exactly the purpose of $parse (ref). This service can well... parse an Angular expression and return a getter and setter. The following example is an oversimplified implementation of your formElement directive, demonstrating the use of $parse:
app.directive('formElement', ['$parse', function($parse) {
return {
restrict: 'E',
scope: {
label: '#',
name: '#',
rootObj: '=',
path: '#'
},
template:
'<label>{{ label }}</label>' +
'<input type="text" ng-model="data.model" />',
link: function(scope) {
var getModel = $parse(scope.path);
var setModel = getModel.assign;
scope.data = {};
Object.defineProperty(scope.data, 'model', {
get: function() {
return getModel(scope.rootObj);
},
set: function(value) {
setModel(scope.rootObj, value);
}
});
}
};
}]);
I have altered slightly the way the directive is used, hopefully without changing the semantics:
<form-element type="text"
label-translation-key="{{prop.key}}"
label="{{prop.key}}"
name="{{prop.key}}"
root-obj="propertyObject"
path="{{prop.key}}"
focus-events-enabled="false">
Where root-obj is the top of the model and path is the expression to reach the actual data.
As you can see, $parse creates the getter and setter function for the given expression, for any root object. In the model.data property, you apply the accessor functions created by $parse to the root object. The entire Object.defineProperty construct could be replaced by watches, but that would only add overhead to the digest cycle.
Here is a working fiddle: https://jsfiddle.net/zb6cfk6y/
By the way, another (more terse and idiomatic) way to write the get/set would be:
Object.defineProperty(scope.data, 'model', {
get: getModel.bind(null, scope.rootObj),
set: setModel.bind(null, scope.rootObj)
});
If you are using lodash you can use the _.get function to achieve this.
You can store _.get in the controller of your property-grid and then use
model="get(propertyObject,prop.key)"
in your template. If you need this functionality in multiple places in your application (and not just in property-grid) you could write a filter for this.
The problem with this is that you can't bind your model this way and thus you can't edit the values. You can use the _.set function and an object with a getter and a setter to make this work.
vm.modelize = function(obj, path) {
return {
get value(){return _.get(obj, path)},
set value(v){_.set(obj, path,v)}
};
}
You can then use the function in the template:
<div ng-repeat="prop in propertyData({object: propertyObject})">
<input type="text"
ng-model="ctrl.modelize(propertyObject,prop.key).value"
ng-model-options="{ getterSetter: true }"></input>
</div>
For a reduced example see this Plunker.
If you don't use lodash you can use this simplified version of the _.get function that I extracted from lodash.
function getPath(object, path) {
path = path.split('.')
var index = 0
var length = path.length;
while (object != null && index < length) {
object = object[path[index++]];
}
return (index && index == length) ? object : undefined;
}
This function makes sure that you won't get any Cannot read property 'foo' of undefined errors. This is useful especially if you have long chains of properties where there might be an undefined value. If you want to be able to use more advanced paths (like foo.bar[0]) you have to use the full _.get function from lodash.
And here is a simplified version of _.set also extracted form lodash:
function setPath(object, path, value) {
path = path.split(".")
var index = -1,
length = path.length,
lastIndex = length - 1,
nested = object;
while (nested != null && ++index < length) {
var key = path[index]
if (typeof nested === 'object') {
var newValue = value;
if (index != lastIndex) {
var objValue = nested[key];
newValue = objValue == null ?
((typeof path[index + 1] === 'number') ? [] : {}) :
objValue;
}
if (!(hasOwnProperty.call(nested, key) && (nested[key] === value)) ||
(value === undefined && !(key in nested))) {
nested[key] = newValue;
}
}
nested = nested[key];
}
return object;
}
Keep in mind that these extracted functions ignore some edge cases that lodash handles. But they should work in most cases.
When you creating the lastSitePropertyData you can create the object in this way to not hardcode it
function createObject (){
for(var key in site){
lastSitePropertyData.push({key:key, value:site[key], editable: true, type:"text"});
} }
And later use function to get data something like this
function getKey(prop){
if(typeof prop.value === 'object'){
return prop.value.key; //can run loop create a go deep reccursive method - thats upto u
}
else return prop.key;
}
function getValue(prop){
if(typeof prop === 'object'){
return prop.value.value; //have tp run loop get value from deep reccursive method - thats upto u
}
else return prop.value;
}
That way can be use in html {{getKey(prop)}} and {{getValue(prop}}
For working demo please have look this link - https://jsfiddle.net/718px9c2/4/
Note: Its just idea for accessing json data in better way, I am not using angular in demo.
Another idea is to do smth like this.
If you wont to avoid making object.proto dirty (this is always good idea) just move this functionality into the other module.
(function () {
'use strict';
if (Object.hasOwnProperty('getDeep')) {
console.error('object prototype already has prop function');
return false;
}
function getDeep(propPath) {
if (!propPath || typeof propPath === 'function') {
return this;
}
var props = propPath.split('.');
var result = this;
props.forEach(function queryProp(propName) {
result = result[propName];
});
return result;
}
Object.defineProperty(Object.prototype, 'getDeep', {
value: getDeep,
writable: true,
configurable: true,
enumerable: false
});
}());
I have something similar used to show data in grids, those grids may show the same objects yet no the same columns. however I don't handle that in one go.
I have a type service where I declare my types and some default configuration
I have a grid service which generates the grid definition options according to what I specified.
In the controller I instantiate the grid using the grid service, specifying the ordering of the columns and some specific configurations, which override the default ones. The grid service itself generate appropriate configuration for filtering, ordering using the type definition of the fields.
Angular.js ngModel has the ability to declare a chain of parsers and formatters. Some more details can be found at the great answer to 'How to do two-way filtering in angular.js?'
now the formatter chain only will be run if the ngModel will update.
so if you have a second input-parameter that affects the viewValue (is used in one of the formatters) this will not trigger an update of the View.
similar as far as i found ngModel only uses a simple $watch - so if your model is a collection/object it will not trigger if sub-elements are changed.
What is the best way to implement a deep watch for ngModel -
or a watch for a additional parameter that should rerun the formatter chain?
there are other similar questions:
Angularjs: how to “rerun” $formatters when some setting is changed?
currently there is no direct api to call the internal formatter chain.
there is a github feature request for this. as work-around you just can copy the internal code:
function runFormatters(ctrl){
// this function is a copy of the internal formatter running code.
// https://github.com/angular/angular.js/issues/3407#issue-17469647
var modelValue = ctrl.$modelValue;
var formatters = ctrl.$formatters;
var idx = formatters.length;
var viewValue = modelValue;
while (idx--) {
viewValue = formatters[idx](viewValue);
}
if (ctrl.$viewValue !== viewValue) {
ctrl.$viewValue = ctrl.$$lastCommittedViewValue = viewValue;
ctrl.$render();
ctrl.$$runValidators(modelValue, viewValue, angular.noop);
}
}
this Plunker demonstrates the usage in combination with a watch for additional parameters:
// deepwatch all listed attributes
scope.$watch(
function(){
return [scope.extraThingToWatchFor, scope.someOther];
},
function() {
console.log("\t runformatters()");
runFormatters();
},
true
);
this is a second Plunker to demonstrate the deepwatch on ngModel
// deepwatch ngModel
scope.$watch(
function(){
return ngModelCtrl.$modelValue;
},
function(newData) {
runFormatters(ngModelCtrl);
},
true
);
I am a strong advocate of best practices, especially when it comes to angular but I can't manage to use the brand new $validators pipeline feature as it should be.
The case is quite simple: 1 input enhanced by a directive using $parser, $formatter and some $validators:
<input name="number" type="text" ng-model="number" number>
Here is the (simplified) directive:
myApp.directive('number', [function() {
return {
restrict: 'A',
require: 'ngModel',
/*
* Must have higher priority than ngModel directive to make
* number (post)link function run after ngModel's one.
* ngModel's priority is 1.
*/
priority: 2,
link: function($scope, $element, $attrs, $controller) {
$controller.$parsers.push(function (value) {
return isFinite(value)? parseInt(value): undefined;
});
$controller.$formatters.push(function (value) {
return value.toString() || '';
});
$controller.$validators.minNumber = function(value) {
return value && value >= 1;
};
$controller.$validators.maxNumber = function(value) {
return value && value <= 10;
};
}
};
}]);
I made a little plunk to play with :)
The behavior I am trying to achieve is: Considering that the initial value stored in the scope is valid, prevent it from being corrupted if the user input is invalid. Keep the old one until a new valid one is set.
NB: Before angular 1.3, I was able to do this using ngModelController API directly in $parser/$formatter. I can still do that with 1.3, but that would not be "angular-way".
NB2: In my app I am not really using numbers, but quantities.The problem remains the same.
It looks like you want some parsing to happen after validation, setting the model to the last valid value rather than one derived from the view. However, I think the 1.3 pipeline works the other way around: parsing happens before validation.
So my answer is to just do it as you would do it in 1.2: using $parsers to set the validation keys and to transform the user's input back to the most recent valid value.
The following directive does this, with an array of validators specified within the directive that are run in order. If any of the previous validators fails, then the later ones don't run: it assumes one validation error can happen at a time.
Most relevant to your question, is that it maintains the last valid value in the model, and only overwrites if there are no validation errors occur.
myApp.directive('number', [function() {
return {
restrict: 'A',
require: 'ngModel',
/*
* Must have higher priority than ngModel directive to make
* number (post)link function run after ngModel's one.
* ngModel's priority is 1.
*/
priority: 2,
link: function($scope, $element, $attrs, $controller) {
var lastValid;
$controller.$parsers.push(function(value) {
value = parseInt(value);
lastValid = $controller.$modelValue;
var skip = false;
validators.forEach(function(validator) {
var isValid = skip || validator.validatorFn(value);
$controller.$setValidity(validator.key, isValid);
skip = skip || !isValid;
});
if ($controller.$valid) {
lastValid = value;
}
return lastValid;
});
$controller.$formatters.push(function(value) {
return value.toString() || '';
});
var validators = [{
key: 'isNumber',
validatorFn: function(value) {
return isFinite(value);
}
}, {
key: 'minNumber',
validatorFn: function(value) {
return value >= 1;
}
}, {
key: 'maxNumber',
validatorFn: function(value) {
return value <= 10;
}
}];
}
};
}]);
This can be seen working at http://plnkr.co/edit/iUbUCfJYDesX6SNGsAcg?p=preview
I think you are over-thinking this in terms of Angular-way vs. not Angular-way. Before 1.3 using $parsers pipeline was the Angular-way and now it's not?
Well, the Angular-way is also that ng-model sets the model to undefined (by default) for invalid values. Follow that Angular-way direction and define another variable to store the "lastValid" value:
<input ng-model="foo" ng-maxlength="3"
ng-change="lastValidFoo = foo !== undefined ? foo : lastValidFoo"
ng-init="foo = lastValidFoo">
No need for a special directive and it works across the board in a way that doesn't try to circumvent what Angular is doing natively - i.e. the Angular-way. :)
As of Angular 1.3 you can use the ngModelOptions directive to have greater control as to when your model value updates. Take a look at this updated Plunker to show you how to achieve the functionality you are looking for: http://plnkr.co/edit/DoWbvlFMEtqF9gvJCjPF?p=preview
Basically you define the model as a getterSetter and only return the new value if it is valid:
$scope.validNumber = function(value) {
return angular.isDefined(value) ? ($scope.number = value) : $scope.number;
}
$scope.modelOptions = {
getterSetter: true,
allowInvalid: false
};
Then to use this code update your as follows:
<input name="number" type="text" ng-model="validNumber" ng-model-options="modelOptions" number>
I really hope this answers all of your questions, please let me know if I can help any more.
Leon.
Here is my plnkr with the relevant code:
$controller.$$runValidators = function(originalRun) {
var lastModelValue, lastViewValue;
return function() {
var ctrl = this;
var doneCallback = arguments[arguments.length-1];
arguments[arguments.length-1] = function(allValid) {
doneCallback(allValid);
console.log(allValid);
console.log('valid:' +allValid+ ' value:' +ctrl.$viewValue);
if (ctrl.$viewValue) {
lastViewValue= allValid ? ctrl.$viewValue : lastViewValue | '';
lastModelValue= allValid ? ctrl.$modelValue : lastModelValue;
ctrl.$modelValue = allValid ? ctrl.$modelValue : lastModelValue;
ctrl.$$writeModelToScope();
ctrl.$viewValue = ctrl.$$lastCommittedViewValue = lastViewValue;
ctrl.$render();
}
console.log(ctrl.$viewValue + ' '+lastViewValue);
// console.log( ctrl.$modelValue);
};
originalRun.apply(this, arguments);
}
}($controller.$$runValidators);
Can it be a valid solution?
the only way i think you can intercept the angular validation flow is override the $$runValidators. Maybe this code need a little bit of tweaking but works.
I have an angularjs app, which validates certain input fields. I was looking to write unit tests via Jasmine to test and maintain the validity of these fields.
NOTE: The validation works fine normally, just with jasmine, it doesn't seem to update.
The unit tests have no syntax errors, but simply result in:
Error: Expected false to equal true.
at new jasmine.ExpectationResult
at null.toEqual
at null.<anonymous>
at jasmine.Block.execute
at jasmine.Queue.next_
at chrome-extension
For instance, I have, in the directives:
}).directive('billingNumberPopup', function() {
return {
require: 'ngModel',
link: function(scope, elm, attrs, ctrl) {
scope.$watch(
function() {
return ctrl.$viewValue;
},
function(value){
numValidation(value);
}
);
function numValidation(viewValue){
if (!viewValue || viewValue == "" || (!viewValue.toString().match(/[a-z]/gi) && viewValue.toString().match(/[0-9]/g).length == 6)){
ctrl.$setValidity('billingNumber',true);
}
else
{
ctrl.$setValidity('billingNumber',false);
}
and then from my unit tests...
it('Check if validation works', function(){
var len = $scope.dataToPost.length;
$scope.addRow();
console.log("Hi");
$scope.$apply(function(){
$scope.dataToPost[len].billingNumber = "HELLO";});
$scope.$apply();
console.log($scope.dataToPost[len].billingNumber);
console.log($("input[ng-model='d.billingNumber']"));
expect($("input[ng-model='d.billingNumber']")[len].classList.contains("ng-invalid")).toEqual(true);
});
where "HELLO" is not a valid billing number, and scope.dataToPost is the data that is binded to the input fields. I would assume, that changing the value, and calling $scope.$apply would trigger validation, any suggestions?
The jasmine error indicates that you are trying to access a null object.
This appears to occur when you are accessing the len instead of the len - 1 index of the array. Try changing the expectation to:
expect($("input[ng-model='d.billingNumber']")[len - 1].classList.contains("ng-invalid")).toEqual(true);