Angular2 - 只允许输入数字的输入框

121

在Angular 2中,我如何将输入字段(文本框)掩盖起来,使其只接受数字而不是字母字符?

我有以下HTML输入:

<input 
  type="text" 
  *ngSwitchDefault 
  class="form-control" 
  (change)="onInputChange()" 
  [(ngModel)]="config.Value" 
  (focus)="handleFocus($event)" 
  (blur)="handleBlur($event)"
/>

上述输入框是一种通用文本输入框,既可以用作简单的文本字段,也可以用作数字字段,例如,显示年份。

使用Angular 2,我如何使用相同的输入控件并在该字段上应用某种过滤器/掩码,以仅接受数字?

我可以通过哪些不同的方式实现这一目标?

注意:我需要仅使用文本框来实现此功能,而不是使用输入数字类型。


2
你能否只使用HTML属性吗? type=number - inoabrian
@inoabrian 我想在不使用数字类型的情况下实现这个。 - Aniruddha Pondhe
这可能会对您有所帮助:https://dev59.com/tVkS5IYBdhLWcg3wemv6 - chandan7
40个回答

134
您可以使用Angular2指令。 Plunkr
import { Directive, ElementRef, HostListener, Input } from '@angular/core';

@Directive({
  selector: '[OnlyNumber]'
})
export class OnlyNumber {

  constructor(private el: ElementRef) { }

  @Input() OnlyNumber: boolean;

  @HostListener('keydown', ['$event']) onKeyDown(event) {
    let e = <KeyboardEvent> event;
    if (this.OnlyNumber) {
      if ([46, 8, 9, 27, 13, 110, 190].indexOf(e.keyCode) !== -1 ||
        // Allow: Ctrl+A
        (e.keyCode === 65 && (e.ctrlKey || e.metaKey)) ||
        // Allow: Ctrl+C
        (e.keyCode === 67 && (e.ctrlKey || e.metaKey)) ||
        // Allow: Ctrl+V
        (e.keyCode === 86 && (e.ctrlKey || e.metaKey)) ||
        // Allow: Ctrl+X
        (e.keyCode === 88 && (e.ctrlKey || e.metaKey)) ||
        // Allow: home, end, left, right
        (e.keyCode >= 35 && e.keyCode <= 39)) {
          // let it happen, don't do anything
          return;
        }
        // Ensure that it is a number and stop the keypress
        if ((e.shiftKey || (e.keyCode < 48 || e.keyCode > 57)) && (e.keyCode < 96 || e.keyCode > 105)) {
            e.preventDefault();
        }
      }
  }
}

你需要将指令名称作为属性写入你的输入中

<input OnlyNumber="true" />

不要忘记在你的模块声明数组中编写指令。
使用正则表达式仍然需要功能键。
export class OnlyNumber {

  regexStr = '^[0-9]*$';
  constructor(private el: ElementRef) { }

  @Input() OnlyNumber: boolean;

  @HostListener('keydown', ['$event']) onKeyDown(event) {
    let e = <KeyboardEvent> event;
    if (this.OnlyNumber) {
        if ([46, 8, 9, 27, 13, 110, 190].indexOf(e.keyCode) !== -1 ||
        // Allow: Ctrl+A
        (e.keyCode == 65 && e.ctrlKey === true) ||
        // Allow: Ctrl+C
        (e.keyCode == 67 && e.ctrlKey === true) ||
        // Allow: Ctrl+V
        (e.keyCode == 86 && e.ctrlKey === true) ||
        // Allow: Ctrl+X
        (e.keyCode == 88 && e.ctrlKey === true) ||
        // Allow: home, end, left, right
        (e.keyCode >= 35 && e.keyCode <= 39)) {
          // let it happen, don't do anything
          return;
        }
      let ch = String.fromCharCode(e.keyCode);
      let regEx =  new RegExp(this.regexStr);    
      if(regEx.test(ch))
        return;
      else
         e.preventDefault();
      }
  }
}

