Browse Source

Merge pull request #4025 from abpframework/feat/abp-validators

Introduced Reusable Validators Based on Object Attributes
pull/4031/head
Mehmet Erim 6 years ago
committed by GitHub
parent
commit
8b5847aa20
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 191
      npm/ng-packs/packages/core/src/lib/tests/validators.spec.ts
  2. 34
      npm/ng-packs/packages/core/src/lib/validators/credit-card.validator.ts
  3. 20
      npm/ng-packs/packages/core/src/lib/validators/index.ts
  4. 32
      npm/ng-packs/packages/core/src/lib/validators/range.validator.ts
  5. 25
      npm/ng-packs/packages/core/src/lib/validators/required.validator.ts
  6. 38
      npm/ng-packs/packages/core/src/lib/validators/string-length.validator.ts
  7. 25
      npm/ng-packs/packages/core/src/lib/validators/url.validator.ts
  8. 1
      npm/ng-packs/packages/core/src/public-api.ts
  9. 24
      npm/ng-packs/packages/theme-basic/src/lib/theme-basic.module.ts

191
npm/ng-packs/packages/core/src/lib/tests/validators.spec.ts

@ -0,0 +1,191 @@
import { FormControl, Validators } from '@angular/forms';
import { AbpValidators, validateRange } from '../validators';
import { validateCreditCard } from '../validators/credit-card.validator';
import { validateRequired } from '../validators/required.validator';
import { validateStringLength } from '../validators/string-length.validator';
import { validateUrl } from '../validators/url.validator';
describe('Validators', () => {
describe('Credit Card Validator', () => {
const error = { creditCard: true };
test.each`
input | expected
${undefined} | ${null}
${null} | ${null}
${''} | ${null}
${'0'} | ${error}
${'5105105105105100' /* Mastercard */} | ${null}
${'5105105105105101' /* Mastercard */} | ${error}
${'5105 1051 0510 5100'} | ${null}
${'5105-1051-0510-5100'} | ${null}
${'5105 - 1051 - 0510 - 5100'} | ${null}
${'4111111111111111' /*Visa*/} | ${null}
${'4111111111111112' /*Visa*/} | ${error}
${'4012888888881881' /* Visa */} | ${null}
${'4012888888881882' /* Visa */} | ${error}
${'4222222222222' /* Visa */} | ${null}
${'4222222222223' /* Visa */} | ${error}
${'378282246310005' /* American Express */} | ${null}
${'378282246310006' /* American Express */} | ${error}
${'6011111111111117' /* Discover */} | ${null}
${'6011111111111118' /* Discover */} | ${error}
`('should return $expected when input is $input', ({ input, expected }) => {
const control = new FormControl(input, [validateCreditCard()]);
control.markAsDirty({ onlySelf: true });
control.updateValueAndValidity({ onlySelf: true, emitEvent: false });
expect(control.errors).toEqual(expected);
});
it('should return null when control is pristine', () => {
const invalidNumber = '5105105105105101';
const control = new FormControl(invalidNumber, [validateCreditCard()]);
// control is not dirty
expect(control.valid).toBe(true);
});
});
describe('Email Validator', () => {
it('should return email validator of Angular', () => {
expect(AbpValidators.emailAddress()).toBe(Validators.email);
});
});
describe('Range Validator', () => {
test.each`
input | options | expected
${null} | ${undefined} | ${null}
${undefined} | ${undefined} | ${null}
${''} | ${undefined} | ${null}
${0} | ${undefined} | ${null}
${Infinity} | ${undefined} | ${null}
${'-1'} | ${{ minimum: 0 }} | ${{ range: { min: 0, max: Infinity } }}
${-1} | ${{ minimum: 0 }} | ${{ range: { min: 0, max: Infinity } }}
${2} | ${{ minimum: 3, maximum: 5 }} | ${{ range: { min: 3, max: 5 } }}
${3} | ${{ minimum: 3, maximum: 5 }} | ${null}
${5} | ${{ minimum: 3, maximum: 5 }} | ${null}
${6} | ${{ minimum: 3, maximum: 5 }} | ${{ range: { min: 3, max: 5 } }}
`(
'should return $expected when input is $input and options are $options',
({ input, options, expected }) => {
const control = new FormControl(input, [validateRange(options)]);
control.markAsDirty({ onlySelf: true });
control.updateValueAndValidity({ onlySelf: true, emitEvent: false });
expect(control.errors).toEqual(expected);
},
);
it('should return null when control is pristine', () => {
const invalidUrl = '';
const control = new FormControl(invalidUrl, [validateRange({ minimum: 3 })]);
// control is not dirty
expect(control.valid).toBe(true);
});
});
describe('Required Validator', () => {
const error = { required: true };
test.each`
input | options | expected
${0} | ${undefined} | ${null}
${false} | ${undefined} | ${null}
${null} | ${undefined} | ${error}
${undefined} | ${undefined} | ${error}
${''} | ${undefined} | ${error}
${''} | ${{}} | ${error}
${''} | ${{ allowEmptyStrings: false }} | ${error}
${''} | ${{ allowEmptyStrings: true }} | ${null}
`(
'should return $expected when input is $input and options are $options',
({ input, options, expected }) => {
const control = new FormControl(input, [validateRequired(options)]);
control.markAsDirty({ onlySelf: true });
control.updateValueAndValidity({ onlySelf: true, emitEvent: false });
expect(control.errors).toEqual(expected);
},
);
it('should return null when control is pristine', () => {
const invalidUrl = '';
const control = new FormControl(invalidUrl, [validateRequired()]);
// control is not dirty
expect(control.valid).toBe(true);
});
});
describe('String Length Validator', () => {
test.each`
input | options | expected
${null} | ${undefined} | ${null}
${undefined} | ${undefined} | ${null}
${''} | ${undefined} | ${null}
${'ab'} | ${{ minimumLength: 3 }} | ${{ minlength: { requiredLength: 3 } }}
${'abp'} | ${{ minimumLength: 3 }} | ${null}
${'abp'} | ${{ maximumLength: 2 }} | ${{ maxlength: { requiredLength: 2 } }}
${'abp'} | ${{ maximumLength: 3 }} | ${null}
`(
'should return $expected when input is $input and options are $options',
({ input, options, expected }) => {
const control = new FormControl(input, [validateStringLength(options)]);
control.markAsDirty({ onlySelf: true });
control.updateValueAndValidity({ onlySelf: true, emitEvent: false });
expect(control.errors).toEqual(expected);
},
);
it('should return null when control is pristine', () => {
const invalidUrl = '';
const control = new FormControl(invalidUrl, [validateStringLength({ minimumLength: 3 })]);
// control is not dirty
expect(control.valid).toBe(true);
});
});
describe('Url Validator', () => {
const error = { url: true };
test.each`
input | expected
${undefined} | ${null}
${null} | ${null}
${''} | ${null}
${'http://x'} | ${null}
${'http:///x'} | ${error}
${'https://x'} | ${null}
${'https:///x'} | ${error}
${'ftp://x'} | ${null}
${'ftp:///x'} | ${error}
${'http://x.com'} | ${null}
${'http://x.photography'} | ${null}
${'http://www.x.org'} | ${null}
${'http://sub.x.gov.tr'} | ${null}
${'x'} | ${error}
${'x.com'} | ${error}
${'www.x.org'} | ${error}
${'sub.x.gov.tr'} | ${error}
`('should return $expected when input is $input', ({ input, expected }) => {
const control = new FormControl(input, [validateUrl()]);
control.markAsDirty({ onlySelf: true });
control.updateValueAndValidity({ onlySelf: true, emitEvent: false });
expect(control.errors).toEqual(expected);
});
it('should return null when control is pristine', () => {
const invalidUrl = 'x';
const control = new FormControl(invalidUrl, [validateUrl()]);
// control is not dirty
expect(control.valid).toBe(true);
});
});
});

