I am very new to angularjs and I just cant figure out how I would go about validating my inputs.
I have an input which should never be empty. The model will always have a value. Whenever a user inputs invalid data (i.e nothing or maybe invalid characters) I would like the input to revert to its original value.
For instance, If the initial value was 50 and I deleted all of that (or entered text) and deselected the input, I would expect the input's value to change back to 50.
Here is my controller:
var MyController = null;
app.controller("MyController", ["$scope", function($scope) {
$scope.data = myData;
$scope.validate = function(value, oldValue) {
if(!value || value == "") {
// Do something after validation
value = oldValue;
}
// Our value should be valid now (either replaced or changed)
}
MyController = $scope;
}]);
And my HTML (i would rather not have to type data.something twice if possible):
<input type="text" ng-model="data.something" ng-change="validate()" />
Small Clarification: If the value of the input is "50" and a user removes the "0", the input would be at "5" which is valid. However if the user adds a "x" after I DONT want the input to change, it should still be at "5". However if all the input is empty, I would like onBlur for the original value to be placed back into the input and the model unchanged.
Yep, you need ng-blur and probably ng-focus:
<input type="text" ng-model="data.something" ng-focus="store()" ng-blur="revertIfInvalid()"/>
$scope.store = function() {
$scope.stored = $scope.data.something;
}
$scope.revertIfInvalid= function() {
if (!$scope.data.something) {
$scope.data.something = $scope.stored;
}
}
This will work, but to make it reusable, you then want to make directive for this, like:
app.directive('fancydirective', function() {
return {
restrict: 'A',
require: 'ngModel',
link: function(sc, elem, attrs, ngModelCtrl) {
var stored;
elem.on('focus', function() {
sc.$apply(function() {
stored = ngModelCtrl.$modelValue;
});
});
elem.on('blur', function() {
sc.$apply(function() {
if (ngModelCtrl.$invalid) {
ngModelCtrl.$setViewValue(stored);
ngModelCtrl.$render();
}
});
});
}
}
});
http://plnkr.co/edit/fiRKS765Kyh6ikRqc8if?p=preview
Related
This is the first time when I am using and try to learn angular and typescript, I have an input field that I want to use a currency format directive for that and I searched and found proper directive but when the user wants to delete the input number and use backspace button, after deleting input numbers, 0 will show in input and then user have to press backspace again to clear the input field, I find that $filter(number) is doing this and I use a simple code to prevent that.
here is my code
mymodule.directive("currency", function($filter) {
"use strict";
return {
require: "?ngModel",
link: function(scope, elem, attrs, ctrl) {
if (!ctrl) {
return;
}
ctrl.$formatters.unshift(function() {
return $filter("number")(ctrl.$modelValue);
});
ctrl.$parsers.unshift(function(viewValue) {
var num= viewValue.replace(/[\,\.]/g, ""),
b = $filter("number")(num);
// i add this if to check if the value is zero don't show in input
if (b == 0) {
elem.val("");
} else {
elem.val(b);
}
return num;
});
}
};
})
but I am not sure that this is a good idea.
Is there any other way to achieve what I want?
I have the following html:
<input type="checkbox" ng-model="user.show_value" ng-blur="update_show_value()">
A value (true / false) is fetched from the database and passed to the ng-model. Depending on it, the checkbox is checked / uncheked. The function inside ng-blur triggers the update in the database and works:
$scope.update_show_value() = function() {
if ($scope.user.show_value != undefined) {
$scope.loading = true;
//IF VALUE IS VALID, CALL THE UPDATEPIN FUNCTIONn
User.updatevalue($scope.user)
//IF SUCCESSFUL, GET VALUE
.success(function(data) {
$scope.loading = false;
$scope.formData = {}; //CLEAR FORM SO THAT USER CAN ENTER NEW DATA
$scope.user.show_value = {type : $scope.user[0].show_value}; //PASS VALUE IN OUR SCOPE
});
}
};
The issue is that I would have to use the checkbox from other devices that don't support the click event. From these devices I should use the equivalent of enter (keycode 13). So I added the onkeydown event to detect when the enter key is being pressed on the checkbox. Using an example from w3schools, I see it works (here is the example http://www.w3schools.com/jsref/tryit.asp?filename=tryjsref_event_key_keycode3)
<input type="checkbox" ng-model="user.show_value" onkeydown="keyCode(event)" ng-blur="update_show_value()">
Now I want to call the update function when the onkeydown event detects that the code 13 was pressed. Something like this:
<script>
function keyCode(event) {
var x = event.keyCode;
if (x == 13) {
alert ("You pressed the Escape key!");
update_show_value();
}
}
</script>
However, calling update_show_value inside the keycode function does not work. Actually, adding update_show_value inside the keycode function causes everything else not to work (ex. the alert)
So for some reason I think that scope functions cannot be called inside javascript functions. If that is true, is there a workaround?
You can get the scope object outside of Angular by using:
angular.element(event.target).scope(); // and then call whatever functions you want to on that scope object
You can define your own directive to handle keydown
angular.directive('myKeydown', [ function () {
return {
link: function(scope, elem, attrs) {
element.bind('keydown', function() {
scope.update_show_value();
});
}
};
}]);
Or just use ng-keydown instead: https://docs.angularjs.org/api/ng/directive/ngKeydown :
$scope.keyCode = function($event) {
. . .
$scope.update_show_value();
};
Remove () from $scope.update_show_value
Try Below Code:
<script>
var app=angular.module("myApp", []);
app.controller("myCtrl", function($scope) {
$scope.update_show_value = function() {
alert(1);
};
});
</script>
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.
I have created an application in angularjs with ckeditor plugin, I have created a directive for ckeditor, The application is working fine but the issue is that i need to set a max character length to be 50, so i put maxlength="50", but its not working,
Can anyone please tell me some solution for this
JSFiddle
html
<div data-ng-app="app" data-ng-controller="myCtrl">
<h3>CKEditor 4.2:</h3>
<div ng-repeat="editor in ckEditors">
<textarea data-ng-model="editor.value" maxlength="50" data-ck-editor></textarea>
<br />
</div>
<button ng-click="addEditor()">New Editor</button>
</div>
script
var app = angular.module('app', []);
app.directive('ckEditor', [function () {
return {
require: '?ngModel',
link: function ($scope, elm, attr, ngModel) {
var ck = CKEDITOR.replace(elm[0]);
ck.on('pasteState', function () {
$scope.$apply(function () {
ngModel.$setViewValue(ck.getData());
});
});
ngModel.$render = function (value) {
ck.setData(ngModel.$modelValue);
};
}
};
}])
function myCtrl($scope){
$scope.ckEditors = [{value: ''}];
}
You need to add an id to your textarea, like this:
<textarea data-ng-model="editor.value" maxlength="50" id="mytext" data-ck-editor></textarea>
You need to handle the key events for CKEDITOR:
window.onload = function() {
CKEDITOR.instances.mytext.on( 'key', function() {
var str = CKEDITOR.instances.mytext.getData();
if (str.length > 50) {
CKEDITOR.instances.mytext.setData(str.substring(0, 50));
}
} );
};
This works, however, note, that the content contains html tags as well, you might want to keep them,
I came across this same issue. I made this function which works with CKEditor to basically mimic the maxlength function.
window.onload = function() {
CKEDITOR.instances.mytext.on('key',function(event){
var deleteKey = 46;
var backspaceKey = 8;
var keyCode = event.data.keyCode;
if (keyCode === deleteKey || keyCode === backspaceKey)
return true;
else
{
var textLimit = 50;
var str = CKEDITOR.instances.mytext.getData();
if (str.length >= textLimit)
return false;
}
});
};
This function will check to make sure the input does not have more than the allowed characters.
If it does it will return false which simply does not allow any more inputs into the field.
If the user presses backspace or delete then it will return true which still allows the user to change their content if they reach the content limit.
I'd like to watch my hide and show expressions on all elements in my app.
I know I can do it by wrapping the show directive with a function which just returns the argument:
<div ng-show="catchShow(myShowExpr == 42)"></div>
However, I'd like to watch all hide/shows across all inputs in my app and the above isn't good enough.
I could also overload the ngShow / ngHide directives though I'd need to reevaluate the expression.
I could also just modify the source since it's quite simple:
var ngShowDirective = ['$animator', function($animator) {
return function(scope, element, attr) {
var animate = $animator(scope, attr);
scope.$watch(attr.ngShow, function ngShowWatchAction(value) {
var fn = toBoolean(value) ? 'show' : 'hide';
animate[fn](element);
//I could add this:
element.trigger(fn);
});
};
}];
Though then I couldn't use the Google CDN...
Is there a nicer way anyone can think of to do this ?
Here's what I've come up with (CoffeeScript)
getDirective = (isHide) ->
['$animator', ($animator) ->
(scope, element, attr) ->
animate = $animator(scope, attr)
last = null
scope.$watch attr.oaShow, (value) ->
value = not value if isHide
action = if value then "show" else "hide"
if action isnt last
scope.$emit 'elementVisibility', { element, action }
animate[action] element
last = action
]
App.directive 'oaShow', getDirective(false)
App.directive 'oaHide', getDirective(true)
Converted to JavaScript:
var getDirective = function(isHide) {
return ['$animator', function($animator) {
//linker function
return function(scope, element, attr) {
var animate, last;
animate = $animator(scope, attr);
last = null;
return scope.$watch(attr.oaShow, function(value) {
var action;
if (isHide)
value = !value;
action = value ? "show" : "hide";
if (action !== last) {
scope.$emit('elementVisibility', {
element: element,
action: action
});
animate[action](element);
}
return last = action;
});
};
}];
};
App.directive('oaShow', getDirective(false));
App.directive('oaHide', getDirective(true));
Another way to approach is to $watch the attribute of ngShow - although this would need to be done within a directive (useful if you're show/hiding an already custom directive)
scope.$watch attrs.ngShow, (shown) ->
if shown
# Do something if we're displaying
else
# Do something else if we're hiding