1
太好了。我能用正则表达式模式实现同样的效果吗? - Aniruddha Pondhe
@Shardul 只需将 (e.keyCode == 86 && e.ctrlKey === true) 添加到条件中,复制可以工作,但粘贴无法工作。 - Al-Mothafar
请问您能否解释一下您是如何处理粘贴的,因为它接受所有字符串,包括@Himanshu Modi。 - macha devendher
在其他答案中已经提到,但对于像我这样的盲目复制粘贴者,您应该使用event.keyCode?event.keyCode:event.which来适应Firefox,因为keyCode会传递为0。 - eatinasandwich
2
keyCode已被弃用 https://developer.mozilla.org/zh-CN/docs/Web/API/KeyboardEvent/keyCode - Raja Rama Mohan Thavalam
显示剩余4条评论

93

如果您不想使用指令:

https://stackblitz.com/edit/numeric-only

在component.html中

<input (keypress)="numberOnly($event)" type="text">

在 component.ts 文件中

export class AppComponent {

  numberOnly(event): boolean {
    const charCode = (event.which) ? event.which : event.keyCode;
    if (charCode > 31 && (charCode < 48 || charCode > 57)) {
      return false;
    }
    return true;

  }
}

49
这种方法的问题在于关键事件无法捕获用户粘贴或浏览器自动填充输入字段,因此这是一个不好的解决方案。 - Darryn Hosking

41