34
npm/ng-packs/packages/core/src/lib/validators/credit-card.validator.ts

@ -0,0 +1,34 @@
import { AbstractControl, ValidatorFn } from '@angular/forms';
export interface CreditCardError {
creditCard: true;
}
export function validateCreditCard(): ValidatorFn {
return (control: AbstractControl): CreditCardError | null => {
if (control.pristine) return null;
if (['', null, undefined].indexOf(control.value) > -1) return null;
return isValidCreditCard(String(control.value)) ? null : { creditCard: true };
};
}
function isValidCreditCard(value: string): boolean {
value = value.replace(/[ -]/g, '');
if (!/^[0-9]{13,19}$/.test(value)) return false;
let checksum = 0;
let multiplier = 1;
for (let i = value.length; i > 0; i--) {
const digit = Number(value[i - 1]) * multiplier;
/* tslint:disable-next-line:no-bitwise */
checksum += (digit % 10) + ~~(digit / 10);
multiplier = (multiplier * 2) % 3;
}
return checksum % 10 === 0;
}

20
npm/ng-packs/packages/core/src/lib/validators/index.ts

@ -0,0 +1,20 @@
import { Validators } from '@angular/forms';
import { validateCreditCard } from './credit-card.validator';
import { validateRange } from './range.validator';
import { validateRequired } from './required.validator';
import { validateStringLength } from './string-length.validator';
import { validateUrl } from './url.validator';
export * from './credit-card.validator';
export * from './range.validator';
export * from './required.validator';
export * from './string-length.validator';
export * from './url.validator';
export const AbpValidators = {
creditCard: validateCreditCard,
emailAddress: () => Validators.email,
range: validateRange,
required: validateRequired,
stringLength: validateStringLength,
url: validateUrl,
};

