diff options
Diffstat (limited to 'src/angular/form-elements')
28 files changed, 937 insertions, 0 deletions
diff --git a/src/angular/form-elements/checkbox/checkbox.component.html.ts b/src/angular/form-elements/checkbox/checkbox.component.html.ts new file mode 100644 index 0000000..f4031db --- /dev/null +++ b/src/angular/form-elements/checkbox/checkbox.component.html.ts @@ -0,0 +1,8 @@ +export default ` +<div class="sdc-checkbox"> + <label SdcRippleClickAnimation [rippleClickDisabled]="disabled"> + <input type="checkbox" class="sdc-checkbox__input" [ngModel]="checked" (ngModelChange)="toggleState($event)" [disabled]="disabled"> + <span class="sdc-checkbox__label">{{ label }}</span> + </label> +</div> +`; diff --git a/src/angular/form-elements/checkbox/checkbox.component.spec.ts b/src/angular/form-elements/checkbox/checkbox.component.spec.ts new file mode 100644 index 0000000..36f478e --- /dev/null +++ b/src/angular/form-elements/checkbox/checkbox.component.spec.ts @@ -0,0 +1,37 @@ +import { TestBed, async } from '@angular/core/testing'; +import { CheckboxComponent } from "./checkbox.component"; +import { AnimationDirectivesModule } from "../../animations/animation-directives.module"; +import { FormsModule } from "@angular/forms"; + + +describe("Checbox Tests", ()=>{ + let component: CheckboxComponent; + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ + CheckboxComponent + ], + imports:[ + FormsModule, + AnimationDirectivesModule + ] + }).compileComponents(); + const fixture = TestBed.createComponent(CheckboxComponent); + component = fixture.componentInstance; + })); + + it("Component Created", async(()=> { + expect(component).toBeDefined(); + })); + + it( "Test Value suppose to be toggled", async( ()=> { + component.toggleState(true) + expect(component.checked).toEqual(true); + })); + + it( "If disabled not toggled"), async(()=>{ + component.disabled = true; + component.toggleState(true); + expect(component.checked).toEqual(false); + }); +}); diff --git a/src/angular/form-elements/checkbox/checkbox.component.ts b/src/angular/form-elements/checkbox/checkbox.component.ts new file mode 100644 index 0000000..ec05eac --- /dev/null +++ b/src/angular/form-elements/checkbox/checkbox.component.ts @@ -0,0 +1,21 @@ +import { Component, Input, Output, EventEmitter, ViewEncapsulation } from '@angular/core'; +import template from "./checkbox.component.html"; + +@Component({ + selector: 'sdc-checkbox', + template: template, + encapsulation: ViewEncapsulation.None +}) +export class CheckboxComponent { + @Input() label:string; + @Input() checked:boolean; + @Input() disabled:boolean; + @Output() checkedChange:EventEmitter<any> = new EventEmitter<any>(); + + public toggleState(newState:boolean) { + if (!this.disabled) { + this.checked = newState; + this.checkedChange.emit(newState); + } + } +} diff --git a/src/angular/form-elements/dropdown/dropdown-models.ts b/src/angular/form-elements/dropdown/dropdown-models.ts new file mode 100644 index 0000000..fa8dc23 --- /dev/null +++ b/src/angular/form-elements/dropdown/dropdown-models.ts @@ -0,0 +1,18 @@ +export enum DropDownTypes { + Regular, + Headless, + Auto +} + +export enum DropDownOptionType { + Simple, // default + Header, + Disable, + HorizontalLine +} + +export interface IDropDownOption { + value: any; + label: string; + type?: DropDownOptionType; +} diff --git a/src/angular/form-elements/dropdown/dropdown-trigger.directive.ts b/src/angular/form-elements/dropdown/dropdown-trigger.directive.ts new file mode 100644 index 0000000..94ab3bc --- /dev/null +++ b/src/angular/form-elements/dropdown/dropdown-trigger.directive.ts @@ -0,0 +1,17 @@ +import { Directive, Input, HostBinding, HostListener } from "@angular/core"; +import { DropDownComponent } from "./dropdown.component"; + +@Directive({ + selector: '[SdcDropdownTrigger]' +}) + +export class DropDownTriggerDirective { + + @HostBinding('class.js-sdc-dropdown--toggle-hook') true; + @Input() dropDown: DropDownComponent; + + @HostListener('click', ['$event']) onClick = (event) => { + this.dropDown.toggleDropdown(event); + } + +} diff --git a/src/angular/form-elements/dropdown/dropdown.component.html.ts b/src/angular/form-elements/dropdown/dropdown.component.html.ts new file mode 100644 index 0000000..a4247a4 --- /dev/null +++ b/src/angular/form-elements/dropdown/dropdown.component.html.ts @@ -0,0 +1,59 @@ +export default ` +<div class="sdc-dropdown" #dropDownWrapper + [ngClass]="{ + 'headless': type === cIDropDownTypes.Headless, + 'sdc-dropdown__error': !valid, + 'open-bottom': show && bottomVisible, + 'open-top':show && !bottomVisible}"> + <label *ngIf="label" class="sdc-dropdown__label" [ngClass]="{'required':required}">{{label}}</label> + <div class="sdc-dropdown__component-container"> + + <!--[DROP-DOWN AUTO HEADER START]--> + <div *ngIf="type===cIDropDownTypes.Auto" class="sdc-dropdown-auto__wrapper"> + <input class="sdc-dropdown__header js-sdc-dropdown--toggle-hook" + [(ngModel)]="this.filterValue" + (ngModelChange)="filterOptions(this.filterValue)" + placeholder="{{this.selectedOption?.label || this.selectedOption?.value || placeHolder}}"> + <svg-icon name="caret1-down-o" mode="secondary" size="small" (click)="toggleDropdown($event)"></svg-icon> + </div> + <!--[DROP-DOWN AUTO HEADER END]--> + + <!--[DROP-DOWN REGULAR HEADER START]--> + <button *ngIf="type===cIDropDownTypes.Regular" + class="sdc-dropdown__header js-sdc-dropdown--toggle-hook" + (click)="toggleDropdown($event)" + [ngClass]="{'disabled': disabled, 'placeholder':!this.selectedOption}"> + {{ this.selectedOption?.label || this.selectedOption?.value || placeHolder}} + <svg-icon name="caret1-down-o" mode="secondary" size="small"></svg-icon> + </button> + <!--[DROP-DOWN HEADER END]--> + + <!--[DROP-DOWN OPTIONS START]--> + <div class="sdc-dropdown__options-wrapper--frame" [ngClass]="{ + 'sdc-dropdown__options-wrapper--top':!bottomVisible, + 'sdc-dropdown__options-wrapper--uncollapsed':show + }"> + <ul #optionsContainerElement *ngIf="options" class="sdc-dropdown__options-list" [ngClass]="{ + 'sdc-dropdown__options-list--headless': headless, + 'sdc-dropdown__options-list--animation-init':animation_init + }"> + <ng-container *ngFor="let option of options; let i = index"> + <!--[Drop down item list or string list start]--> + <li *ngIf="option" class="sdc-dropdown__option" + [ngClass]="{ + 'selected': option == selectedOption, + 'sdc-dropdown__option--group':isGroupDesign, + 'sdc-dropdown__option--header': option.type && option.type === cIDropDownOptionType.Header, + 'sdc-dropdown__option--disabled': option.type && option.type === cIDropDownOptionType.Disable, + 'sdc-dropdown__option--hr': option.type && option.type === cIDropDownOptionType.HorizontalLine + }" + (click)="selectOption(option.value, $event)">{{option.label || String(option.value)}}</li> + <!--[Drop down item list or string list end]--> + </ng-container> + </ul> + </div> + <!--[DROP-DOWN OPTIONS END]--> + + </div> +</div> +`; diff --git a/src/angular/form-elements/dropdown/dropdown.component.spec.ts b/src/angular/form-elements/dropdown/dropdown.component.spec.ts new file mode 100644 index 0000000..1c0cb4d --- /dev/null +++ b/src/angular/form-elements/dropdown/dropdown.component.spec.ts @@ -0,0 +1,71 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { DropDownComponent } from './dropdown.component'; +import { IDropDownOption, DropDownTypes } from "./dropdown-models"; +import { FormsModule } from "@angular/forms"; +import {SvgIconModule} from "../../svg-icon/svg-icon.module"; + + +const label:string = "DropDown example"; +const placeHolder:string = "Please choose option"; +const options:IDropDownOption[] = [ + { + label:'First Option', + value: 'First Option' + }, + { + label:'Second Option', + value: 'Second Option' + }, + { + label:'Third Option', + value: 'Third Option' + } +]; + +describe('DropDown component', () => { + let fixture: ComponentFixture<DropDownComponent>; + let component: DropDownComponent; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ DropDownComponent ], + imports:[ + FormsModule, + SvgIconModule + ] + }).compileComponents(); + fixture = TestBed.createComponent(DropDownComponent); + component = fixture.componentInstance; + + })); + + beforeEach(()=>{ + component.label = label; + component.placeHolder = placeHolder; + component.options = options; + component.type = DropDownTypes.Regular; + console.log('herer we got component', component) + fixture.detectChanges(); + }); + + it('component should be created', () => { + expect(component).toBeTruthy(); + }); + + it('component should export the selected value', () => { + const option = options[1]; + component.selectOption(option); + fixture.detectChanges(); + expect(component.selectedOption).toEqual(option); + }); + + it('component should have autocomplite', () => { + expect(component.options.length).toEqual(3); + component.type = DropDownTypes.Auto; + component.filterValue = 'testERrorotesttresadfadfasdfasf'; + fixture.detectChanges(); + component.filterOptions(component.filterValue); + expect(component.options.length).toEqual(0); + }); + +}); diff --git a/src/angular/form-elements/dropdown/dropdown.component.ts b/src/angular/form-elements/dropdown/dropdown.component.ts new file mode 100644 index 0000000..a23072f --- /dev/null +++ b/src/angular/form-elements/dropdown/dropdown.component.ts @@ -0,0 +1,149 @@ +import { Component, EventEmitter, Input, Output, forwardRef, OnChanges, SimpleChanges, OnInit, ElementRef, ViewChild, AfterViewInit, HostListener, Renderer } from '@angular/core'; +import { IDropDownOption, DropDownOptionType, DropDownTypes } from "./dropdown-models"; +import { ValidatableComponent } from './../validation/validatable.component'; +import template from './dropdown.component.html'; + +@Component({ + selector: 'sdc-dropdown', + template: template +}) +export class DropDownComponent extends ValidatableComponent implements OnChanges, OnInit { + + @Output('changed') changeEmitter:EventEmitter<IDropDownOption> = new EventEmitter<IDropDownOption>(); + @Input() label: string; + @Input() options: IDropDownOption[]; + @Input() disabled: boolean; + @Input() placeHolder: string; + @Input() required: boolean; + @Input() maxHeight: number; + @Input() selectedOption: IDropDownOption; + @Input() type: DropDownTypes = DropDownTypes.Regular; + @ViewChild('dropDownWrapper') dropDownWrapper: ElementRef; + @ViewChild('optionsContainerElement') optionsContainerElement: ElementRef; + @HostListener('document:click', ['$event']) onClick(e) { + this.onClickDocument(e); + } + + private bottomVisible = true; + private myRenderer: Renderer; + + // Drop-down show/hide flag. default is false (closed) + public show = false; + + // Export DropDownOptionType enum so we can use it on the template + public cIDropDownOptionType = DropDownOptionType; + public cIDropDownTypes = DropDownTypes; + + // Configure unselectable option types + private unselectableOptions = [ + DropDownOptionType.Disable, + DropDownOptionType.Header, + DropDownOptionType.HorizontalLine + ]; + + // Set or unset Group style on drop-down + public isGroupDesign = false; + public animation_init = false; + public allOptions: IDropDownOption[]; + public filterValue: string; + + constructor(public renderer: Renderer) { + super(); + this.myRenderer = renderer; + this.maxHeight = 244; + this.filterValue = ''; + } + + ngOnInit(): void { + if (this.options) { + this.allOptions = this.options; + if (this.options.find(option => option.type === DropDownOptionType.Header)) { + this.isGroupDesign = true; + } + } + } + + ngOnChanges(changes: SimpleChanges): void { + console.log("ngOnChanges"); + if (changes.selectedOption && changes.selectedOption.currentValue !== changes.selectedOption.previousValue) { + if (typeof changes.selectedOption.currentValue === 'string' && this.isSelectable(changes.selectedOption.currentValue)) { + this.setSelected(changes.selectedOption.currentValue); + } else if (this.isSelectable(changes.selectedOption.currentValue.value)) { + this.setSelected(changes.selectedOption.currentValue.value); + } else { + this.setSelected(undefined); + } + } + } + + public getValue(): any { + return this.selectedOption && this.selectedOption.value; + } + + public selectOption = (option: IDropDownOption | string, event?): void => { + if (event) { event.stopPropagation(); } + if (this.type === DropDownTypes.Headless) { + // Hide the options when in headless mode and user select option. + this.myRenderer.setElementStyle(this.dropDownWrapper.nativeElement, 'display', 'none'); + } + if (typeof option === 'string' && this.isSelectable(option)) { + this.setSelected(option); + } else if (this.isSelectable((option as IDropDownOption).value)) { + this.setSelected((option as IDropDownOption).value); + } + } + + public toggleDropdown = (event?): void => { + if (event) { event.stopPropagation(); } + if (this.type === DropDownTypes.Headless) { + // Show the options when in headless mode. + this.myRenderer.setElementStyle(this.dropDownWrapper.nativeElement, 'display', 'block'); + } + if (this.disabled) { return; } + this.animation_init = true; + this.bottomVisible = this.isBottomVisible(); + this.show = !this.show; + } + + public filterOptions = (filterValue): void => { + if (filterValue.length >= 1 && !this.show) { this.toggleDropdown(); } + if (this.selectedOption) { this.selectedOption = null; } + this.options = this.allOptions.filter((option) => { + return option.value.toLowerCase().indexOf(filterValue.toLowerCase()) > -1; + }); + } + + private isSelectable = (value: string): boolean => { + const option: IDropDownOption = this.options.find(o => o.value === value); + if (!option) { return false; } + if (!option.type) { return true; } + return !this.unselectableOptions.find(optionType => optionType === option.type); + } + + private setSelected = (value: string): void => { + this.selectedOption = this.options.find(o => o.value === value); + if (this.type === DropDownTypes.Auto) { this.filterValue = value; } + this.show = false; + this.changeEmitter.next(this.selectedOption); + } + + private isBottomVisible = (): boolean => { + const windowPos = window.innerHeight + window.pageYOffset; + const boundingRect = this.dropDownWrapper.nativeElement.getBoundingClientRect(); + const dropDownPos = boundingRect.top + boundingRect.height + this.maxHeight; + return windowPos > dropDownPos; + } + + private onClickDocument = (event): void => { + if (this.type === DropDownTypes.Headless) { + if (!this.optionsContainerElement.nativeElement.contains(event.target)) { + this.show = false; + } + } else { + if (!this.dropDownWrapper.nativeElement.contains(event.target)) { + this.show = false; + } + } + } + +} diff --git a/src/angular/form-elements/form-elements.module.ts b/src/angular/form-elements/form-elements.module.ts new file mode 100644 index 0000000..744f8b8 --- /dev/null +++ b/src/angular/form-elements/form-elements.module.ts @@ -0,0 +1,38 @@ +import { NgModule } from "@angular/core"; +import { FormsModule, ReactiveFormsModule } from "@angular/forms"; +import { InputComponent } from "./input/input.component"; +import { DropDownComponent } from "./dropdown/dropdown.component"; +import { CommonModule } from "@angular/common"; +import { CheckboxComponent } from "./checkbox/checkbox.component"; +import { RadioGroupComponent } from "./radios/radio-buttons-group.component"; +import { AnimationDirectivesModule } from '../animations/animation-directives.module'; +import { DropDownTriggerDirective } from "./dropdown/dropdown-trigger.directive"; +import {SvgIconModule} from "../svg-icon/svg-icon.module"; +import { ValidationModule } from './validation/validation.module'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + AnimationDirectivesModule, + SvgIconModule + ], + declarations: [ + DropDownComponent, + InputComponent, + CheckboxComponent, + RadioGroupComponent, + DropDownTriggerDirective, + ], + exports: [ + DropDownComponent, + DropDownTriggerDirective, + InputComponent, + CheckboxComponent, + RadioGroupComponent, + ValidationModule + ] +}) +export class FormElementsModule { +} diff --git a/src/angular/form-elements/input/input.component.html.ts b/src/angular/form-elements/input/input.component.html.ts new file mode 100644 index 0000000..f8a4609 --- /dev/null +++ b/src/angular/form-elements/input/input.component.html.ts @@ -0,0 +1,19 @@ +export default ` +<div class="sdc-input "> + <label class="sdc-input__label" *ngIf="label" [ngClass]="{'required':required}">{{label}}</label> + <input + class="sdc-input__input {{classNames}}" + [ngClass]="{'error': !valid, 'disabled':disabled}" + [attr.name]="name ? name : null" + [placeholder]="placeHolder" + [(ngModel)]="value" + [maxlength]="maxLength" + [minlength]="minLength" + [type]="type" + [formControl]="control" + [attr.disabled]="disabled ? 'disabled' : null" + (input)="onKeyPress($event.target.value)" + [attr.data-tests-id]="testId" + /> +</div> +`; diff --git a/src/angular/form-elements/input/input.component.ts b/src/angular/form-elements/input/input.component.ts new file mode 100644 index 0000000..af0e9f4 --- /dev/null +++ b/src/angular/form-elements/input/input.component.ts @@ -0,0 +1,54 @@ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { FormControl } from "@angular/forms"; +import { ValidationComponent } from '../validation/validation.component'; +import { ValidatableComponent } from './../validation/validatable.component'; +import 'rxjs/add/operator/debounceTime'; +import template from "./input.component.html"; + +@Component({ + selector: 'sdc-input', + template: template, +}) +export class InputComponent extends ValidatableComponent implements OnInit { + + @Output('valueChange') public baseEmitter: EventEmitter<any> = new EventEmitter<any>(); + @Input() public label: string; + @Input() public value: any; + @Input() public name: string; + @Input() public classNames: string; + @Input() public disabled: boolean; + @Input() public type: string; + @Input() public placeHolder: string; + @Input() public required: boolean; + @Input() public minLength: number; + @Input() public maxLength: number; + @Input() public debounceTime: number; + @Input() public testId: string; + + protected control: FormControl; + + constructor() { + super(); + this.control = new FormControl('', []); + this.debounceTime = 0; + this.placeHolder = ''; + this.type = 'text'; + } + + ngOnInit() { + this.control.valueChanges. + debounceTime(this.debounceTime) + .subscribe((newValue: any) => { + this.baseEmitter.emit(this.value); + }); + } + + public getValue(): any { + return this.value; + } + + onKeyPress(value: string) { + this.valueChanged(this.value); + } + +} diff --git a/src/angular/form-elements/radios/radio-button.model.ts b/src/angular/form-elements/radios/radio-button.model.ts new file mode 100644 index 0000000..1ad4b3f --- /dev/null +++ b/src/angular/form-elements/radios/radio-button.model.ts @@ -0,0 +1,15 @@ +export interface IRadioButtonModel { + label: string; + disabled: boolean; + name: string; + value: string; +}; + +export interface IOptionGroup { + items: IRadioButtonModel[]; +}; + +export enum Direction { + vertical, + horizontal +} diff --git a/src/angular/form-elements/radios/radio-buttons-group.component.html.ts b/src/angular/form-elements/radios/radio-buttons-group.component.html.ts new file mode 100644 index 0000000..28a27af --- /dev/null +++ b/src/angular/form-elements/radios/radio-buttons-group.component.html.ts @@ -0,0 +1,20 @@ +export default ` +<label class='sdc-radio-group__legend'>{{legend}}</label> +<div class='sdc-radio-group__radios {{direction}}'> + <template *ngFor="let item of options.items"> + <div class="sdc-radio"> + <label class="sdc-radio__animation-wrapper" SdcRippleClickAnimation [rippleClickDisabled]="disabled"> + <input class="sdc-radio__input" + type="radio" + name="{{item.name}}" + value="{{item.value}}" + disabled="{{disabled || item.disabled || false}}" + (change)="onValueChanged(item.value)" + [(ngModel)]="value" + /> + <span class="sdc-radio__label">{{ item.label }}</span> + </label> + </div> + </template> +</div> +`; diff --git a/src/angular/form-elements/radios/radio-buttons-group.component.spec.ts b/src/angular/form-elements/radios/radio-buttons-group.component.spec.ts new file mode 100644 index 0000000..273a701 --- /dev/null +++ b/src/angular/form-elements/radios/radio-buttons-group.component.spec.ts @@ -0,0 +1,52 @@ +import { TestBed, async } from '@angular/core/testing'; +import { RadioGroupComponent } from "./radio-buttons-group.component"; +import { FormsModule } from "@angular/forms"; +import { IRadioButtonModel } from "./radio-button.model"; +import { AnimationDirectivesModule } from "../../animations/animation-directives.module"; + + +describe("Radio Buttons unit-tests", ()=>{ + let component: RadioGroupComponent; + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ + RadioGroupComponent + ], + imports:[ + FormsModule, + AnimationDirectivesModule + ] + }).compileComponents(); + + const fixture = TestBed.createComponent(RadioGroupComponent); + component = fixture.componentInstance; + component.disabled = false;//TODO constructor + component.options = { + items: [] + }; + })); + + it('Component Created', async(()=> { + expect(component).toBeDefined(); + })); + + it('Not possible to choose value which not exists', async(() =>{ + component.value = 'test'; + expect(component.value).not.toEqual('test'); + })); + + it('Normal flow', async(() =>{ + component.options.items = [ <IRadioButtonModel> { + value: 'val1', + name: 'exp6', + label: 'Label of Radio1' + }, <IRadioButtonModel> { + value: 'val2', + name: 'exp6', + label: 'Label of Radio2' + }]; + component.value = component.options.items[0].value; + expect(component.value).toEqual(component.options.items[0].value); + })); + +}); diff --git a/src/angular/form-elements/radios/radio-buttons-group.component.ts b/src/angular/form-elements/radios/radio-buttons-group.component.ts new file mode 100644 index 0000000..800d8b0 --- /dev/null +++ b/src/angular/form-elements/radios/radio-buttons-group.component.ts @@ -0,0 +1,52 @@ +import { Component, Input, Output, ViewEncapsulation, EventEmitter, HostBinding } from "@angular/core"; +import { Direction, IOptionGroup, IRadioButtonModel } from "./radio-button.model"; +import template from './radio-buttons-group.component.html'; + +@Component({ + selector: 'sdc-radio-group', + template: template, + encapsulation: ViewEncapsulation.None +}) +export class RadioGroupComponent { + + private _direction: Direction = Direction.vertical; + private _selectedValue: string; + + @HostBinding('class') classes = 'sdc-radio-group'; + + @Input() public legend: string; + @Input() public options: IOptionGroup; + @Input() public disabled: boolean; + + @Input() + get value(): string { + return this._selectedValue; + } + set value(value: string) { + if (this.isOptionExists(value)) { + this._selectedValue = value; + } + } + + @Output() public valueChange: EventEmitter<string> = new EventEmitter<string>(); + + @Input() + get direction(): string { + return Direction[this._direction]; + } + set direction(direction: string) { + this._direction = (direction === 'horizontal' ? Direction.horizontal : Direction.vertical); + } + + public onValueChanged(value): void { + this.valueChange.emit(value); + } + + private isOptionExists(value) { + const exist = this.options.items.find((item: IRadioButtonModel) => { + return item.value === value; + }); + return exist !== undefined; + } + +} diff --git a/src/angular/form-elements/validation/validatable.component.ts b/src/angular/form-elements/validation/validatable.component.ts new file mode 100644 index 0000000..4817dea --- /dev/null +++ b/src/angular/form-elements/validation/validatable.component.ts @@ -0,0 +1,25 @@ +import { Input, Component } from "@angular/core"; +import { ValidationComponent } from './validation.component'; +import { Subject } from 'rxjs/Subject'; +import { IValidatableComponent } from './validatable.interface'; + +export abstract class ValidatableComponent implements IValidatableComponent { + + // Each ValidatableComponent should handle the style in case of error, according to this boolean + public valid = true; + + // Each ValidatableComponent will notify when the value is changed. + public notifier: Subject<string>; + + constructor() { + this.notifier = new Subject(); + } + + public abstract getValue(): any; + + // Each ValidatableComponent should call the valueChanged on value changed function. + protected valueChanged = (value: any): void => { + this.notifier.next(value); + } + +} diff --git a/src/angular/form-elements/validation/validatable.interface.ts b/src/angular/form-elements/validation/validatable.interface.ts new file mode 100644 index 0000000..6aceafe --- /dev/null +++ b/src/angular/form-elements/validation/validatable.interface.ts @@ -0,0 +1,5 @@ +export interface IValidatableComponent { + + getValue(): any; + +} diff --git a/src/angular/form-elements/validation/validation-group.component.html.ts b/src/angular/form-elements/validation/validation-group.component.html.ts new file mode 100644 index 0000000..dff591e --- /dev/null +++ b/src/angular/form-elements/validation/validation-group.component.html.ts @@ -0,0 +1,3 @@ +export default ` +<ng-content></ng-content> +`; diff --git a/src/angular/form-elements/validation/validation-group.component.ts b/src/angular/form-elements/validation/validation-group.component.ts new file mode 100644 index 0000000..59ecf4c --- /dev/null +++ b/src/angular/form-elements/validation/validation-group.component.ts @@ -0,0 +1,47 @@ +import { Input, Component, ContentChildren, EventEmitter, Output, QueryList, SimpleChanges, HostBinding, AfterContentInit } from "@angular/core"; +import { AbstractControl, FormControl } from "@angular/forms"; +import { Subscribable } from "rxjs/Observable"; +import { AnonymousSubscription } from "rxjs/Subscription"; +import { IValidator } from './validators/validator.interface'; +import { ValidatorComponent } from './validators/base.validator.component'; +import { RegexValidatorComponent } from './validators/regex.validator.component'; +import { RequiredValidatorComponent } from './validators/required.validator.component'; +import { ValidatableComponent } from './validatable.component'; +import { ValidationComponent } from './validation.component'; +import { CustomValidatorComponent } from './validators/custom.validator.component'; +import template from "./validation.component.html"; + +@Component({ + selector: 'sdc-validation-group', + template +}) +export class ValidationGroupComponent implements AfterContentInit { + + @Input() public disabled: boolean; + @HostBinding('class') classes; + + @ContentChildren(ValidationComponent) public validationsComponents: QueryList<ValidationComponent>; + + private supportedValidator: Array<QueryList<ValidatorComponent>>; + + constructor() { + this.disabled = false; + this.classes = 'sdc-validation-group'; + } + + ngAfterContentInit(): void { + + } + + public validate(): boolean { + let validationResult = true; + // Iterate over all validationComponent inside the group and return boolean result true in case all validations passed. + this.validationsComponents.forEach((validationComponent) => { + if (validationComponent.validate()) { + validationResult = false; + } + }); + return validationResult; + } + +} diff --git a/src/angular/form-elements/validation/validation.component.html.ts b/src/angular/form-elements/validation/validation.component.html.ts new file mode 100644 index 0000000..0f11a23 --- /dev/null +++ b/src/angular/form-elements/validation/validation.component.html.ts @@ -0,0 +1,3 @@ +export default ` +<ng-content *ngIf="!disabled"></ng-content> +`; diff --git a/src/angular/form-elements/validation/validation.component.ts b/src/angular/form-elements/validation/validation.component.ts new file mode 100644 index 0000000..4abdd12 --- /dev/null +++ b/src/angular/form-elements/validation/validation.component.ts @@ -0,0 +1,79 @@ +import { Input, Component, ContentChildren, EventEmitter, Output, QueryList, SimpleChanges, HostBinding, AfterContentInit } from "@angular/core"; +import { AbstractControl, FormControl } from "@angular/forms"; +import { Subscribable } from "rxjs/Observable"; +import { AnonymousSubscription } from "rxjs/Subscription"; +import { IValidator } from './validators/validator.interface'; +import { ValidatorComponent } from './validators/base.validator.component'; +import { RegexValidatorComponent } from './validators/regex.validator.component'; +import { RequiredValidatorComponent } from './validators/required.validator.component'; +import { ValidatableComponent } from './validatable.component'; +import { CustomValidatorComponent } from './validators/custom.validator.component'; +import template from "./validation.component.html"; + +@Component({ + selector: 'sdc-validation', + template +}) +export class ValidationComponent implements AfterContentInit { + + @Input() public validateElement: ValidatableComponent; + @Input() public disabled: boolean; + @Output() public validityChanged: EventEmitter<boolean> = new EventEmitter<boolean>(); + @HostBinding('class') classes; + + // @ContentChildren does not recieve type any or IValidator or ValidatorComponent, so need to create @ContentChildren for each validator type. + @ContentChildren(RegexValidatorComponent) public regexValidator: QueryList<ValidatorComponent>; + @ContentChildren(RequiredValidatorComponent) public requireValidator: QueryList<ValidatorComponent>; + @ContentChildren(CustomValidatorComponent) public customValidator: QueryList<ValidatorComponent>; + + private supportedValidator: Array<QueryList<ValidatorComponent>>; + + constructor() { + this.disabled = false; + this.classes = 'sdc-validation'; + } + + ngAfterContentInit(): void { + this.supportedValidator = [ + this.regexValidator, + this.requireValidator, + this.customValidator + ]; + + this.validateElement.notifier.subscribe( + (value) => { + const validationResult = this.validateOnChange(value); + this.validateElement.valid = validationResult; + }, + (error) => console.log('Validation subscribe error') + ); + } + + public validate = (): boolean => { + const value = this.validateElement.getValue(); + return this.validateOnChange(value); + } + + private validateOnChange(value: any): boolean { + if (this.disabled) { return true; } + + /** + * Iterate over all validators types (required, regex, etc...), and inside each iterate over + * all validators with same type, and return boolean result true in case all validations passed. + */ + const validationResult: boolean = this.supportedValidator.reduce((sum, validatorName) => { + const response: boolean = validatorName.reduce((_sum, validator) => { + return _sum && validator.validate(value); + }, true); + return sum && response; + }, true); + + if (this.validateElement.valid !== validationResult) { + this.validityChanged.emit(validationResult); + } + + return validationResult; + + } + +} diff --git a/src/angular/form-elements/validation/validation.module.ts b/src/angular/form-elements/validation/validation.module.ts new file mode 100644 index 0000000..4213f76 --- /dev/null +++ b/src/angular/form-elements/validation/validation.module.ts @@ -0,0 +1,35 @@ +import { NgModule } from "@angular/core"; +import { FormsModule, ReactiveFormsModule } from "@angular/forms"; +import { CommonModule } from "@angular/common"; +import { SvgIconModule } from './../../svg-icon/svg-icon.module'; +import { ValidationComponent } from './validation.component'; +import { ValidatorComponent } from './validators/base.validator.component'; +import { RequiredValidatorComponent } from './validators/required.validator.component'; +import { RegexValidatorComponent } from './validators/regex.validator.component'; +import { CustomValidatorComponent } from './validators/custom.validator.component'; +import { ValidationGroupComponent } from './validation-group.component'; + +@NgModule({ + imports: [ + FormsModule, + CommonModule, + ReactiveFormsModule, + SvgIconModule + ], + declarations: [ + ValidationComponent, + RegexValidatorComponent, + RequiredValidatorComponent, + CustomValidatorComponent, + ValidationGroupComponent + ], + exports: [ + ValidationComponent, + RegexValidatorComponent, + RequiredValidatorComponent, + CustomValidatorComponent, + ValidationGroupComponent + ] +}) +export class ValidationModule { +} diff --git a/src/angular/form-elements/validation/validators/base.validator.component.html.ts b/src/angular/form-elements/validation/validators/base.validator.component.html.ts new file mode 100644 index 0000000..aba8eed --- /dev/null +++ b/src/angular/form-elements/validation/validators/base.validator.component.html.ts @@ -0,0 +1,10 @@ +export default ` +<svg-icon-label + *ngIf="!isValid" + name="alert-triangle" + mode="error" + size="small" + [label]="message" + labelPlacement="right"> +</svg-icon-label> +`; diff --git a/src/angular/form-elements/validation/validators/base.validator.component.ts b/src/angular/form-elements/validation/validators/base.validator.component.ts new file mode 100644 index 0000000..3d751af --- /dev/null +++ b/src/angular/form-elements/validation/validators/base.validator.component.ts @@ -0,0 +1,25 @@ +import { Input, Component, ContentChildren, EventEmitter, Output, QueryList, SimpleChanges, HostBinding } from "@angular/core"; +import { IValidator } from './validator.interface'; +import template from "./base.validator.component.html"; + +@Component({ + selector: 'sdc-validator', + template: template +}) +export abstract class ValidatorComponent { + + @Input() public message: any; + @Input() public disabled: boolean; + @HostBinding('class') classes; + + protected isValid: boolean; + + constructor() { + this.disabled = false; + this.isValid = true; + this.classes = 'sdc-validator sdc-label__error'; + } + + public abstract validate(value: any): boolean; + +} diff --git a/src/angular/form-elements/validation/validators/custom.validator.component.ts b/src/angular/form-elements/validation/validators/custom.validator.component.ts new file mode 100644 index 0000000..eb09636 --- /dev/null +++ b/src/angular/form-elements/validation/validators/custom.validator.component.ts @@ -0,0 +1,23 @@ +import { Input, Component } from "@angular/core"; +import { ValidatorComponent } from "./base.validator.component"; +import { IValidator } from './validator.interface'; +import template from "./base.validator.component.html"; + +@Component({ + selector: 'sdc-custom-validator', + template: template +}) +export class CustomValidatorComponent extends ValidatorComponent implements IValidator { + + @Input() public callback: (...args) => boolean; + + constructor() { + super(); + } + + public validate(value: any): boolean { + this.isValid = this.callback(value); + return this.isValid; + } + +} diff --git a/src/angular/form-elements/validation/validators/regex.validator.component.ts b/src/angular/form-elements/validation/validators/regex.validator.component.ts new file mode 100644 index 0000000..5929016 --- /dev/null +++ b/src/angular/form-elements/validation/validators/regex.validator.component.ts @@ -0,0 +1,24 @@ +import { Input, Component } from "@angular/core"; +import { ValidatorComponent } from "./base.validator.component"; +import { IValidator } from './validator.interface'; +import template from "./base.validator.component.html"; + +@Component({ + selector: 'sdc-regex-validator', + template: template +}) +export class RegexValidatorComponent extends ValidatorComponent implements IValidator { + + @Input() public pattern: RegExp; + + constructor() { + super(); + } + + public validate(value: any): boolean { + const regexp = new RegExp(this.pattern); + this.isValid = regexp.test(value); + return this.isValid; + } + +} diff --git a/src/angular/form-elements/validation/validators/required.validator.component.ts b/src/angular/form-elements/validation/validators/required.validator.component.ts new file mode 100644 index 0000000..7eee932 --- /dev/null +++ b/src/angular/form-elements/validation/validators/required.validator.component.ts @@ -0,0 +1,25 @@ +import { Input, Component } from "@angular/core"; +import { ValidatorComponent } from "./base.validator.component"; +import { IValidator } from './validator.interface'; +import template from "./base.validator.component.html"; + +@Component({ + selector: 'sdc-required-validator', + template: template +}) +export class RequiredValidatorComponent extends ValidatorComponent implements IValidator { + + constructor() { + super(); + } + + public validate(value: any): boolean { + if (value) { + this.isValid = true; + } else { + this.isValid = false; + } + return this.isValid; + } + +} diff --git a/src/angular/form-elements/validation/validators/validator.interface.ts b/src/angular/form-elements/validation/validators/validator.interface.ts new file mode 100644 index 0000000..c0adc24 --- /dev/null +++ b/src/angular/form-elements/validation/validators/validator.interface.ts @@ -0,0 +1,3 @@ +export interface IValidator { + validate(value: any): void; +} |