I downloaded a directive that receives only numbers and has some additional options; but, after running it I get a rootScope error in one of the options that is:
<input type="text" ng-model="mynumber" nks-only-number allow-decimal="false" />
I believe the false conditional is making this error appear, but I don't know why.
Here is the demo:
http://jsfiddle.net/RmDuw/896/
Code:
(function(){
angular.module('myApp', [])
.directive('nksOnlyNumber', function () {
return {
restrict: 'EA',
require: 'ngModel',
link: function (scope, element, attrs, ngModel) {
scope.$watch(attrs.ngModel, function(newValue, oldValue) {
var spiltArray = String(newValue).split("");
if(attrs.allowNegative == "false") {
if(spiltArray[0] == '-') {
newValue = newValue.replace("-", "");
ngModel.$setViewValue(newValue);
ngModel.$render();
}
}
if(attrs.allowDecimal == "false") {
newValue = parseInt(newValue);
ngModel.$setViewValue(newValue);
ngModel.$render();
}
if(attrs.allowDecimal != "false") {
if(attrs.decimalUpto) {
var n = String(newValue).split(".");
if(n[1]) {
var n2 = n[1].slice(0, attrs.decimalUpto);
newValue = [n[0], n2].join(".");
ngModel.$setViewValue(newValue);
ngModel.$render();
}
}
}
if (spiltArray.length === 0) return;
if (spiltArray.length === 1 && (spiltArray[0] == '-' || spiltArray[0] === '.' )) return;
if (spiltArray.length === 2 && newValue === '-.') return;
/*Check it is number or not.*/
if (isNaN(newValue)) {
ngModel.$setViewValue(oldValue || '');
ngModel.$render();
}
});
}
};
});
}());
I believe the problem, looking at your pasted code (not the different JSFiddle), is that ngModel.$render() gets called twice. If I delete it from either the attrs.allowDecimal == false conditional or the end isNaN(newValue) conditional, the code runs fine.
Since I'm not sure what your end goal is, I've neglected to actually rewrite your code. But, that solved the infinite $digest loop error.
Related
I have written an AngularJS directive to validate percent value,
AngularJS Directive
app.directive('validatePercent', function () {
return {
restrict: 'A',
link: function ($scope, elem, attr) {
$scope.$watch(function () { return elem.val(); },
function (newVal, oldVal) {
console.log("old : ", oldVal);
console.log("new : ", newVal);
if (newVal < 0 || newVal > 100)
{
elem.val(oldVal);
}
}
);
}
};
});
Here's my HTML
<input validate-percent ng-model="obj.progress" type="number" class="form-control" />
Note : obj.progress is of type int, also input type is number
The issue is when I try to change value of this input field multiple times quickly one after the another value goes to -1 or even 101 sometimes. Although condition in my directive is newVal < 0 || newVal > 100
Need help.
UPDATE 1:
This happens only when user changes values using mouse wheel. It doesn't happens while increment or decrements by arrow keys on keyboard.
Instead of using $watch, you can handle it using focus/blur events.
app.directive('validatePercent', function () {
return {
restrict: 'A',
link: function ($scope, elem, attr) {
var oldVal = elem.val();
elem.bind('focus', function(e) {
oldVal = elem.val();
});
elem.bind('blur', function(e) {
if ( elem.val()< 0 || elem.val() > 100)
{
elem.val(oldVal);
}
});
}
};
});
Hope it helps.
Consider the following simple md-input-container with textarea inside:
<md-input-container class="md-block">
<textarea aria-label="tt" ng-model="obj.prop" md-maxlength="250" rows="5"></textarea>
</md-input-container>
When i update obj.prop in my controller, the text is changed (therefore i should call $scope.$evalAsync() too). However, the md-char-count is still not updated. In order it to be updated, user will click on text-area and change it. Only then it is being changed.
I think its a known bug but is there any improvements or is there a -at least- a workaround to this problem?
Here you find a codepen
ps(if needed):angular-material version 1.0.1 & angular 1.4.5
I resolved this issue by override - md-maxlength directive.
Try this codepen - http://codepen.io/himvins/pen/JEgyOK?editors=1010
Function for overriding md-maxlength:
function override_mdMaxlength($provide) {
$provide.decorator(
'mdMaxlengthDirective',
function($delegate) {
var mdMaxlength = $delegate[0];
var link = mdMaxlength.link;
mdMaxlength.compile = function() {
//Line 18 to 64: Code of md-maxlength directive. Change in line 62
return function(scope, element, attr, ctrls) {
var maxlength;
var ngModelCtrl = ctrls[0];
var containerCtrl = ctrls[1];
var charCountEl = angular.element('<div class="md-char-counter">');
attr.$set('ngTrim', 'false');
containerCtrl.element.append(charCountEl);
ngModelCtrl.$formatters.push(renderCharCount);
ngModelCtrl.$viewChangeListeners.push(renderCharCount);
element.on(
'input keydown',
function() {
renderCharCount(); //make sure it's called with no args
}
);
scope.$watch(attr.mdMaxlength, function(value) {
maxlength = value;
if (angular.isNumber(value) && value > 0) {
if (!charCountEl.parent().length) {
$animate.enter(
charCountEl,
containerCtrl.element,
angular.element(containerCtrl.element[0].lastElementChild)
);
}
renderCharCount();
} else {
$animate.leave(charCountEl);
}
});
ngModelCtrl.$validators['md-maxlength'] = function(modelValue, viewValue) {
if (!angular.isNumber(maxlength) || maxlength < 0) {
return true;
}
return (modelValue || element.val() || viewValue || '').length <= maxlength;
};
function renderCharCount(value) {
//Original code commented
debugger;
if(ngModelCtrl.$modelValue !== "")
charCountEl.text( ( element.val() || value || '' ).length + '/' + maxlength );
else
charCountEl.text((ngModelCtrl.$modelValue || '').length + '/' + maxlength);
return value;
}
};
};
return $delegate;
}
);
}
Add this function into your module's config like below:
yourModule.config(override_mdMaxlength);
This will correct character-count behavior.
I meet this bug too, after look deeper into the mdMaxlength directive, I found that we could solve this problem easily like this(according to your code):
angular.module('app').controller(function($timeout){
$scope.obj={prop:""};
//after user input some value reset the form
$scope.reset=function(){
$scope.obj.prop=null;
$scope.formName.$setPristine();
$scope.formName.$setValidity();
$scope.formName.$setUntouched();
$timeout(function(){
$scope.obj.prop="";
})
}
})
I'm trying to buld a custom directive that is actually a wrapper around input field (to simplify formatting, encapsulate animations, etc.).
One goal is to use ngModel so my directive would also be compatible with ng-maxlength, ng-required and similar directives depending on ng-Model.
I've created this plunkr with my current state:
http://embed.plnkr.co/xV8IRqTmQmKEBhRhCfBQ/
My problem is that ng-required seems to be working, but invalidates only the complete form (so that form.$invalid becomes true), but not the element itself form.element.$invalid remains false.
Also, ng-maxlength / ng-minlength does not seem to have any effect at all.
What am I missing here? Any hints welcome :)
Hi everyone and thanks a lot for your answers!
I finally figured out what the missing piece for me was: the name attribute which is used by the form to reference the element MUST NOT be on the inner input field.
It has to reside on the outer element that carries the mg-model that also gets the other directives (that interact with the ng-model).
So, to illustrate this in more detail, before my template looked like:
<span class="custom-input-element">
<label for="{{elementId}}-input">{{elementLabel}}<span class="required-marker" ng-if="elementRequired">*</span></label>
<input id="{{elementId}}-input" type="text" name="{{elementName}}" ng-trim ng-model="value" ng-init="focused = false" ng-focus="focused = true" ng-blur="focused = false"/>
</span>
Which was used like
<custom-input id="foldername" name="foldername" label="Folder Name:"
ng-model="folder.name" ng-maxlength="15" ng-required="true"> </custom-input>
Notice the name={{elementName}} that basically overlayed the name="foldername" on my directive's tag.
After removing it from the directives template, the form references my directive and the ngModel on my directive for validation - the input and the inner ng-model keeps hidden. Thus, the interaction with other directives like ng-maxlength and mg-minlength and also custom directives/validators works as expected.
So now, not only the form gets invalidated but also each element is validated in the expected way.
I updated my plunker where everything is working as desired now: http://embed.plnkr.co/i3SzV8H7tnkUk2K9Pq6m/
Thanks for your time and your very valuable input!
I have created one that works, i'll try to show you the relevant part of the code.
The one really annoying point was to reattach the input and the validation to the form of the parent controller.
For this i had to cc a bunch of private code from angular :
/**
* start cc from angular.js to modify $setValidity of ngModel to retrieve the parent form...
*/
var VALID_CLASS = 'data-ng-valid',
INVALID_CLASS = 'data-ng-invalid',
PRISTINE_CLASS = 'data-ng-pristine',
DIRTY_CLASS = 'data-ng-dirty',
UNTOUCHED_CLASS = 'data-ng-untouched',
TOUCHED_CLASS = 'data-ng-touched',
PENDING_CLASS = 'data-ng-pending';
function addSetValidityMethod(context) {
var ctrl = context.ctrl,
$element = context.$element,
classCache = {},
set = context.set,
unset = context.unset,
parentForm = context.parentForm,
$animate = context.$animate;
classCache[INVALID_CLASS] = !(classCache[VALID_CLASS] = $element.hasClass(VALID_CLASS));
ctrl.$setValidity = setValidity;
function setValidity(validationErrorKey, state, controller) {
if (state === undefined) {
createAndSet('$pending', validationErrorKey, controller);
} else {
unsetAndCleanup('$pending', validationErrorKey, controller);
}
if (!isBoolean(state)) {
unset(ctrl.$error, validationErrorKey, controller);
unset(ctrl.$$success, validationErrorKey, controller);
} else {
if (state) {
unset(ctrl.$error, validationErrorKey, controller);
set(ctrl.$$success, validationErrorKey, controller);
} else {
set(ctrl.$error, validationErrorKey, controller);
unset(ctrl.$$success, validationErrorKey, controller);
}
}
if (ctrl.$pending) {
cachedToggleClass(PENDING_CLASS, true);
ctrl.$valid = ctrl.$invalid = undefined;
toggleValidationCss('', null);
} else {
cachedToggleClass(PENDING_CLASS, false);
ctrl.$valid = isObjectEmpty(ctrl.$error);
ctrl.$invalid = !ctrl.$valid;
toggleValidationCss('', ctrl.$valid);
}
// re-read the state as the set/unset methods could have
// combined state in ctrl.$error[validationError] (used for forms),
// where setting/unsetting only increments/decrements the value,
// and does not replace it.
var combinedState;
if (ctrl.$pending && ctrl.$pending[validationErrorKey]) {
combinedState = undefined;
} else if (ctrl.$error[validationErrorKey]) {
combinedState = false;
} else if (ctrl.$$success[validationErrorKey]) {
combinedState = true;
} else {
combinedState = null;
}
toggleValidationCss(validationErrorKey, combinedState);
parentForm.$setValidity(validationErrorKey, combinedState, ctrl);
}
function createAndSet(name, value, controller) {
if (!ctrl[name]) {
ctrl[name] = {};
}
set(ctrl[name], value, controller);
}
function unsetAndCleanup(name, value, controller) {
if (ctrl[name]) {
unset(ctrl[name], value, controller);
}
if (isObjectEmpty(ctrl[name])) {
ctrl[name] = undefined;
}
}
function cachedToggleClass(className, switchValue) {
if (switchValue && !classCache[className]) {
$animate.addClass($element, className);
classCache[className] = true;
} else if (!switchValue && classCache[className]) {
$animate.removeClass($element, className);
classCache[className] = false;
}
}
function toggleValidationCss(validationErrorKey, isValid) {
validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : '';
cachedToggleClass(VALID_CLASS + validationErrorKey, isValid === true);
cachedToggleClass(INVALID_CLASS + validationErrorKey, isValid === false);
}
}
function arrayRemove(array, value) {
var index = array.indexOf(value);
if (index >= 0) {
array.splice(index, 1);
}
return index;
}
function isBoolean(value) {
return typeof value === 'boolean';
};
var SNAKE_CASE_REGEXP = /[A-Z]/g;
function snake_case(name, separator) {
separator = separator || '_';
return name.replace(SNAKE_CASE_REGEXP, function(letter, pos) {
return (pos ? separator : '') + letter.toLowerCase();
});
}
function isObjectEmpty(obj) {
if (obj) {
for (var prop in obj) {
if (obj.hasOwnProperty(prop)) {
return false;
}
}
}
return true;
};
/**
* end of cc
*/
Then in the link function :
function(scope, element, attrs, ctrl, transclude){
[...]
scope.form = element.parent().controller('form');
var transcludedContent = transclude(scope.$parent);
// find the input
var fieldContent = findFormField(transcludedContent);
var ngModelCtrl = angular.element(fieldContent).controller('ngModel');
if(!ngModelCtrl){
throw 'transcluded form field must have a ng-model';
}
addSetValidityMethod({
ctrl: ngModelCtrl,
$element: angular.element(fieldContent),
set: function(object, property, controller) {
var list = object[property];
if (!list) {
object[property] = [controller];
} else {
var index = list.indexOf(controller);
if (index === -1) {
list.push(controller);
}
}
},
unset: function(object, property, controller) {
var list = object[property];
if (!list) {
return;
}
arrayRemove(list, controller);
if (list.length === 0) {
delete object[property];
}
},
parentForm: scope.form,
$animate: $animate
});
scope.form.$addControl(ngModelCtrl);
element.html(template);
$compile(element.contents())(scope);
element.find('.ng-form-field-content').append(transcludedContent);
// remove the control from the form, otherwise an ng-if that hide an invalid input will block your form
scope.$on(
"$destroy",
function handleDestroyEvent() {
scope.form.$removeControl(ngModelCtrl);
});
The template is a variable containing the html of my wrapping around the input. (it generates the label, put a start if required, show a check or cross sign if field valid/invalid,...).
EDIT :
With my directive i can do :
<div my-directive>
<input/textarea/select ng-model="", required/ng-required, ng-pattern, <custom directive validation>...
</div>
And it will give something like
<div my-directive>
<label for=<input'sname>>Texte</label>
<input [the input will all his attrs]/>
[some extra content]
</div>
I can even put some intermediary nodes or have multiple input that point to the same ng-model (like with checkbox/Radio buttons), however it won't works with different ng-models. I didn't push it that far.
My goal:
Input field for end date:
<input class="input-sm form-control" unlimited-input="" ng-model="something.EndDate" placeholder="{{'PLACEHOLDER_DDMMYYYY' | translate}}" ng-change-on-blur="updateSomething(something)">
Directive unlimitedInput:
app.directive('unlimitedInput', function($parse) {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, elm, attrs, ngModelCtrl) {
var unlimitedEndValue = "31.12.2099";
var getter = $parse(attrs.ngModel); // get the value of ng-model
var oldValue = getter(scope);
elm.on('load', function() {
scope.$apply(function() {
ngModelCtrl.$viewValue = 'unlimited';
ngModelCtrl.$render();
});
});
elm.on('focus',function() {
if(oldValue==unlimitedEndValue){
scope.$apply(function() {
ngModelCtrl.$viewValue = '';
ngModelCtrl.$render();
});
}
});
elm.on('blur', function() {
scope.$apply(function() {
var newValue = elm.val();
if ((newValue != '') && (newValue!=oldValue)){
}else if(newValue == ''){
ngModelCtrl.$viewValue = oldValue;
ngModelCtrl.$render();
}
});
});
}
};
});
The Problem is the first event listener (load, ready does not work also), it does not work at all. What I want to achieve: when ng-model has a value and it is 31.12.2099 the user should see "unlimited". Blur and Focus work fine!
Thank you and sorry for my english, if something is not clear, ask:D
Update:
Now I have reached what I wanted, but one thing. I want to update the 'something.UnlimitedEnd' (unlimited in the input field) from the directive. I already tried with setter ($parse ...), I get the error, that "something" ia read only. In my understanding I achieve through scope:true I create a child sope with the two way binding, so scope.unlimited = false should work. My new problem is it assigns the value 'false' only in the directive scope also only one way binding. What I want now to achieve is two way binding for unlimited.
input field
<input class="input-sm form-control" unlimited-input ng-model="something.EndDate" placeholder="{{'PLACEHOLDER_DDMMYYYY' | translate}}"
ng-change-on-blur="dosomething(something)" unlimited="something.UnlimitedEnd"/>
Part of the directive:
restrict: 'EA',
require: 'ngModel',
scope: true,
link: function(scope, elm, attrs, ngModelCtrl) {
if (!ngModelCtrl) return;
var unlimitedEndValue = "31.12.2099";
var getter = $parse(attrs.ngModel);
var unlimitedGetter = $parse(attrs.unlimited);
var oldValue = getter(scope);
function setUnlimited(){
if(getter(scope)==unlimitedEndValue){
return 'unlimited';
}else{
return oldValue;
}
}
ngModelCtrl.$formatters.push(setUnlimited);
elm.on('focus',function() {
if(oldValue==unlimitedEndValue){
scope.$apply(function() {
ngModelCtrl.$setViewValue('');
scope.unlimited = should be updated!! so set false
ngModelCtrl.$render();
});
}
});
elm.on('blur', function() {
scope.$apply(function() {
var newValue = elm.val();
if(newValue == ''){
ngModelCtrl.$setViewValue(oldValue);
ngModelCtrl.$modelValue = oldValue;
ngModelCtrl.$render();
}else if(newValue=='unlimited' || unlimitedGetter(scope)){
ngModelCtrl.$setViewValue(unlimitedEndValue);
ngModelCtrl.$modelValue = unlimitedEndValue;
ngModelCtrl.$render();
}
});
});
}
So first day on the job with angularjs and i'm not quite getting it. I'm trying to mimic an html5 placeholder using an angular directive. It totally works until I add an ng-model to the field and then it only works after a user interacts with the field and also breaks any value the field had.
code up here
http://jsbin.com/esujax/32/edit
the directive
App.directive('placehold', function(){
return {
restrict: 'A',
link: function(scope, element, attrs) {
var insert = function() {
element.val(attrs.placehold);
};
element.bind('blur', function(){
if(element.val() === '')
insert();
});
element.bind('focus', function(){
if(element.val() === attrs.placehold)
element.val('');
});
if(element.val() === '')
insert();
}
}
});
the html
<textarea ng-model="comment" placehold="with a model it doesn't work"></textarea>
seems super simple but i'm lost
Just few modifications in your sample:
app.directive('placehold', function() {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, element, attr, ctrl) {
var value;
var placehold = function () {
element.val(attr.placehold)
};
var unplacehold = function () {
element.val('');
};
scope.$watch(attr.ngModel, function (val) {
value = val || '';
});
element.bind('focus', function () {
if(value == '') unplacehold();
});
element.bind('blur', function () {
if (element.val() == '') placehold();
});
ctrl.$formatters.unshift(function (val) {
if (!val) {
placehold();
value = '';
return attr.placehold;
}
return val;
});
}
};
});
You can test it here: http://plnkr.co/edit/8m54JO?p=preview
Not sure, that it is the best solution, anyway it works. Even if you type the same text, that you have in your placehold attribute, cause it checks the model's value on focus.
You can also checkout an Angular.JS module that implements the "placeholder" attribute for older web browsers:
https://github.com/urish/angular-placeholder-shim
I have created a placeholder directive that can take angularjs expressions and also hides the placeholder text on input. You can read about the placeholder at http://blog.f1circle.com/2013/09/supporting-placeholders-in-non-html5.html
Here is the gist.
(function(angular, app) {
"use strict";
app.directive('placeholder',["$document", "$timeout", function($document, $timeout){
var link = function(scope,element,attrs,ctrl){
// if you dont use modernizr library use the solution given at
// http://stackoverflow.com/questions/5536236/javascript-check-for-native-placeholder-support-in-ie8
// to check if placeholder is supported natively
if(Modernizr.input.placeholder){
return;
}
/*
The following keys all cause the caret to jump to the end of the input value
27, Escape
33, Page up
34, Page down
35, End
36, Home
Arrow keys allow you to move the caret manually, which should be prevented when the placeholder is visible
37, Left
38, Up
39, Right
40, Down
The following keys allow you to modify the placeholder text by removing characters, which should be prevented when the placeholder is visible
8, Backspace
46 Delete
*/
var pTxt, modelValue, placeholding = false, badKeys = [27,33,34,35,36,37,38,39,40,8,46];
var unplacehold = function(){
if(!placeholding){
return;
}
placeholding = false;
element.removeClass('placeholder');
element.val('');
};
var placehold = function(){
if(placeholding || modelValue){
return;
}
placeholding = true;
element.addClass('placeholder');
element.val(pTxt);
};
var moveCaret = function(elem, index) {
var range;
if (elem.createTextRange) {
range = elem.createTextRange();
range.move("character", index);
range.select();
} else if (elem.selectionStart) {
elem.focus();
elem.setSelectionRange(index, index);
}
};
attrs.$observe('placeholder',function(value){
pTxt = value;
placeholding = false;
placehold();
});
ctrl.$parsers.unshift(function (value){
modelValue = value;
if(!value){
placehold();
}
if(placeholding){
return '';
}
return value;
});
ctrl.$formatters.unshift(function (value){
if(!value){
placehold();
modelValue = '';
return pTxt;
}
return value;
});
element.on('click focus contextmenu',function(event){
if($document[0].activeElement !== this){
return;
}
if(!modelValue){
moveCaret(this,0);
}
});
element.on('blur',function(){
placehold();
});
element.on('keydown',function(e){
if(!placeholding){
return;
}
if(_.contains(badKeys,e.keyCode)){
if(e.preventDefault){
e.preventDefault();
}
return false;
}
unplacehold();
});
element.on('keyup',function(e){
if(modelValue){
return;
}
placehold();
moveCaret(this,0);
});
element.on('paste',function(e){
$timeout(function(){
modelValue = element.val();
},0);
});
};
return{
restrict: 'A',
require: 'ngModel',
link : link,
priority:3,
};
}]);
})(angular, app);
This works on all cases except when copy and paste the same placeholder text.