32
npm/ng-packs/packages/core/src/lib/validators/range.validator.ts

@ -0,0 +1,32 @@
import { AbstractControl, ValidatorFn } from '@angular/forms';
export interface RangeError {
range: {
max: number;
min: number;
};
}
export interface RangeOptions {
maximum?: number;
minimum?: number;
}
export function validateRange({ maximum = Infinity, minimum = 0 }: RangeOptions = {}): ValidatorFn {
return (control: AbstractControl): RangeError | null => {
if (control.pristine) return null;
if (['', null, undefined].indexOf(control.value) > -1) return null;
const value = Number(control.value);
return getMinError(value, minimum, maximum) || getMaxError(value, maximum, minimum);
};
}
function getMaxError(value: number, max: number, min: number): RangeError {
return value > max ? { range: { max, min } } : null;
}
function getMinError(value: number, min: number, max: number): RangeError {
return value < min ? { range: { min, max } } : null;
}

25
npm/ng-packs/packages/core/src/lib/validators/required.validator.ts

@ -0,0 +1,25 @@
import { AbstractControl, ValidatorFn } from '@angular/forms';
export interface RequiredError {
required: true;
}
export interface RequiredOptions {
allowEmptyStrings?: boolean;
}
export function validateRequired({ allowEmptyStrings }: RequiredOptions = {}): ValidatorFn {
return (control: AbstractControl): RequiredError | null => {
return control.pristine || isValidRequired(control.value, allowEmptyStrings)
? null
: { required: true };
};
}
function isValidRequired(value: any, allowEmptyStrings: boolean): boolean {
if (value || value === 0 || value === false) return true;
if (allowEmptyStrings && value === '') return true;
return false;
}

38
npm/ng-packs/packages/core/src/lib/validators/string-length.validator.ts

@ -0,0 +1,38 @@
import { AbstractControl, ValidatorFn } from '@angular/forms';
export interface StringLengthError {
maxlength?: {
requiredLength: number;
};
minlength?: {
requiredLength: number;
};
}
export interface StringLengthOptions {
maximumLength?: number;
minimumLength?: number;
}
export function validateStringLength({
maximumLength = Infinity,
minimumLength = 0,
}: StringLengthOptions = {}): ValidatorFn {
return (control: AbstractControl): StringLengthError | null => {
if (control.pristine) return null;
if (['', null, undefined].indexOf(control.value) > -1) return null;
const value = String(control.value);
return getMinLengthError(value, minimumLength) || getMaxLengthError(value, maximumLength);
};
}
function getMaxLengthError(value: string, requiredLength: number): StringLengthError {
return value.length > requiredLength ? { maxlength: { requiredLength } } : null;
}
function getMinLengthError(value: string, requiredLength: number): StringLengthError {
return value.length < requiredLength ? { minlength: { requiredLength } } : null;
}

25
npm/ng-packs/packages/core/src/lib/validators/url.validator.ts

