角度输入限制指令 - 否定正则表达式

问题描述 投票:3回答:3

编辑:请使用此简单指令随意添加对其他人有用的其他验证。

--

我正在尝试创建一个Angular Directive,将字符输入限制在文本框中。我已经成功地使用了几个常见的用例(alphbetical,alphanumeric和numeric),但是使用流行的方法验证电子邮件地址,日期和货币我无法使指令工作,因为我需要它否定正则表达式。至少这是我认为它需要做的事情。

任何货币协助(可选千分隔符和美分),日期(mm / dd / yyyy)和电子邮件非常感谢。我根本没有正则表达式。

这就是我目前所拥有的:http://jsfiddle.net/corydorning/bs05ys69/

HTML

<div ng-app="example">
<h1>Validate Directive</h1>

<p>The Validate directive allow us to restrict the characters an input can accept.</p>

<h3><code>alphabetical</code> <span style="color: green">(works)</span></h3>
<p>Restricts input to alphabetical (A-Z, a-z) characters only.</p>
<label><input type="text" validate="alphabetical" ng-model="validate.alphabetical"/></label>

<h3><code>alphanumeric</code> <span style="color: green">(works)</span></h3>
<p>Restricts input to alphanumeric (A-Z, a-z, 0-9) characters only.</p>
<label><input type="text" validate="alphanumeric" ng-model="validate.alphanumeric" /></label>

