Commit 2b0e0ceb authored by Ryan Diehl's avatar Ryan Diehl

Merge branch 'feature/form' into 'develop'

Feature/form

See merge request !27
parents 2a251efc f99603ce
Pipeline #100124 passed with stages
in 4 minutes and 48 seconds
......@@ -420,6 +420,34 @@
"styleext": "scss"
}
}
},
"utils-form": {
"projectType": "library",
"root": "libs/utils/form",
"sourceRoot": "libs/utils/form/src",
"prefix": "ut",
"architect": {
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": ["libs/utils/form/tsconfig.lib.json", "libs/utils/form/tsconfig.spec.json"],
"exclude": ["**/node_modules/**", "!libs/utils/form/**"]
}
},
"test": {
"builder": "@nrwl/jest:jest",
"options": {
"jestConfig": "libs/utils/form/jest.config.js",
"tsConfig": "libs/utils/form/tsconfig.spec.json",
"setupFile": "libs/utils/form/src/test-setup.ts"
}
}
},
"schematics": {
"@nrwl/angular:component": {
"styleext": "scss"
}
}
}
},
"cli": {
......
# @psu/utils/form
Provides useful functions for working with @angular/forms,
and a directive to let you easily disable a form control.
module.exports = {
name: 'utils-form',
preset: '../../../jest.config.js',
coverageDirectory: '../../../coverage/libs/utils/form',
snapshotSerializers: [
'jest-preset-angular/AngularSnapshotSerializer.js',
'jest-preset-angular/HTMLCommentSerializer.js'
]
};
{
"lib": {
"entryFile": "src/index.ts"
}
}
export * from './lib/disable-control/disable-control.directive';
export * from './lib/disable-control/disable-control.module';
export * from './lib/utils/form.utils';
export { PsuValidators } from './lib/validators/psu.validators';
import { Component, Input } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { By } from '@angular/platform-browser';
import { DisableControlDirective } from './disable-control.directive';
@Component({
template: `
<input [formControl]="control" [utDisableControl]="disabled" />
`
})
class TestComponent {
@Input()
public disabled: boolean;
public control: FormControl;
constructor() {
this.control = new FormControl();
}
}
describe('DisableControlDirective', () => {
let fixture: ComponentFixture<TestComponent>;
let component: TestComponent;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ReactiveFormsModule],
declarations: [TestComponent, DisableControlDirective]
});
fixture = TestBed.createComponent(TestComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeDefined();
});
function getInput(): HTMLInputElement {
return fixture.debugElement.query(By.css('input')).nativeElement;
}
it('should disable input when disabled is true', () => {
component.disabled = true;
fixture.detectChanges();
expect(component.control.disabled).toBe(true);
expect(getInput().disabled).toBe(true);
});
it('should enable input when disabled is false', () => {
component.disabled = false;
fixture.detectChanges();
expect(component.control.disabled).toBe(false);
expect(getInput().disabled).toBe(false);
});
});
import { Directive, Input } from '@angular/core';
import { NgControl } from '@angular/forms';
@Directive({
selector: '[utDisableControl]'
})
export class DisableControlDirective {
constructor(private ngControl: NgControl) {}
@Input('utDisableControl')
public set disableControl(condition: boolean) {
const action = !!condition ? 'disable' : 'enable';
this.ngControl.control[action]();
}
}
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { DisableControlDirective } from './disable-control.directive';
@NgModule({
imports: [CommonModule, FormsModule, ReactiveFormsModule],
declarations: [DisableControlDirective],
exports: [DisableControlDirective]
})
export class DisableControlModule {}
import { FormUtils } from './form.utils';
describe('FormUtils', () => {
describe('safeTrim', () => {
it('should return original value when value is falsy', () => {
expect(FormUtils.safeTrim(undefined)).toBeUndefined();
});
it('should return original value when value has no whitespace', () => {
expect(FormUtils.safeTrim('me')).toBe('me');
});
it('should trim leading and trailing whitespace', () => {
expect(FormUtils.safeTrim(' some ')).toBe('some');
});
});
});
import { FormGroup, AbstractControl, FormArray } from '@angular/forms';
export class FormUtils {
public static touchAllFields(group: FormGroup): void {
Object.keys(group.controls).forEach(controlName => {
const control: AbstractControl = group.get(controlName);
if (control instanceof FormArray) {
const arr = control as FormArray;
for (let i = 0; i < arr.length; ++i) {
FormUtils.touchAllFields(arr.at(i) as FormGroup);
}
} else if (control instanceof FormGroup) {
FormUtils.touchAllFields(control);
} else {
control.markAsTouched({ onlySelf: true });
}
});
}
public static safeTrim(value: string): string {
return value ? value.trim() : value;
}
}
import { emailValidator } from './email.validator';
import { FormControl } from '@angular/forms';
function getControl(value: string): FormControl {
const c = new FormControl();
c.patchValue(value);
return c;
}
describe('emailValidator', () => {
it('empty string should be valid', () => {
expect(emailValidator(getControl(''))).toBeNull();
});
it('valid email should be valid', () => {
expect(emailValidator(getControl('irock2@gmail.com'))).toBeNull();
});
it('invalid email with special characters ending should be invalid', () => {
expect(emailValidator(getControl('irock2@gmail.co!'))).toEqual({
validateEmail: {
valid: false
},
email: true
});
});
it('invalid email should have error', () => {
expect(emailValidator(getControl('invalid'))).toEqual({
validateEmail: {
valid: false
},
email: true
});
});
});
import { ValidationErrors, AbstractControl } from '@angular/forms';
// tslint:disable-next-line:max-line-length
const RE: RegExp = new RegExp(
"^[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\\.)+[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$"
);
/**
* Validates an email address using the same regular expression the server uses.
*
* @param email the form control to validate
*/
export function emailValidator(email: AbstractControl): ValidationErrors {
const value = email.value;
if (!value) {
return null;
}
return RE.test(email.value)
? null
: {
email: true,
validateEmail: {
valid: false
}
};
}
import { FormControl } from '@angular/forms';
import { minLengthValidator, maxLengthValidator } from './length.validators';
describe('length validators', () => {
function getControl(value: any): FormControl {
const c = new FormControl();
c.patchValue(value);
return c;
}
describe('minLength', () => {
it('should be valid when empty', () => {
expect(minLengthValidator(3)(getControl(''))).toBeNull();
});
it('should be valid when at length', () => {
expect(minLengthValidator(3)(getControl('abc'))).toBeNull();
});
it('should be invalid when less than length', () => {
expect(minLengthValidator(2)(getControl('a'))).toEqual({
minLength: {
requiredLength: 2,
actualLength: 1
}
});
});
it('should be invalid when less than length when trimmed', () => {
expect(minLengthValidator(3)(getControl(' a '))).toEqual({
minLength: {
requiredLength: 3,
actualLength: 1
}
});
});
});
describe('maxLength', () => {
it('should be valid when empty', () => {
expect(maxLengthValidator(3)(getControl(''))).toBeNull();
});
it('should be valid when at length', () => {
expect(maxLengthValidator(3)(getControl('abc'))).toBeNull();
});
it('should be invalid when greater than length', () => {
expect(maxLengthValidator(2)(getControl('abc'))).toEqual({
maxLength: {
requiredLength: 2,
actualLength: 3
}
});
});
it('should be invalid when greater than length when trimmed', () => {
expect(maxLengthValidator(3)(getControl(' abcd '))).toEqual({
maxLength: {
requiredLength: 3,
actualLength: 4
}
});
});
it('should be valid when at length when trimmed', () => {
expect(maxLengthValidator(3)(getControl(' abc '))).toBeNull();
});
});
});
import { ValidatorFn, AbstractControl, ValidationErrors } from '@angular/forms';
function isEmptyValue(value: any): boolean {
return value === undefined || value === null || (typeof value === 'string' && value.trim().length === 0);
}
export function minLengthValidator(minLength: number): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
if (isEmptyValue(control.value)) {
return null;
}
let length;
if (control.value) {
if (typeof control.value === 'string') {
length = control.value.trim().length;
} else {
length = control.value.length;
}
} else {
length = 0;
}
return length < minLength
? {
minLength: {
requiredLength: minLength,
actualLength: length
}
}
: null;
};
}
export function maxLengthValidator(maxLength: number): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
if (isEmptyValue(control.value)) {
return null;
}
let length;
if (control.value) {
if (typeof control.value === 'string') {
length = control.value.trim().length;
} else {
length = control.value.length;
}
} else {
length = 0;
}
return length > maxLength
? {
maxLength: {
requiredLength: maxLength,
actualLength: length
}
}
: null;
};
}
import { FormControl } from '@angular/forms';
import { nonPsuEmailValidator } from './non-psu-email.validator';
describe('nonPsuEmailValidator', () => {
function getValue(val: string): FormControl {
const ret = new FormControl();
ret.patchValue(val);
return ret;
}
it('should fail email address @psu.edu', () => {
expect(nonPsuEmailValidator(getValue('someone@psu.edu'))).toEqual({
psuEmail: true
});
});
it('should pass email address from a subdomain under psu.edu', () => {
expect(nonPsuEmailValidator(getValue('something@somewhere.psu.edu'))).toBeNull();
});
it('should pass email from another tld', () => {
expect(nonPsuEmailValidator(getValue('someone@google.com'))).toBeNull();
});
it('should pass email from another tricky tld', () => {
expect(nonPsuEmailValidator(getValue('someone@psu.edu.com'))).toBeNull();
});
});
import { AbstractControl, ValidationErrors } from '@angular/forms';
export function nonPsuEmailValidator(email: AbstractControl): ValidationErrors {
const regexp = /^.+@psu.edu$/i;
return regexp.test(email.value) ? { psuEmail: true } : null;
}
import { notAnEmailValidator } from './not-email.validator';
import { FormControl } from '@angular/forms';
describe('notAnEmailValidator', () => {
function getValue(val: string): FormControl {
const ret = new FormControl();
ret.patchValue(val);
return ret;
}
it('when value contains an at sign only', () => {
expect(notAnEmailValidator(getValue('@m'))).toEqual({ notAnEmail: true });
});
it('when value contains an at sign at the beginning', () => {
expect(notAnEmailValidator(getValue('@me'))).toEqual({ notAnEmail: true });
});
it('when value contains an at sign at the end', () => {
expect(notAnEmailValidator(getValue('me@'))).toEqual({ notAnEmail: true });
});
it('when value contains an at sign in the middle', () => {
expect(notAnEmailValidator(getValue('you@me'))).toEqual({
notAnEmail: true
});
});
it('when value does not contain an at sign', () => {
expect(notAnEmailValidator(getValue('rpd123'))).toBeNull();
});
it('when value is empty', () => {
expect(notAnEmailValidator(getValue(''))).toBeNull();
});
});
import { ValidationErrors, AbstractControl } from '@angular/forms';
export function notAnEmailValidator(control: AbstractControl): ValidationErrors {
return /@/.test(control.value)
? {
notAnEmail: true
}
: null;
}
import { FormControl } from '@angular/forms';
import { office365EmailValidator } from './office365-email.validator';
describe('psuEmailValidator', () => {
function getControl(value: string): FormControl {
const c = new FormControl();
c.patchValue(value);
return c;
}
it('empty string should be valid', () => {
expect(office365EmailValidator(getControl(''))).toBeNull();
});
it('non-pennstateoffice365.onmicrosoft.com email should be valid', () => {
expect(office365EmailValidator(getControl('irock@gmail.com'))).toBeNull();
});
it('@pennstateoffice365.onmicrosoft.com email should be invalid', () => {
expect(office365EmailValidator(getControl('me@pennstateoffice365.onmicrosoft.com'))).toEqual({
psuEmail: true
});
});
it('@pennstateoffice365.onmicrosoft.com email with uppercase should be invalid', () => {
expect(office365EmailValidator(getControl('me@pennstateOFFice365.onmicROsoft.com'))).toEqual({
psuEmail: true
});
});
});
import { ValidationErrors, AbstractControl } from '@angular/forms';
/*
Validates that the supplied email is not an office 365 extension.
*/
export function office365EmailValidator(email: AbstractControl): ValidationErrors {
return /^.+@pennstateoffice365.onmicrosoft.com$/i.test(email.value)
? {
psuEmail: true
}
: null;
}
import { FormControl } from '@angular/forms';
import { psuEmailValidator, psuStrictEmailValidator } from './psu-email.validator';
function getControl(value: string): FormControl {
const c = new FormControl();
c.patchValue(value);
return c;
}
describe('psuEmailValidator', () => {
it('empty string should be valid', () => {
expect(psuEmailValidator(getControl(''))).toBeNull();
});
it('non-psu email should be valid', () => {
expect(psuEmailValidator(getControl('irock@gmail.com'))).toBeNull();
});
it('@psu.edu email should be invalid', () => {
expect(psuEmailValidator(getControl('me@psu.edu'))).toEqual({
psuEmail: true
});
});
it('@psu.edu email with uppercase should be invalid', () => {
expect(psuEmailValidator(getControl('me@PSU.EDU'))).toEqual({
psuEmail: true
});
});
it('@engr.psu.edu email should be invalid', () => {
expect(psuEmailValidator(getControl('me@engr.psu.edu'))).toEqual({
psuEmail: true
});
});
it('@engr.psu.edu email with uppercase should be invalid', () => {
expect(psuEmailValidator(getControl('me@ENGR.pSu.eDu'))).toEqual({
psuEmail: true
});
});
it('@garbage.psu.edu.something.else.domain email should be valid', () => {
expect(psuEmailValidator(getControl('me@garbage.psu.edu.something.else.domain'))).toBeNull();
});
it('@garbage.psu.edu.something.else.domain email when upper should be valid', () => {
expect(psuEmailValidator(getControl('me@garbage.PSU.edu.something.ELSE.domain'))).toBeNull();
});
it('@alumni.psu.edu email should be valid', () => {
expect(psuEmailValidator(getControl('me@alumni.psu.edu'))).toBeNull();
});
it('@alumni.psu.edu email when uppercase should be valid', () => {
expect(psuEmailValidator(getControl('ME@ALUMNI.PSU.EDU'))).toBeNull();
});
it('@arl.psu.edu email should be valid', () => {
expect(psuEmailValidator(getControl('me@arl.psu.edu'))).toBeNull();
});
it('@arl.psu.edu email when uppercase should be valid', () => {
expect(psuEmailValidator(getControl('me@ARL.PSU.EDU'))).toBeNull();
});
it('@pennstatehealth.psu.edu email should be valid', () => {
expect(psuEmailValidator(getControl('me@pennstatehealth.psu.edu'))).toBeNull();
});