@ -0,0 +1,25 @@
import { AbstractControl, ValidatorFn } from '@angular/forms';
export interface UrlError {
url: true;
}
export function validateUrl(): ValidatorFn {
return (control: AbstractControl): UrlError | null => {
if (control.pristine) return null;
if (['', null, undefined].indexOf(control.value) > -1) return null;
return isValidUrl(control.value) ? null : { url: true };
};
}
function isValidUrl(value: string): boolean {
if (/^http(s)?:\/\/[^/]/.test(value) || /^ftp:\/\/[^/]/.test(value)) {
const a = document.createElement('a');
a.href = value;
return !!a.host;
}
return false;
}

1
npm/ng-packs/packages/core/src/public-api.ts

@ -20,3 +20,4 @@ export * from './lib/states';
export * from './lib/strategies';
export * from './lib/tokens';
export * from './lib/utils';
export * from './lib/validators';

24
npm/ng-packs/packages/theme-basic/src/lib/theme-basic.module.ts

@ -7,12 +7,12 @@ import { NgxsModule } from '@ngxs/store';
import { AccountLayoutComponent } from './components/account-layout/account-layout.component';
import { ApplicationLayoutComponent } from './components/application-layout/application-layout.component';
import { EmptyLayoutComponent } from './components/empty-layout/empty-layout.component';
import { LayoutState } from './states/layout.state';
import { ValidationErrorComponent } from './components/validation-error/validation-error.component';
import { InitialService } from './services/initial.service';
import { LogoComponent } from './components/logo/logo.component';
import { RoutesComponent } from './components/routes/routes.component';
import { NavItemsComponent } from './components/nav-items/nav-items.component';
import { RoutesComponent } from './components/routes/routes.component';
import { ValidationErrorComponent } from './components/validation-error/validation-error.component';
import { InitialService } from './services/initial.service';
import { LayoutState } from './states/layout.state';
export const LAYOUTS = [ApplicationLayoutComponent, AccountLayoutComponent, EmptyLayoutComponent];
@ -34,15 +34,19 @@ export const LAYOUTS = [ApplicationLayoutComponent, AccountLayoutComponent, Empt
NgxValidateCoreModule.forRoot({
targetSelector: '.form-group',
blueprints: {
email: 'AbpAccount::ThisFieldIsNotAValidEmailAddress.',
max: 'AbpAccount::ThisFieldMustBeBetween{0}And{1}[{{ min }},{{ max }}]',
creditCard: 'AbpValidation::ThisFieldIsNotAValidCreditCardNumber.',
email: 'AbpValidation::ThisFieldIsNotAValidEmailAddress.',
max: 'AbpValidation::ThisFieldMustBeBetween{0}And{1}[{{ min }},{{ max }}]',
maxlength:
'AbpAccount::ThisFieldMustBeAStringOrArrayTypeWithAMaximumLengthOf{0}[{{ requiredLength }}]',
min: 'AbpAccount::ThisFieldMustBeBetween{0}And{1}[{{ min }},{{ max }}]',
'AbpValidation::ThisFieldMustBeAStringOrArrayTypeWithAMaximumLengthOf{0}[{{ requiredLength }}]',
min: 'AbpValidation::ThisFieldMustBeBetween{0}And{1}[{{ min }},{{ max }}]',
minlength:
'AbpAccount::ThisFieldMustBeAStringOrArrayTypeWithAMinimumLengthOf{0}[{{ requiredLength }}]',
required: 'AbpAccount::ThisFieldIsRequired.',
'AbpValidation::ThisFieldMustBeAStringOrArrayTypeWithAMinimumLengthOf{0}[{{ requiredLength }}]',
ngbDate: 'AbpValidation::ThisFieldIsNotValid.',
passwordMismatch: 'AbpIdentity::Identity.PasswordConfirmationFailed',
range: 'AbpValidation::ThisFieldMustBeBetween{0}And{1}[{{ min }},{{ max }}]',
required: 'AbpValidation::ThisFieldIsRequired.',
url: 'AbpValidation::ThisFieldIsNotAValidFullyQualifiedHttpHttpsOrFtpUrl',
},
errorTemplate: ValidationErrorComponent,
}),

Loading…
Cancel
Save