<h3><code>currency</code> <span style="color: red">(doesn't work)</span></h3>
<p>Restricts input to US currency characters with comma for thousand separator (optional) and cents (optional).</p>
<label><input type="text" validate="currency.us" ng-model="validate.currency" /></label>

<h3><code>date</code> <span style="color: red">(doesn't work)</span></h3>
<p>Restricts input to the mm/dd/yyyy date format only.</p>
<label><input type="text" validate="date" ng-model="validate.date" /></label>

<h3><code>email</code> <span style="color: red">(doesn't work)</span></h3>
<p>Restricts input to email format only.</p>
<label><input type="text" validate="email" ng-model="validate.email" /></label>

<h3><code>numeric</code> <span style="color: green">(works)</span></h3>
<p>Restricts input to numeric (0-9) characters only.</p>
<label><input type="text" validate="numeric" ng-model="validate.numeric" /></label>

JavaScript的

angular.module('example', [])
  .directive('validate', function () {
    var validations = {
      // works
      alphabetical: /[^a-zA-Z]*$/,

      // works
      alphanumeric: /[^a-zA-Z0-9]*$/,

      // doesn't work - need to negate?
      // taken from: http://stackoverflow.com/questions/354044/what-is-the-best-u-s-currency-regex
      currency: /^[+-]?[0-9]{1,3}(?:,?[0-9]{3})*(?:\.[0-9]{2})?$/,

      // doesn't work - need to negate?
      // taken from here: http://stackoverflow.com/questions/15196451/regular-expression-to-validate-datetime-format-mm-dd-yyyy
      date: /(?:0[1-9]|1[0-2])\/(?:0[1-9]|[12][0-9]|3[01])\/(?:19|20)[0-9]{2}/,

      // doesn't work - need to negate?
      // taken from: http://stackoverflow.com/questions/46155/validate-email-address-in-javascript
      email: /^([\w-]+(?:\.[\w-]+)*)@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$/i,

      // works
      numeric: /[^0-9]*$/
    };

  return {
    require: 'ngModel',

    scope: {
      validate: '@'
    },

    link: function (scope, element, attrs, modelCtrl) {
      var pattern = validations[scope.validate] || scope.validate
      ;

      modelCtrl.$parsers.push(function (inputValue) {
        var transformedInput = inputValue.replace(pattern, '')
        ;

        if (transformedInput != inputValue) {
          modelCtrl.$setViewValue(transformedInput);
          modelCtrl.$render();
        }

        return transformedInput;
      });
    }
  };
});
regex angularjs angularjs-directive angular-ngmodel
3个回答
2
投票

我很确定,有更好的方法,可能正则表达式也不是最好的工具,但这里是我的命题。

这样,您只能限制允许输入的字符并强制用户使用正确的格式,但您需要在用户完成输入后验证最终输入,但这是另一个故事。

字母,数字和字母数字非常简单,用于输入和验证输入,因为很清楚您可以键入什么,以及什么是正确的最终输入。但是对于日期,邮件,货币,您无法使用正则表达式验证输入以获得完整的有效输入,因为用户需要先输入它,同时输入需要在最终有效输入方面无效。因此,这是一件事,例如限制用户只键入数字和/的日期格式,如:12/12/1988,但最后你需要检查他是否输入正确的日期或只是12/12/126例如。当用户提交答案或文本字段失去焦点等时,需要检查这一点。

要仅验证键入的字符,您可以尝试使用: JSFiddle DEMO 第一次改变:

var transformedInput = inputValue.replace(pattern, '')

var transformedInput = inputValue.replace(pattern, '$1')

然后使用正则表达式:

  • /^([a-zA-Z]*(?=[^a-zA-Z]))./ - 字母
  • /^([a-zA-Z0-9]*(?=[^a-zA-Z0-9]))./ - 字母数字
  • /(\.((?=[^\d])|\d{2}(?![^,\d.]))|,((?=[^\d])|\d{3}(?=[^,.$])|(?=\d{1,2}[^\d]))|\$(?=.)|\d{4,}(?=,)).|[^\d,.$]|^\$/- currency(允许字符串如:343243.34,1,123,345.34,.05有或没有$)
  • ^(((0[1-9]|1[012])|(\d{2}\/\d{2}))(?=[^\/])|((\d)|(\d{2}\/\d{2}\/\d{1,3})|(.+\/))(?=[^\d])|\d{2}\/\d{2}\/\d{4}(?=.)).|^(1[3-9]|[2-9]\d)|((?!^)(3[2-9]|[4-9]\d)\/)|[3-9]\d{3}|2[1-9]\d{2}|(?!^)\/\d\/|^\/|[^\d/] - 日期(00-12 / 00-31 / 0000-2099)
  • /^(\d*(?=[^\d]))./ - 数字
  • /^([\w.$-]+\@[\w.]+(?=[^\w.])|[\w.$-]+\@(?=[^\w.-])|[\w.@-]+(?=[^\w.$@-])).$|\.(?=[^\w-@]).|[^\w.$@-]|^[^\w]|\.(?=@).|@(?=\.)./i - 电子邮件

通常,它使用此模式:

([valid characters or structure] captured in group $1)(?= positive lookahead for not allowed characters) any character

实际上,它将捕获组$1中的所有有效字符,如果用户键入无效字符,则整个字符串将替换为已从组$1捕获的有效字符。它由一部分补充,该部分应排除一些明显的无效字符,如邮件中的@@或货币的34...2

通过理解这些正则表达式如何工作,尽管它看起来很复杂,我认为通过添加额外的允许/不允许的字符来扩展它很容易。

用于验证货币,日期和邮件的正则表达式很容易找到,所以我觉得在这里发布它们是多余的。

无关。更多的是你的演示中的currency部分不起作用,它是因为:validate="currency.us"而不是validate="currency",或者至少它在修改后工作。


1
投票

在我看来,不可能创建正则表达式,用于匹配日期或电子邮件等内容与您使用的解析器。这主要是因为你需要在正则表达式中使用非捕获组(这是可能的),这些组不会被解析器函数中的inputValue.replace(pattern, '')调用所取代。这是JavaScript中不可能实现的部分。 JavaScript也取代了您在非捕获组中放置的内容。

那么......你需要采取不同的方法。我建议使用正数正则表达式,当输入有效时将产生匹配。然后,您当然需要更改解析器的代码。例如,您可以决定从输入文本的末尾删除字符,直到剩下的字符通过正则表达式测试。您可以编写如下代码:

  modelCtrl.$parsers.push(function (inputValue) {
    var transformedInput = inputValue;
    while (transformedInput && !pattern.exec(transformedInput)) {
       // validation fails: chop off last character and try again
       transformedInput = transformedInput.slice(0, -1);
    }

    if (transformedInput !== inputValue) {
      modelCtrl.$setViewValue(transformedInput);
      modelCtrl.$render();
    }

    return transformedInput;
  });

现在生活变得容易一些。请注意,您要以不拒绝部分输入的方式制作正则表达式。因此,“01 /”应被视为对日期有效,否则用户永远无法输入日期。另一方面,一旦明确添加字符将不再允许有效输入,正则表达式应该拒绝它。所以“101”应该被拒绝作为日期,因为你永远不能在最后添加字符以使其成为有效日期。

此外,所有这些正则表达式都应该检查整个输入,因此他们需要使用^$符号。

以下是(部分)日期的正则表达式如下所示:

^([0-9]{0,2}|[0-9]{2}[\/]([0-9]{0,2}|[0-9]{2}[\/][0-9]{0,4}))$

这意味着:0到2位的输入有效,或者正好是2位数,后跟斜杠,后跟以下任一项:

  1. 0到2位数,或
  2. 正好是2位数后跟斜线,后跟0到4位数

不可否认,并不像你找到的那样聪明,但是需要进行大量的编辑以允许部分输入日期。这是可能的,但它代表了很长的表达,有很多括号和|

一旦设置了所有正则表达式,您就可以考虑进一步改进解析器。一个想法是不要让它从最后切掉字符,而是让它测试所有字符串,其中一个字符与原始字符相比被移除,并查看哪个字符通过了测试。如果找不到删除一个字符并取得成功,则在输入值的任何位置删除两个连续字符,然后删除三个,等等,直到找到通过测试或达到空值的值为止。

对于用户在输入中间插入字符的情况,这将更好。只是一个想法......


0
投票

import { Directive, ElementRef, EventEmitter, HostListener, Input, Output, Renderer2 } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { CurrencyPipe, DecimalPipe } from '@angular/common';

import { ValueChangeEvent } from '@goomTool/goom-elements/events/value-change-event.model';

const noOperation = () => {
};

@Directive({
    selector: '[formattedNumber]',
    providers: [{
        provide: NG_VALUE_ACCESSOR,
        useExisting: FormattedNumberDirective,
        multi: true
    }]
})
export class FormattedNumberDirective implements ControlValueAccessor {

    @Input() public configuration;
    @Output() public valueChange: EventEmitter<ValueChangeEvent> = new EventEmitter();

    public locale: string = process.env.LOCALE;
    private el: HTMLInputElement;
    // Keeps track of the value without formatting
    private innerInputValue: any;
    private specialKeys: string[] =
        ['Backspace', 'Tab', 'End', 'Home', 'Enter', 'Shift', 'ArrowRight', 'ArrowLeft', 'Delete'];

    private onTouchedCallback: () => void = noOperation;
    private onChangeCallback: (a: any) => void = noOperation;
    constructor(private elementRef: ElementRef,
                private decimalPipe: DecimalPipe,
                private currencyPipe: CurrencyPipe,
                private renderer: Renderer2) {
        this.el = elementRef.nativeElement;
    }

    public writeValue(value: any) {
        if (value !== this.innerInputValue) {
            if (!!value) {
                this.renderer.setAttribute(this.elementRef.nativeElement, 'value', this.getFormattedValue(value));
            }
            this.innerInputValue = value;
        }
    }

    public registerOnChange(fn: any) {
        this.onChangeCallback = fn;
    }

    public registerOnTouched(fn: any) {
        this.onTouchedCallback = fn;
    }

    // On Focus remove all non-digit ,display actual value
    @HostListener('focus', ['$event.target.value'])
    public onfocus(value) {
        if (!!this.innerInputValue) {
            this.el.value = this.innerInputValue;
        }
    }

    // On Blur set values to pipe format
    @HostListener('blur', ['$event.target.value'])
    public onBlur(value) {
        this.innerInputValue = value;
        if (!!value) {
            this.el.value = this.getFormattedValue(value);
        }
    }

    /**
     *  Allows special key, Unit Interval, value based on regular expression
     *
     * @param event
     */

    @HostListener('keydown', ['$event'])
    public onKeyDown(event) {
        // Allow Backspace, tab, end, and home keys . .
        if (this.specialKeys.indexOf(event.key) !== -1) {
            if (event.key === 'Backspace') {
                this.updateValue(this.getBackSpaceValue(this.el.value, event));
            }
            if (event.key === 'Delete') {
                this.updateValue(this.getDeleteValue(this.el.value, event));
            }
            return;
        }
        const next: string = this.concatAtIndex(this.el.value, event);
        if (this.configuration.angularPipe && this.configuration.angularPipe.length > 0) {
            if (!this.el.value.includes('.')
                && (this.configuration.min == null || this.configuration.min < 1)) {
                if (next.startsWith('0') || next.startsWith('0.') || next.startsWith('.')) {
                    if (next.length > 1) {
                        this.updateValue(next);
                    }
                    return;
                }
            }
        }
        /* pass your pattern in component regex e.g. 
        * regex = new RegExp(RegexPattern.WHOLE_NUMBER_PATTERN)
        */
        if (next && !String(next).match(this.configuration.regex)) {
            event.preventDefault();
            return;
        }
        if (!!this.configuration.minFractionDigits && !!this.configuration.maxFractionDigits) {
            if (!!next.split('\.')[1] && next.split('\.')[1].length > this.configuration.minFractionDigits) {
                return this.validateFractionDigits(next, event);
            }
        }
        this.innerInputValue = next;
        this.updateValue(next);
    }

    private updateValue(newValue) {
        this.onTouchedCallback();
        this.onChangeCallback(newValue);
        if (newValue) {
            this.renderer.setAttribute(this.elementRef.nativeElement, 'value', newValue);
        }
    }

    private validateFractionDigits(next, event) {
        // create real-time pattern to validate min & max fraction digits
        const regex = `^[-]?\\d+([\\.,]\\d{${this.configuration.minFractionDigits},${this.configuration.maxFractionDigits}})?$`;
        if (!String(next).match(regex)) {
            event.preventDefault();
            return;
        }
        this.updateValue(next);
    }

    private concatAtIndex(current: string, event) {
        return current.slice(0, event.currentTarget.selectionStart) + event.key +
            current.slice(event.currentTarget.selectionEnd);
    }

    private getBackSpaceValue(current: string, event) {
        return current.slice(0, event.currentTarget.selectionStart - 1) +
            current.slice(event.currentTarget.selectionEnd);
    }

    private getDeleteValue(current: string, event) {
        return current.slice(0, event.currentTarget.selectionStart) +
            current.slice(event.currentTarget.selectionEnd + 1);
    }

    private transformCurrency(value) {
        return this.currencyPipe.transform(value, this.configuration.currencyCode, this.configuration.display,
            this.configuration.digitsInfo, this.locale);
    }

    private transformDecimal(value) {
        return this.decimalPipe.transform(value, this.configuration.digitsInfo, this.locale);
    }

    private transformPercent(value) {
        return this.decimalPipe.transform(value, this.configuration.digitsInfo, this.locale) + ' %';
    }

    private getFormattedValue(value) {
        switch (this.configuration.angularPipe) {
            case ('decimal'): {
                return this.transformDecimal(value);
            }
            case ('currency'): {
                return this.transformCurrency(value);
            }
            case ('percent'): {
                return this.transformPercent(value);
            }
            default: {
                return value;
            }
        }
    }
}

----------------------------------

export const RegexPattern = Object.freeze({
    PERCENTAGE_PATTERN: '^([1-9]\\d*(\\.)\\d*|0?(\\.)\\d*[1-9]\\d*|[1-9]\\d*)$',  // e.g. '.12% ' or 12%
    DECIMAL_PATTERN: '^(([-]+)?([1-9]\\d*(\\.|\\,)\\d*|0?(\\.|\\,)\\d*[1-9]\\d*|[1-9]\\d*))$',  // e.g. '123.12'
    CURRENCY_PATTERN: '\\$?[-]?[0-9]{1,3}(?:,?[0-9]{3})*(?:\\.[0-9]{2})?$',  // e.g. '$123.12'
    KEY_PATTERN: '^[a-zA-Z\\-]+-[0-9]+',    // e.g. ABC-1234
    WHOLE_NUMBER_PATTERN: '^([-]?([1-9][0-9]*)|([0]+)$)$'    // e.g 1234

});
© www.soinside.com 2019 - 2024. All rights reserved.