Input type number "only numeric value" validation - forms

How can I validate an input of type="number" to only be valid if the value is numeric or null using only Reactive Forms (no directives)?
Only numbers [0-9] and . are allowed, no "e" or any other characters.
What I've tried so far:
Template:
<form [formGroup]="form" novalidate>
<input type="number" formControlName="number" id="number">
</form>
Component:
export class App {
form: FormGroup = new FormGroup({});
constructor(
private fb: FormBuilder,
) {
this.form = fb.group({
number: ['', [CustomValidator.numeric]]
})
}
}
CustomValidator:
export class CustomValidator{
// Number only validation
static numeric(control: AbstractControl) {
let val = control.value;
if (val === null || val === '') return null;
if (!val.toString().match(/^[0-9]+(\.?[0-9]+)?$/)) return { 'invalidNumber': true };
return null;
}
}
Plunker
The problem is when a user enters something that is not a number ("123e" or "abc") the FormControl's value becomes null, keep in mind I don't want the field to be required so if the field really is empty null value should be valid.
Cross browser support is also important (Chrome's number input fields do not allow the user to input letters - except "e", but FireFox and Safari do).

In the HTML file, you can add ngIf for your pattern like this,
<div class="form-control-feedback" *ngIf="Mobile.errors && (Mobile.dirty || Mobile.touched)">
<p *ngIf="Mobile.errors.pattern" class="text-danger">Number Only</p>
</div>
In .ts file you can add the Validators pattern -"^[0-9]*$"
this.Mobile = new FormControl('', [
Validators.required,
Validators.pattern("^[0-9]*$"),
Validators.minLength(8),
]);

Using directive it becomes easy and can be used throughout the application
HTML
<input type="text" placeholder="Enter value" numbersOnly>
As .keyCode() and .which() are deprecated, codes are checked using .key()
Referred from
Directive:
#Directive({
selector: "[numbersOnly]"
})
export class NumbersOnlyDirective {
#Input() numbersOnly:boolean;
navigationKeys: Array<string> = ['Backspace']; //Add keys as per requirement
constructor(private _el: ElementRef) { }
#HostListener('keydown', ['$event']) onKeyDown(e: KeyboardEvent) {
if (
// Allow: Delete, Backspace, Tab, Escape, Enter, etc
this.navigationKeys.indexOf(e.key) > -1 ||
(e.key === 'a' && e.ctrlKey === true) || // Allow: Ctrl+A
(e.key === 'c' && e.ctrlKey === true) || // Allow: Ctrl+C
(e.key === 'v' && e.ctrlKey === true) || // Allow: Ctrl+V
(e.key === 'x' && e.ctrlKey === true) || // Allow: Ctrl+X
(e.key === 'a' && e.metaKey === true) || // Cmd+A (Mac)
(e.key === 'c' && e.metaKey === true) || // Cmd+C (Mac)
(e.key === 'v' && e.metaKey === true) || // Cmd+V (Mac)
(e.key === 'x' && e.metaKey === true) // Cmd+X (Mac)
) {
return; // let it happen, don't do anything
}
// Ensure that it is a number and stop the keypress
if (e.key === ' ' || isNaN(Number(e.key))) {
e.preventDefault();
}
}
}

Simplest and most effective way to do number validation is (it will restrict space and special character also)
if you dont want length restriction you can remove maxlength property
HTML
<input type="text" maxlength="3" (keypress)="validateNo($event)"/>
TS
validateNo(e): boolean {
const charCode = e.which ? e.which : e.keyCode;
if (charCode > 31 && (charCode < 48 || charCode > 57)) {
return false
}
return true
}

