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

时间:2015-10-27 16:59:35

标签: regex angularjs angularjs-directive angular-ngmodel

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

-

我正在尝试创建一个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;
      });
    }
  };
});

3 个答案:

答案 0 :(得分:2)

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

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

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

要验证打字字符,您可以尝试使用:

JSFiddle DEMO

第一次改变:

12/12/126

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]))./ - 货币(允许字符串如:343243.34,1,123,345.34,.05,带或不带$)
  • /(\.((?=[^\d])|\d{2}(?![^,\d.]))|,((?=[^\d])|\d{3}(?=[^,.$])|(?=\d{1,2}[^\d]))|\$(?=.)|\d{4,}(?=,)).|[^\d,.$]|^\$/ - 日期(00-12 / 00-31 / 0000-2099)
  • ^(((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/] - 数字
  • /^(\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或货币中的@@

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

用于验证货币,日期和邮件的正则表达式很容易找到,因此我发现将它们发布在此处是多余的。

OffTopic。您演示中的34...2部分无法正常工作,因为:currency代替validate="currency.us",或至少它修改后可以使用。

答案 1 :(得分: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位
  3. 不可否认,并不像你找到的那样聪明,但是需要进行大量编辑以允许部分输入日期。这是可能的,但是 它代表了一个很长的表达式,有很多括号和|

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

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

答案 2 :(得分: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

});