我知道这是一个老问题,但由于这是一种常见的功能,我想分享我所做的修改:

  • Custom decimal separator (point or comma)
  • Support for integers only or integer and decimals
  • Support for positive numbers only or positives and negatives
  • Validate minus sign(-) is in the beginning
  • Support to mouse pasting (with some limitation though https://caniuse.com/#feat=clipboard)
  • Support for Mac command key
  • Replace strings like ".33" and "33." for the correct versions: 0.33 and 33.0

    import { Directive, ElementRef, HostListener, Input } from '@angular/core';
    
    @Directive({ selector: '[NumbersOnly]' })
    export class NumbersOnly { 
    
        @Input() allowDecimals: boolean = true;
        @Input() allowSign: boolean = false;
        @Input() decimalSeparator: string = '.';
    
        previousValue: string = '';
    
        // --------------------------------------
        //  Regular expressions
        integerUnsigned: string = '^[0-9]*$';
        integerSigned: string = '^-?[0-9]+$';
        decimalUnsigned: string = '^[0-9]+(.[0-9]+)?$';
        decimalSigned: string = '^-?[0-9]+(.[0-9]+)?$';
    
        /**
         * Class constructor
         * @param hostElement
         */
        constructor(private hostElement: ElementRef) { }
    
        /**
         * Event handler for host's change event
         * @param e
         */
        @HostListener('change', ['$event']) onChange(e) {
    
                this.validateValue(this.hostElement.nativeElement.value);
    }
    
    /**
     * Event handler for host's paste event
     * @param e
     */
    @HostListener('paste', ['$event']) onPaste(e) {
    
        // get and validate data from clipboard
        let value = e.clipboardData.getData('text/plain');
        this.validateValue(value);
        e.preventDefault();
    }
    
    /**
     * Event handler for host's keydown event
     * @param event
     */
    @HostListener('keydown', ['$event']) onKeyDown(e: KeyboardEvent) {
    
        let cursorPosition: number = e.target['selectionStart'];
        let originalValue: string = e.target['value'];
        let key: string = this.getName(e);
        let controlOrCommand = (e.ctrlKey === true || e.metaKey === true);
        let signExists = originalValue.includes('-');
        let separatorExists = originalValue.includes(this.decimalSeparator);
    
        // allowed keys apart from numeric characters
        let allowedKeys = [
            'Backspace', 'ArrowLeft', 'ArrowRight', 'Escape', 'Tab'
        ];
    
        // when decimals are allowed, add
        // decimal separator to allowed codes when
        // its position is not close to the the sign (-. and .-)
        let separatorIsCloseToSign = (signExists && cursorPosition <= 1);
        if (this.allowDecimals && !separatorIsCloseToSign && !separatorExists) {
    
            if (this.decimalSeparator == '.')
                allowedKeys.push('.');
            else
                allowedKeys.push(',');
        }
    
        // when minus sign is allowed, add its
        // key to allowed key only when the
        // cursor is in the first position, and
        // first character is different from
        // decimal separator
        let firstCharacterIsSeparator = (originalValue.charAt(0) != this.decimalSeparator);
        if (this.allowSign && !signExists &&
            firstCharacterIsSeparator && cursorPosition == 0) {
    
            allowedKeys.push('-');
        }
    
        // allow some non-numeric characters
        if (allowedKeys.indexOf(key) != -1 ||
            // Allow: Ctrl+A and Command+A
            (key == 'a' && controlOrCommand) ||
            // Allow: Ctrl+C and Command+C
            (key == 'c' && controlOrCommand) ||
            // Allow: Ctrl+V and Command+V
            (key == 'v' && controlOrCommand) ||
            // Allow: Ctrl+X and Command+X
            (key == 'x' && controlOrCommand)) {
            // let it happen, don't do anything
            return;
        }
    
        // save value before keydown event
        this.previousValue = originalValue;
    
        // allow number characters only
        let isNumber = (new RegExp(this.integerUnsigned)).test(key);
        if (isNumber) return; else e.preventDefault();
    }
    
    /**
     * Test whether value is a valid number or not
     * @param value
     */
    validateValue(value: string): void {
    
        // choose the appropiate regular expression
        let regex: string;
        if (!this.allowDecimals && !this.allowSign) regex = this.integerUnsigned;
        if (!this.allowDecimals && this.allowSign) regex = this.integerSigned;
        if (this.allowDecimals && !this.allowSign) regex = this.decimalUnsigned;
        if (this.allowDecimals &&  this.allowSign) regex = this.decimalSigned;
    
        // when a numbers begins with a decimal separator,
        // fix it adding a zero in the beginning
        let firstCharacter = value.charAt(0);
        if (firstCharacter == this.decimalSeparator)
            value = 0 + value;
    
        // when a numbers ends with a decimal separator,
        // fix it adding a zero in the end
        let lastCharacter = value.charAt(value.length-1);
        if (lastCharacter == this.decimalSeparator)
            value = value + 0;
    
        // test number with regular expression, when
        // number is invalid, replace it with a zero
        let valid: boolean = (new RegExp(regex)).test(value);
        this.hostElement.nativeElement['value'] = valid ? value : 0;
    }
    
    /**
     * Get key's name
     * @param e
     */
    getName(e): string {
    
        if (e.key) {
    
            return e.key;
    
        } else {
    
            // for old browsers
            if (e.keyCode && String.fromCharCode) {
    
                switch (e.keyCode) {
                    case   8: return 'Backspace';
                    case   9: return 'Tab';
                    case  27: return 'Escape';
                    case  37: return 'ArrowLeft';
                    case  39: return 'ArrowRight';
                    case 188: return ',';
                    case 190: return '.';
                    case 109: return '-'; // minus in numbpad
                    case 173: return '-'; // minus in alphabet keyboard in firefox
                    case 189: return '-'; // minus in alphabet keyboard in chrome
                    default: return String.fromCharCode(e.keyCode);
                }
            }
        }
    }
    

使用方法:

 <input NumbersOnly
        [allowDecimals]="true"
        [allowSign]="true"
        type="text">

我修改了validatevalue方法的最后一行,以防止在无效粘贴时添加零。如果(valid){this.hostElement.nativeElement ['value'] = value;} - Abdul Rehman Sayed
1
请问您能否添加拖放验证功能呢? 另外,我注意到输入字段的值会在前导和尾随小数分隔符上进行0填充,但该值不会更新到双向绑定变量中。例如:[(NgModel)]="myVariable",在这里,如果我们在输入字段中键入.3,则文本输入框中的值在模糊时会更改为0.3,但myVariable中的值仍然是'.3'。 - Sushmit Sagar
删除和输入缺失,但无论如何解决方案非常好。 - Oleg Bondarenko
为了确保双向数据绑定正常工作,请添加以下内容:@Output() ngModelChange:EventEmitter<any> = new EventEmitter(); 到声明中,然后在 validateValue 方法的最后一行添加:this.ngModelChange.emit(valid ? value : 0); - Gabriel H
还要将这些行添加到validateValue方法中://删除不必要的前导零 let secondChar=value.charAt(1); if (firstCharacter=='0'&&(secondChar!=''||secondChar!=this.decimalSeparator)) { value=value.substring(1); } - Gabriel H

36
我想在@omeralper提供的答案基础上进一步完善,我认为他提供了一个坚实解决方案的良好基础。 我提出的是最新Web标准的简化版本,需要注意的是event.keycode已从Web标准中删除,并且未来的浏览器更新可能不再支持它。请参见https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode

此外,该方法

String.fromCharCode(e.keyCode);

不能保证用户按下的键所对应的keyCode能够映射到预期的字母,因为不同的键盘配置会导致特定的keyCode对应不同字符。使用这种方法会引入难以识别的bug,并且可能会轻易地破坏某些用户的功能。相反,我建议使用event.key,请参见文档https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key

此外,我们只希望输出结果是有效的十进制数。这意味着数字1、11.2和5000.2341234应该被接受,但值1.1.2不应该被接受。

需要注意的是,在我的解决方案中,我排除了剪切、复制和粘贴功能,因为这会引起一些bug,特别是当人们在相关字段中粘贴不需要的文本时。这将需要一个keyup处理程序上的清理过程;这不在本主题的范围内。

以下是我提出的解决方案。

import { Directive, ElementRef, HostListener } from '@angular/core';

@Directive({
    selector: '[myNumberOnly]'
})
export class NumberOnlyDirective {
    // Allow decimal numbers. The \. is only allowed once to occur
    private regex: RegExp = new RegExp(/^[0-9]+(\.[0-9]*){0,1}$/g);

    // Allow key codes for special events. Reflect :
    // Backspace, tab, end, home
    private specialKeys: Array<string> = [ 'Backspace', 'Tab', 'End', 'Home' ];

    constructor(private el: ElementRef) {
    }

    @HostListener('keydown', [ '$event' ])
    onKeyDown(event: KeyboardEvent) {
        // Allow Backspace, tab, end, and home keys
        if (this.specialKeys.indexOf(event.key) !== -1) {
            return;
        }

        // Do not use event.keycode this is deprecated.
        // See: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode
        let current: string = this.el.nativeElement.value;
        // We need this because the current value on the DOM element
        // is not yet updated with the value from this event
        let next: string = current.concat(event.key);
        if (next && !String(next).match(this.regex)) {
            event.preventDefault();
        }
    }
}

这是一个非常有趣的方法。你有没有关于如何实现复制/粘贴功能而不诉诸于旧的方法,例如(e.keyCode == 67 && e.ctrlKey === true)的建议? - Ender2050
1
我个人没有尝试过这个,不过你可以类似地监听触发的复制/粘贴事件。它们会生成一个包含被复制/粘贴数据的 ClipboardEvent(https://developer.mozilla.org/en-US/docs/Web/API/ClipboardEvent)。唯一的缺点是这仍然是实验性的,并且只受最新浏览器的支持- https://caniuse.com/#search=paste。 - JeanPaul A.
我尝试了类似的方法,但不幸的是这种方法并不适用于每一种情况。你的“next”变量假设按下的字符将放在当前输入值的末尾,但这并不总是正确的。例如,如果有人输入100,然后决定在前面添加一个1使其变成1100。你的“next”变量将是不正确的(1001)。 - Carlos Rodriguez
由于“next”值仅用于检查输入金额是否为有效十进制数(而不是设置该值),因此在末尾附加它不会改变正则表达式验证。 - JeanPaul A.
太棒了,解决方案很好,代码易懂且易于修改。我将箭头事件添加到specialKeys中,现在它像魔法一样运行。 - KeaganFouche
显示剩余3条评论

28

一种更简洁的解决方案。尝试使用这个指令。

如果您正在使用ReactiveForms,也可以使用此指令。

export class NumberOnlyDirective {
  private el: NgControl;

  constructor(private ngControl: NgControl) {
    this.el = ngControl;
  }

  // Listen for the input event to also handle copy and paste.
  @HostListener('input', ['$event.target.value'])
  onInput(value: string) {
    // Use NgControl patchValue to prevent the issue on validation
    this.el.control.patchValue(value.replace(/[^0-9]/g, ''));
  }
}

你可以像这样在你的输入中使用它:

<input matInput formControlName="aNumberField" numberOnly>

6
虽然这个解决方案有效,但它会触发两次模型更改事件。话虽如此,使用正则表达式的方法是正确的,这里有一个不会触发两次模型更改事件的版本:https://stackblitz.com/edit/angular-numbers-only-directive?file=app%2Fnumbers-only.directive.ts - ntziolis
对于ntziolis的评论:到目前为止,Ben Gulapa的解决方案对我有效。但是ntziolis提到的解决方案不行。如果我错了,请原谅,但是对于上面的stackblitz链接中的代码,至少对我来说,问题似乎在于我键入的最后一个不需要的字符,尽管它从未显示在用户界面中,但某种方式被放入了我的组件的绑定变量中。只有最后一个不需要的字符。 - user2367418
继续我的评论:使用Angular 7和一个限制为两个字符的HTML输入文本。 - user2367418
我可以确认这个解决方案存在双重发射问题。在此处查看重现: https://stackblitz.com/edit/angular-ivy-icq8we?file=src/app/app.module.ts - spierala
@ntziolis,经过测试你的解决方案并不起作用,因为绑定变量保留了字母字符。在双重发射得到妥善解决之前,我将继续使用Ben的解决方案。 - Sebastián Echeverry

21
你可以使用正则表达式:
<input type="text" (keypress)="numericOnly($event)">

numericOnly(event): boolean {    
    let patt = /^([0-9])$/;
    let result = patt.test(event.key);
    return result;
}

1
是的,这很有帮助,但我想在我的输入框中也要有(.)小数。 - Rinku Choudhary

19
<input type="text" (keypress)="keyPress($event)">


  keyPress(event: any) {
    const pattern = /[0-9\+\-\ ]/;

    let inputChar = String.fromCharCode(event.charCode);
    if (event.keyCode != 8 && !pattern.test(inputChar)) {
      event.preventDefault();
    }
  }

19

你需要使用type="number"而不是text。你还可以指定最大和最小数字。

<input type="number" name="quantity" min="1" max="5">

2
我想在不使用数字类型的情况下实现这个。 - Aniruddha Pondhe
3
支持数字类型的功能仍然存在很多漏洞,正如此答案所描述的:https://dev59.com/2W7Xa4cB1Zd3GeqPs7n-#14995890 - Nicolas Forney
13
"number"类型的输入框的缺陷在于它将字符"e"作为科学计数法的一部分接受。 - user776686
1
type="number" 的缺点是它会破坏键盘,让用户猜测为什么键盘停止工作。过滤非常不友好,更好的方法是显示清晰的错误信息并保持输入不变。当用户输入 a1b2c3 时,给他结果 "123" 毫无意义。 - Cesar

15

你可以像这样实现它

<input type="text" pInputText (keypress)="onlyNumberKey($event)" maxlength="3"> 

onlyNumberKey(event) {
    return (event.charCode == 8 || event.charCode == 0) ? null : event.charCode >= 48 && event.charCode <= 57;
}

//for Decimal you can use this as

onlyDecimalNumberKey(event) {
    let charCode = (event.which) ? event.which : event.keyCode;
    if (charCode != 46 && charCode > 31
        && (charCode < 48 || charCode > 57))
        return false;
    return true;
}

希望这能对你有所帮助。


你能详细说明一下吗?event.charCode==8是做什么的? - bosari
提醒一下,这不适用于数字键盘键。 - Brett.J

12

我知道这个问题已经有很多答案了,但是我需要解决以下问题(似乎没有一个答案能够完全支持):

  • 支持带有多行选项的文本框
  • 支持小数或负数
  • 每行的最大长度
  • 跨浏览器支持(Chrome、Edge、IE 11)
  • 处理剪切/粘贴操作和事件

解决方案使我可以像这样定义一个文本框:

<textarea class="form-control" [(ngModel)]="this.myModelVariable"
    appOnlyNumbers [allowNegative]="true" [allowMultiLine]="true" 
    [allowDecimal]="true" [maxLength]="10"
    placeholder="Enter values (one per line)"></textarea>

或者如果我只想要正整数

<textarea class="form-control" [(ngModel)]="this.myModelVariable"
    appOnlyNumbers [allowMultiLine]="true" [maxLength]="9"
    placeholder="Enter values (one per line)"></textarea>

这是我的指令:

import { Directive, HostListener, Input, ElementRef } from '@angular/core';

@Directive({
  selector: '[appOnlyNumbers]'
})
export class OnlyNumbersDirective {
  constructor(private el: ElementRef) { }

  @Input() allowMultiLine: boolean = false;
  @Input() allowNegative: boolean = false;
  @Input() allowDecimal: boolean = false;
  @Input() maxLength: number = 0;
  regex: RegExp;

  @HostListener('keypress', ['$event'])
  onKeyPress(event: KeyboardEvent) {
    this.validate(event, event.key === 'Enter' ? '\n' : event.key);
  }

  @HostListener('paste', ['$event'])
  onPaste(event: Event) {
    const pastedText = (<any>window).clipboardData && (<any>window).clipboardData.getData('Text') // If IE, use window
      || <ClipboardEvent>event && (<ClipboardEvent>event).clipboardData.getData('text/plain'); // Non-IE browsers
    this.validate(event, pastedText);
  }

  @HostListener('cut', ['$event'])
  onCut(event: Event) {
    this.validate(event, '');
  }

  validate(event: Event, text: string) {
    const txtInput = this.el.nativeElement;
    const newValue = (txtInput.value.substring(0, txtInput.selectionStart)
      + text + txtInput.value.substring(txtInput.selectionEnd));
    if (!this.regex) {
      this.regex = <RegExp>eval('/^'
        + (this.allowNegative ? '-?' : '')
        + (this.allowDecimal ? '((\\d+\\.?)|(\\.?))\\d*' : '\\d*')
        + '$/g');
    }
    var lines = this.allowMultiLine ? newValue.split('\n') : [newValue];
    for (let line of lines) {
      let lineText = line.replace('\r', '');
      if (this.maxLength && lineText.length > this.maxLength || !lineText.match(this.regex)) {
        event.preventDefault();
        return;
      }
    }
  }

}

很好,谢谢伙计 ^_^ - Leebeedev
我尝试根据我的需求修改这个答案(不需要多行),但发现它对于 input type="number" 失败了,因为它没有可用的 selectionStart/End。事实上,在数字输入框内部获取插入符位置似乎没有办法 :( - AsGoodAsItGets

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接