I had a similar problem, too: I wanted numbers and null on an input field that is not required. Worked through a number of different variations. I finally settled on this one, which seems to do the trick. You place a Directive, ntvFormValidity, on any form control that has native invalidity and that doesn't swizzle that invalid state into ng-invalid.
Sample use:
<input type="number" formControlName="num" placeholder="0" ntvFormValidity>
Directive definition:
import { Directive, Host, Self, ElementRef, AfterViewInit } from '#angular/core';
import { FormControlName, FormControl, Validators } from '#angular/forms';
#Directive({
selector: '[ntvFormValidity]'
})
export class NtvFormControlValidityDirective implements AfterViewInit {
constructor(#Host() private cn: FormControlName, #Host() private el: ElementRef) { }
/*
- Angular doesn't fire "change" events for invalid <input type="number">
- We have to check the DOM object for browser native invalid state
- Add custom validator that checks native invalidity
*/
ngAfterViewInit() {
var control: FormControl = this.cn.control;
// Bridge native invalid to ng-invalid via Validators
const ntvValidator = () => !this.el.nativeElement.validity.valid ? { error: "invalid" } : null;
const v_fn = control.validator;
control.setValidators(v_fn ? Validators.compose([v_fn, ntvValidator]) : ntvValidator);
setTimeout(()=>control.updateValueAndValidity(), 0);
}
}
The challenge was to get the ElementRef from the FormControl so that I could examine it. I know there's #ViewChild, but I didn't want to have to annotate each numeric input field with an ID and pass it to something else. So, I built a Directive which can ask for the ElementRef.
On Safari, for the HTML example above, Angular marks the form control invalid on inputs like "abc".
I think if I were to do this over, I'd probably build my own CVA for numeric input fields as that would provide even more control and make for a simple html.
Something like this:
<my-input-number formControlName="num" placeholder="0">
PS: If there's a better way to grab the FormControl for the directive, I'm guessing with Dependency Injection and providers on the declaration, please let me know so I can update my Directive (and this answer).

IMO the most robust and general way to do this is by checking if the value may be converted to number. For that add a validator:
numberValidator(control: FormControl) {
if (isNaN(control?.value)) {
return {
number: true
}
}
return null;
}
export class App {
form: FormGroup = new FormGroup({});
constructor(
private fb: FormBuilder,
) {
this.form = fb.group({
number: ['', [numberValidator]]
})
}
}
You can combine it with Validators.min and/or Validators.max to further limiting the accepted values.

The easiest way would be to use a library like this one and specifically you want noStrings to be true
export class CustomValidator{ // Number only validation
static numeric(control: AbstractControl) {
let val = control.value;
const hasError = validate({val: val}, {val: {numericality: {noStrings: true}}});
if (hasError) return null;
return val;
}
}

Try to put a minimum input and allow only numbers from 0 to 9. This worked for me in Angular Cli
<input type="number" oninput="this.value=this.value.replace(/[^\d]/,'')" min=0>

You need to use regular expressions in your custom validator. For example, here's the code that allows only 9 digits in the input fields:
function ssnValidator(control: FormControl): {[key: string]: any} {
const value: string = control.value || '';
const valid = value.match(/^\d{9}$/);
return valid ? null : {ssn: true};
}
Take a look at a sample app here:
https://github.com/Farata/angular2typescript/tree/master/Angular4/form-samples/src/app/reactive-validator

Sometimes it is just easier to try something simple like this.
validateNumber(control: FormControl): { [s: string]: boolean } {
//revised to reflect null as an acceptable value
if (control.value === null) return null;
// check to see if the control value is no a number
if (isNaN(control.value)) {
return { 'NaN': true };
}
return null;
}
Hope this helps.
updated as per comment,
You need to to call the validator like this
number: new FormControl('',[this.validateNumber.bind(this)])
The bind(this) is necessary if you are putting the validator in the component which is how I do it.

Related

Type guarding React.KeyboardEvent to reuse event handlers

I created a search-bar-React-Component that resembles the one by Google.
It should fire off a search based on the input if I either click on the 'search' icon or if I hit the enter key.
I want to reuse the same function for both the click and the keydown handler:
...
var [searchParam, setSearchParam] = useState('');
function initSearch(
e:
| React.MouseEvent<HTMLButtonElement>
| React.KeyboardEvent<HTMLInputElement>
): void {
if (e.type == 'click' || (e.type == 'keydown' && e.key == 'Enter')) {
console.log(searchParam); /* ⬆️ this throws the error */
}
}
...
TypeScript keeps giving me the following error:
'Property 'key' does not exist on type 'MouseEvent<HTMLButtonElement, MouseEvent>'
I tried both of the following:
(e instance of KeyboardEvent && e.key == 'Enter') // This is always false, since e is a React.KeyboardEvent
(e instance of React.KeyboardEvent) // KeyboardEvent is not a property of React.
What is a good way to typeguard? Is there a better way to write the function?
Thank you.
Turns out using an intersection type solved the problem:
function initSearch(
e:
| (React.MouseEvent<HTMLButtonElement> & { type: 'click' }) /*⬅️*/
| (React.KeyboardEvent<HTMLInputElement> & { type: 'keydown' }) /*⬅️*/
): void {
if (e.type == 'click' || (e.type == 'keydown' && e.key == 'Enter')) {
console.log(searchParam);
}
}
I checked the type definitions, turns out the 'type' property is only defined as 'string', not as a definite primitive value.
In case I'm missing something here (i.e. that a keydown event can somehow not include the e.type == 'keydown' property), please let me know.
It feels unnecessarily hacky!

How do I query a Meteor.publication collection by a component prop correctly?

Given the following React component, I am passing this component this.ticket.props AND I am subscribing to a collection called userReservations. The ticket properties are rendering correctly i.e. each ticket is rendering only it's specific properties.
However inside the .renderReservationInfo() method. After I make the Meteor.subscription call with the ticket._id value as a selector in the createContainer class, which corresponds to the "ticketId" value in the UserReservation collection, the Meteor publication responds with the correct collection info but renders it to every ticket.
If I'm querying by ticket._id, why is the collection applied to both tickets? How can I render just the reservation info to the corresponding ticket._id?
Are there react lifecycle components that can help me manage this?
class ComponentName extends React.Component {
renderReservationInfo() {
let {
ticket,
UserReservation
} = this.props;
return UserReservation.map((reservation) => {
return (
<div key = {reservation._id} >
< h4 > {reservation.property1} < /h4>
< h4 > {reservation.property2} < /h4>
< h4 > {reservation.property3} < /h4>
<Component2 prop = {reservation} />
</div >
)
});
}
render() {
return (
<div >
< div className = "make-it-pretty" >
< h4 > Ticket Price: $ { this.props.ticket.property1 } </h4>
< h4 > Ticket Price: $ { this.props.ticket.property2 } </h4>
< h4 > Ticket Price: $ { this.props.ticket.property3 } </h4>
{ this.renderReservationInfo() }
</div>
</div >
)
}
}
export default createContainer(({ ticket }) => {
Meteor.subscribe('userReservations', ticket._id);
return {
UserReservation: UserReservations.find({}).fetch()
};
}, ComponentName);
And my Publication info
//Server
Meteor.publish('userReservations', function(ticket_Id) {
if (!ticket_Id) {
console.log('No ticketId sent');
} else {
console.log('sending UserReservations', ticket_Id);
return UserReservations.find({ ticketId:ticket_Id });
}
});
Okay so this turned out to be a simple enough solution. I had previously thought I needed to keep my subscribe call empty in the client side for security purposes, and maybe that's an accurate statement, but inserting the the specific ticketId I was able to start returning what I needed to in the correct way.
Also by making stricter query parameters in my publication.find() I was able to limit what was sent back. I think this is probably an example of a simple problem that I snowballed into a bigger one.

How to prevent individual form elements from updating using Meteor + React?

I have a Meteor + React single-page-application with a basic form in it. Data is collected from MongoDB using the createContainer method and passed to a form component. The problem I am facing is this. A user starts completing the form but, if the data that originally populated the form changes (by another user somewhere else in the world saving the form), the createContainer method will re-compute, which in turn pushes a new set of props to the form component and therefore overwrites what the user is typing in.
For many reasons, I cannot use the shouldComponentUpdate lifecycle method within the form component. One reason is that the form contains a select element, whose list of items should still accept reactive updates.
I need a way of halting certain reactive updates, but allowing others, whilst the user is completing the form. Any suggestions?
export default FormContainer = createContainer(( params ) => {
const dataFormHandle = Meteor.subscribe('FormsPub');
const dataFormIsReady = dataFormHandle.ready();
const dataListHandle = Meteor.subscribe('ListItemsPub');
const dataListIsReady = dataListHandle.ready();
let name = "";
let listItems = [];
let listSelectedValue = null;
if(dataListIsReady) {
listItems = collections.ListItemsColl.find({_id: ListId}).fetch();
}
if(dataFormIsReady) {
let formData = collections.FormsColl.find({_id: formId}).fetch();
name = formData[0].name;
listSelectedValue = formData[0].listSelectedValue;
}
return {
name,
listItems,
listSelectedValue
};
}, Form);
...
export default class Form extends Component {
constructor(props) {
super(props);
this.state = {
name: (this.props.name) ? this.props.name : "",
listSelectedValue: (this.props.listSelectedValue) ? this.props.listSelectedValue : null
};
}
componentWillReceiveProps(nextProps) {
this.setState({name: (nextProps.name) ? nextProps.name : ""});
this.setState({listSelectedValue: (nextProps.listSelectedValue) ? nextProps.listSelectedValue : null});
}
updateFormState(){
var name = e.target.name;
var val = e.target.value;
if(name == "name"){this.setState({name: val});}
if(name == "list"){
if( typeof e.target[e.target.selectedIndex] != "undefined" ) {
this.setState({listSelectedValue: val});
}
}
}
render(){
return (
<div>
<input type="text" name="name" value={this.state.name} onChange={this.updateFormState.bind(this)} />
<Select2
value={this.state.listSelectedValue}
name="list"
onChange={this.updateFormState.bind(this)}
options={{
minimumResultsForSearch: Infinity
}}
data={this.props.listItems}
/>
</div>
);
}
}
For the data in the form that you wish to be non-reactive simply specify reactive: false in your .find(), for example:
let formData = collections.FormsColl.find({ _id: formId },{ reactive: false }).fetch();
This will prevent the data from reactively updating while the form is open.

kendo-ui grid foreign key column and mvvm

I am struggling with kendo-ui grid foreign key column and mvvm
I would like to be able to combine the "Foreign Key Column" example with the "MVVM" example
My question is: "how do I data-bind the values property of a look-up field?"
So this is kind of an older post, but I just had to work around the same issue and found this while trying to solve. Figured I'd answer the question for posterity.
The "values" property doesn't seem to work 100% in the kendo grid in MVVM. I have worked around this in a two step process.
Tack "this.viewModel" (where "viewModel" is whatever you are calling your VM) in front of the "loggerSeverityValues". This will give you a dropdownlist when editing the field.
Utilize the template functionality to display the correct value in the grid. I use a little function to make this easier:
getText: function (matchValue, valuesArray, text, value)
{
if (text === undefined)
{
text = 'text';
}
if (value === undefined)
{
value = 'value';
}
var retText = "No Value Found";
finalArr = $.grep(valuesArray, function (val, integer)
{
return val[value] == matchValue;
});
if (finalArr.length > 0)
{
retText = finalArr[0].text;
}
return retText;
}
The final look of the field will be something along the lines of this:
{ field: 'severity', width: 270, values: this.viewModel.loggerSeverityValues, template: '#: getText(severity, this.viewModel.loggerSeverityValues) #' }
Note that with the getText() function you can override the text and value parameters if you need to.
Anyway this worked for me. Kind of a workaround, but as of release 2014.3.1411 anyway it doesn't appear that the kendo MVVM bindings will work properly with foreign keys.
EDIT:
For anyone now using the kendo ng2+ components, same pattern but with a pipe transform works.
Pipe:
import { Pipe, PipeTransform } from '#angular/core';
#Pipe({ name: 'getText' })
export class GetTextPipe implements PipeTransform {
transform(value: any, idPropertyName: string, valuePropertyName: string, valueArray: any[]): any {
if (value != null && valueArray != undefined) {
let retIndex = valueArray.map(v => v[idPropertyName]).indexOf(value);
return valueArray[retIndex][valuePropertyName];
}
else {
return '';
}
}
}
Usage:
<kendo-grid-column field="severity" title="Severity" width="150px">
<template kendoGridCellTemplate let-dataItem="dataItem">
{{dataItem.severity | getText:'severity':'severityName':loggerSeverityValues}}
</template>
</kendo-grid-column>

recall form value not working for textareas

Im trying to modify this script
http://www.dynamicdrive.com/dynamicindex16/formremember2.htm
to work for textareas, and not just input text boxes. Heres what im guessing are the relevant parts of the script, i just cant figure it out myself
rememberForm.prototype.savevalues=function(){ //get form values and store in cookie
for (var i=0; i<this.fields.length; i++){
if (this.fields[i].type=="text")
this.cookiestr+=this.fields[i].fname+":"+escape(this.fields[i].value)+"#"
}
if (typeof this.togglebox!="undefined"){ //if "remember values checkbox" is defined
this.persistdays=(this.togglebox.checked)? this.persistdays : -1 //decide whether to save form values
this.cookiestr=(this.togglebox.checked)? this.cookiestr+"toggleboxid:on;" : this.cookiestr
}
else //if checkbox isn't defined, just remove final "#" from cookie string
this.cookiestr=this.cookiestr.substr(0, this.cookiestr.length-1)+";"
setCookie(this.cookiename, this.cookiestr, this.persistdays)
}
rememberForm.prototype.recallvalues=function(){ //populate form with saved values
var cookievalue=getCookie(this.cookiename)
if (cookievalue!=""){ //parse cookie, where cookie looks like: field1:value1#field2:value2...
var cookievaluepair=cookievalue.split("#")
for (var i=0; i<cookievaluepair.length; i++){
if (cookievaluepair[i].split(":")[0]!="toggleboxid" && this.getfield(cookievaluepair[i].split(":")[0]).type=="text")
this.getfield(cookievaluepair[i].split(":") [0]).value=unescape(cookievaluepair[i].split(":")[1])
else //else if name in name/value pair is "toggleboxid"
this.togglebox.checked=true
}
}
The method persistfields(id, ...) sets the fields you want to persist in the cookie. The fields are looked up by id so I guess adding a textarea with an id attribute would suffice.
For example:
<form id="myFormId">
<input type="text" id="someInputId" />
<textarea id="textareaId"></textarea>
</form>
<script>
var f = new rememberForm('myFormId');
f.persistfields('someInputId', 'textareaId');
</script>
This will add the input and textarea to the rememberForm instance fields property.
UPDATE
The problem lies in this method of rememberForm. I formatted the code for readability since the original source has horrible formatting.
rememberForm.prototype.savevalues = function() {
for (var i = 0; i < this.fields.length; i++) {
// PROBLEM: only allows type="text"
if (this.fields[i].type == "text") {
this.cookiestr += this.fields[i].fname + " : " + escape(this.fields[i].value) + "#"
}
if (typeof this.togglebox != "undefined") {
this.persistdays = (this.togglebox.checked) ? this.persistdays : -1;
this.cookiestr = (this.togglebox.checked) ? this.cookiestr + "toggleboxid:on;" : this.cookiestr
} else {
this.cookiestr = this.cookiestr.substr(0, this.cookiestr.length - 1) + ";"
setCookie(this.cookiename, this.cookiestr, this.persistdays)
}
}
}
As mentioned in my comment it will test the type of the input element to be 'text'. If you'd like to add textareas in the cookie you could change that line to:
if (this.fields[i].type == "text" || this.fields[i].type == 'textarea') {
That should work.