Directive for restricting typing by Regex in AngularJS - javascript

I coded an angular directive for inhibiting typing from inputs by specifying a regex. In that directive I indicate a regex that will be used for allow the input data. Conceptually, it works fine, but there are two bugs in this solution:
In the first Plunker example the input must allow only numbers or numbers followed by a dot [.], or numbers followed by a dot followed by numbers with no more than four digits.
If I type a value '1.1111' and after that I go to the first digit and so type another digit (in order to get a value as '11.1111') , nothing happening. The bug is in the fact I use the expression elem.val() + event.key on my regex validator. I do not know how to get the whole
current value for a input on a keypress event;
The second one is the fact that some characters (grave, acute, tilde, circumflex) are being allowed on typing (press one of them more than once), althought the regex does not allow them.
What changes do I need to make in my code in order to get an effective type restriction by regex?
<html ng-app="app">
<head>
<script data-require="angularjs#1.6.4" data-semver="1.6.4" src="https://code.angularjs.org/1.6.4/angular.min.js"></script>
<link rel="stylesheet" href="style.css" />
<script src="script.js"></script>
</head>
<body>
<h1>Restrict typing by RegExp</h1>
PATTERN 1 (^\d+$|^\d+[.]$|^\d+[.]\d{1,4}$) <input type="text" allow-typing="^\d+$|^\d+[.]$|^\d+[.]\d{1,4}$"/><br>
ONLY NUMBERS <input type="text" allow-typing="^[0-9]+$"/><br>
ONLY STRINGS <input type="text" allow-typing="^[a-zA-Z]+$"/>
</body>
</html>
Directive
angular.module('app', []).directive('allowTyping', function() {
return {
restrict: 'A',
link: function(scope, elem, attrs, ctrl) {
var regex = attrs.allowTyping;
elem.bind('keypress', function(event) {
var input = elem.val() + event.key;
var validator = new RegExp(regex);
if(!validator.test(input)) {
event.preventDefault();
return false;
}
});
}
};
});

If this were my code, I'd change tactics entirely: I would listen for input events instead of trying to micromanage the user interactions with the field.
The approach you are taking, in general, has problems. The biggest one is that keypress won't be emitted for all changes to the field. Notably,
It is not triggered by DELETE and BACKSPACE keys.
Input methods can bypass it. When you entered diacritics as diacritics, your code was not registering the change. In general, if the user is using an input method, there is no guarantee that each new character added to the field will result in a keypress event. It depends on the method the user has chosen.
keypress does not help when the user cuts from the field or pastes into the field.
You could add code to try to handle all the cases above, but it would get complex quick. You've already run into an issue with elem.val() + event.key because the keypress may not always be about a character inserted at the end of the field. The user may have moved the caret so you have to keep track of caret position. One comment suggested listening to keyup but that does not help with input methods or paste/cut events.
In contrast, the input event is generated when the value of the field changes, as the changes occur. All cases above are taken care of. This, for instance, would work:
elem.bind('input', function(event) {
var validator = new RegExp(regex);
elem.css("background-color", !validator.test(elem.val()) ? "red" : null);
});
This is a minimal illustration that you could plop into your fiddle to replace your current event handler. In a real application, I'd give the user a verbose error message rather than just change the color of the field and I'd create validator just once, outside the event handler, but this gives you the idea.
(There's also a change event but you do no want to use that. For text fields, it is generated when the focus leaves the field, which is much too late.)

See Plnkr Fixed as per your approach:
The explanation of why and the changes are explained below.
Side note: I would not implement it this way (use ngModel with $parsers and $formatters, e.g. https://stackoverflow.com/a/15090867/2103767) - implementing that is beyond the scope of your question. However I found a full implementation by regexValidate by Ben Lesh which will fit your problem domain:-
If I type a value '1.1111' and after that I go to the first digit and so type another digit (in order to get a value as '11.1111') , nothing happening.
because in your code below
var input = elem.val() + event.key;
you are assuming that the event.key is always appended at the end.
So how to get the position of the correct position and validate the the reconstructed string ? You can use an undocumented event.target.selectionStart property. Note even though you are not selecting anything you will have this populated (IE 11 and other browsers). See Plnkr Fixed
The second one is the fact that some characters (grave, acute, tilde, circumflex) are being allowed on typing (press one of them more than once), althought the regex does not allow them.
Fixed the regex - correct one below:
^[0-9]*(?:\.[0-9]{0,4})?$
So the whole thing looks as below
link: function(scope, elem, attrs, ctrl) {
var regex = attrs.allowTyping;
elem.bind('keypress', function(event) {
var pos = event.target.selectionStart;
var oldViewValue = elem.val();
var input = newViewValue(oldViewValue, pos, event.key);
console.log(input);
var validator = new RegExp(regex);
if (!validator.test(input)) {
event.preventDefault();
return false;
}
});
function newViewValue(oldViewValue, pos, key) {
if (!oldViewValue) return key;
return [oldViewValue.slice(0, pos), key, oldViewValue.slice(pos)].join('');
}
}

You specified 4 different patterns 3 different pattens in your regex separated by an alteration sign: ^\d+$|^\d+[.]$|^\d+[.]\d{1,4}$ - this will not fulfill the criteria of input must allow only numbers followed by a dot [.], followed by a number with no more than four digits. The bug "where nothing happens" occurs because the variable you are checking against is not what you think it is, check the screenshot on how you can inspect it, and what it is:
Can not reproduce.

You can change the event to keyup, so the test would run after every additional character is added.
It means you need to save the last valid input, so if the user tries to insert a character that'll turn the string invalid, the test will restore the last valid value.
Hence, the updated directive:
angular.module('app', [])
.directive('allowTyping', function() {
return {
restrict : 'A',
link : function(scope, elem, attrs, ctrl) {
var regex = attrs.allowTyping;
var lastInputValue = "";
elem.bind('keyup', function(event) {
var input = elem.val();
var validator = new RegExp(regex);
if (!validator.test(input))
// Restore last valid input
elem.val(lastInputValue).trigger('input');
else
// Update last valid input
lastInputValue = input;
});
}
};
});

Related

javascript code prevents typing except special characters

I was putting together some simple javascript to prevent characters being input in my form. I got to this stage where I was able to prevent all typing, and noticed that it prevents all characters except special ones, like å´ˆø`¨
which I can enter on my mac using: [Option]+`+[Letter key]
How can I prevent these being entered?
HTML:
<form>
<input name="myinput"></input>
</form>
JS:
document.querySelector('input').addEventListener('keypress', function (e) {
e.preventDefault();
return false;
});
fiddle:
https://jsfiddle.net/6qty7dgr/
This is not working because these keystrokes actually start a composition event, i.e the full input has not yet been processed by the IME and you have "half-a-character". So calling preventDefault() on the keydown event here won't prevent the typing of this half character and the browser will insert it in the input anyway.
document.querySelector('input').addEventListener('keypress', function (e) {
e.preventDefault();
});
document.querySelector('input').addEventListener('compositionstart', function (e) {
console.log("composition start fired");
e.preventDefault(); // should have worked per specs, but doesn't...
});
<input name="myinput"></input>
By specs, you should have been able to prevent this by calling preventDefault() on the compositionstart event that does fire. However, no browser actually seems to support cancelling this event and it seems that for technical reasons they won't support it ever. However they should support cancelling the beforeinput event, but as of today only Safari does support cancelling it when it comes from a composition event...
"I was putting together some simple javascript to prevent characters being input in my form."
"How can I prevent these being entered?"
Emphasis added by me
Avoid keyboard events if you are using a form control. Form events usually work better since there is very little variation when interpreting the value of an input as opposed to keyboard events.
In the example below, the <input> listens for the "input" event. As the user types into the <input>, the .value property is being filtered by the following Regexp:
const rgx = new RegExp(/([å´ˆø`¨])*/, 'g');
(...)- Capture group: matched characters that are extracted.
*----- Quantifier: zero or more matches.
[...]- Class: a literal character. -, \, ], and ^ must be escaped by prefixing \ to it.
å´ˆø``¨ are instantly replaced with a zero-space "".
Details are commented in example below
// Bind input tag to the input event
document.forms[0].elements.filter.oninput = dataFilter;
// Pass the Event Object
function dataFilter(e) {
// Delegate to input tag only
if (this.name == 'filter') {
/*
This matches
å´ˆø`¨
*/
const rgx = new RegExp(/([å´ˆø`¨])*/, 'g');
// Replace everything in rgx
const filtered = this.value.replace(rgx, '');
// Assign filtered string as value
this.value = filtered;
}
// Prevent input from rendering everything
e.preventDefault();
};
<form>
<input name='filter'>
</form>
Note: The onkeypress event is not fired for all keys (e.g. ALT, CTRL, SHIFT, ESC) in all browsers. To detect only whether the user has pressed a key, use the onkeydown event instead, because it works for all keys.
https://www.w3schools.com/jsref/event_onkeypress.asp

Characters being injected into input field

I am sure this is something really stupid I am missing.
I am writing a small snippet that adds a hyphen to a string grabbed from an input field. The hyphen is only added once we hit position 4, so I can type 123 and the hyphen will not appear. If I type 1234, it'll automatically change to 1234-. The problem is with handling pasting, somewhere down the line inside jQuery (after my code has executed), it's injecting more characters into the field.
My approach is simple enough. I look at the keyup and keydown event, check the input and insert the hyphen. For pasting I look at the paste even, grab the string, split it and insert a hyphen depending on if one is present or not.
$('[id$="field"]').on('paste', function (event) {
var element = this;
var text = event.originalEvent.clipboardData.getData('text').split('');
if (text.length > 4 && text.indexOf('-') < 0) {
text.splice(4, 0, '-');
$(element).val(text.join(''));
}
});
$('[id$="field"]').bind('keyup keydown', function (event) {
var input = $(this).val();
if (input.length === 4 && event.keyCode !== 8) {
$($(this).val(input + '-'));
}
});
The keyup and keydown listener works just fine. If I paste in 12345, I end up with 1234-5 when I hit $(element).val(text.join('')); yet afterwards that extra char pops whilst jQuery is doing its thing.
I am rather baffled.
Any ideas?
Since you are overriding the typical "paste" behavior by updating the value of the input box directly, you need to prevent the "default" paste behavior.
$('[id$="field"]').on('paste', function (event) {
event.preventDefault();
// ...

Twitter Typeahead differentiate between events

I'm designing a system that allows users to annotate elements with either strings or terms from a vocabulary. I use Twitter typeahead for autocompletion and want to distinguish between an entered string and a term.
I am not able to figure out how to differentiate between the following situations:
a user pressing enter after coming up with its own value
a user pressing a down (or up) arrow (thereby selecting an autocompletion option) and pressing enter
The event listeners I wrote:
$("#itemInp").on('typeahead:select', function(event, term) {
console.log("save: term", term);
});
$("#itemInp").on('keyup', function(event) {
if(event.which == 13) {
var string = $("#itemInp").val();
console.log("save: string", string);
}
});
With the following HTML:
<input id="itemInp"><input>
The first listener catches all selected typeahead terms, allowing for proper saving of the term. Problem is, the second listener is also triggered in case a user presses down and enter, selecting a typeahead term, which is now also saved as a plain string. Is there a way to not trigger the second listener in case a typeahead suggestion is selected?
A fiddle with the code loaded:
https://jsfiddle.net/zban3vs6/1/
Edit:
I considered a number of hacks, but all come with their own problems:
Add a special character to the string shown in input at the moment it is selected, by adding custom Typeahead display: https://jsfiddle.net/2t9rzhwf/
This causes an additional character to be introduced, which is troublesome if a user presses the down arrow without pressing enter.
Attempt at sequencing the listeners, have the term listener trigger first, set a boolean to true, and filter on this boolean in the second listener. Don't like this way of filtering, since it introduces a delay.
The solution provided by #peter-clause, checking if the selected value is in the list of available items. But that way I can not keep track of whether the user intended to use the autocomplete option to select a term or explicitly is adding a plain string.
I had a similar problem. This is how I solved it.
var wasSelected = false;
$("#itemInp").on('typeahead:select', function(event, term) {
wasSelected = true;
console.log("save: term", term);
});
$("#itemInp").on('change', function(event) {
if(!wasSelected) {
var string = $("#itemInp").val();
console.log("save: string", string);
}
wasSelected = false;
});
A rather hacky solution, but you could check if the selected value is in the list of available items.
if (states.indexOf(string) == -1) {
//...
}
See https://jsfiddle.net/zban3vs6/2/

How to combine Angular $parser with $validator?

I'm building an Angular directive which allows the user to enter a North American phone number in a variety of common formats (such as "1(301) 797-1483" or "301.797.1483"), but stores it internally as a normalized number in the form "3017971483".
I have the $parser working: it strips out all non-numeric characters as the user types, and strips off the first character if it's a "1". However, I'd like to add validation to this, such that:
If the $parser can't translate the current $viewValue to a valid normalized number (i.e., because it doesn't contain enough digits, or contains unacceptable junk characters), then:
$modelValue will be empty; and
the ModelController's $valid / $invalid flags will be set appropriately, and its $error.pattern property will be set to true. (I guess it doesn't have to be pattern, but that seems like the sensible one to use.)
This is basically how Angular handles default validation via attributes such as pattern and required. But I'm having a devil of a time figuring out how to make this work with my directive.
My code is below; you can also view this on CodePen. Here's my HTML:
<div ng-app="DemoApp">
<form ng-controller="MainController" name="phoneForm">
<input phone-input type="tel" name="phone" ng-model="phone">
<p ng-show="phoneForm.phone.$error.pattern">Invalid format!</p>
<p>$viewValue is: {{ phoneForm.phone.$viewValue }}</p>
<p>$modelValue is: {{ phone }}</p>
</form>
</div>
And here's my JavaScript:
angular
.module( 'DemoApp', [] )
.controller( 'MainController', [ '$scope', function( $scope ) {
$scope.phone = '';
} ] )
.directive( 'phoneInput', function() {
return {
restrict: 'A',
require: 'ngModel',
link: function( scope, $el, attrs, ngModel ) {
// convert human-friendly input to normalized format for internal storage
ngModel.$parsers.push( function( value ) {
return normalizePhone( value );
} );
function normalizePhone( phone ) {
// remove all non-numeric characters
phone = phone.replace( /[^0-9]/g, '' );
// if the first character is a "1", remove it
if ( phone[0] === '1' ) {
phone = phone.substr( 1 );
}
return phone;
}
}
};
} );
If you play with the form, you'll notice that the "Invalid format!" message is never shown. I don't expect this code to show it - that's what I'm trying to figure out how to do cleanly.
I already have a solid regex for determining whether the $viewValue can be translated into a valid number - e.g., "1(301) 797-1483" passes the regex, but "1(301) 797-148" does not. What I'm trying to figure out is where/when/how to perform this check, and where/when/how to flag the model as invalid.
Simply adding pattern="^regex_goes_here$" to my <input> doesn't work - that checks the format of the $modelValue after normalization, which is not what I want.
I've tried a bunch of different things, but nothing quite behaves the way I want, and I'm out of ideas at this point.
What is the "right" way to combine a $parser with a $validator? Surely there's an established pattern for this.
A method passed to $validators receives as parameters both $modelValue and $viewValue, which means that you can actually validate by both of them (pen - enter some letters):
ngModel.$validators.pattern = function($modelValue, $viewValue) {
var pattern = /^\d*$/; // whatever your pattern is
return pattern.test($viewValue);
};

Auto Updating the Value of an Input Field

I'm not sure how simple or how complicated this question may as I am just learning javascript, but here it goes.
Say I have an input field:
<input type="text" value=""/>
I am trying to write a javascript function that checks the input value every time a space is entered, and then edit the value live while the user is typing.
For example, say I start typing: hello, how are u
After I hit space following 'u', I would like to replace 'u' with 'you'.
So, if anyone who be so kind, where do I start here? This may be ironic because I see the text I am typing right this instant being updated just to the bottom of this textarea. However, I am trying to get it to update in the field itself.
Thanks!!!
You don't necessarily need jQuery to do this, but it is a little more work with vanilla JS. First, give your input an ID:
<input id="ValueField" type="text" value="" />
Then add an event listener:
var field = document.getElementById('ValueField');
field.addEventListener('keyup',function(e){
if(e.keyCode==32){
// do your stuff
}
});
There are a million ways to detect change, but first thing that comes to mind is a function:
var field = document.getElementById('ValueField');
field.addEventListener('keyup',function(e){
if(e.keyCode==32){
field.value = replaceValues(field.value);
}
});
function replaceValues(fieldval){
var baditems = [' u ',' i '],
newitems = [' you ',' I '],
length = baditems.length;
for(var i = length; i--;){
fieldval = fieldval.replace(baditems[i],newitems[i]);
}
return fieldval;
}
Just build out the array of bad and new items to be the same length with their order matching and you should be good. This way you can replace as many items you want, and should run pretty quickly.
EDIT
Might help if I have the function return something! Here is a jsFiddle to show example. Also, I had to add spaces around the u and i because otherwise it would just match any instance of the letter.
if you are using jquery, look at this: https://stackoverflow.com/a/2249215/146602
good way to detect if a space character is being used.

Categories

Resources