From 9b6bc17ef38f843e7d7697615399255469cf9483 Mon Sep 17 00:00:00 2001 From: Bozhidar Dryanovski Date: Mon, 15 Jun 2020 17:19:25 +0300 Subject: [PATCH] feat(form): adding clr-control-success as part of Clarity containers - create component to represent clr-control-success inside containers - add success icon inside containers - combine if-success and if-error service into one if-control-state - if-success and if-error components now extend single abstract classs having the base logic inside. - update all containers to provide clr-control-success - provide demo page Signed-off-by: Bozhidar Dryanovski --- packages/angular/golden/clr-angular.d.ts | 59 +++++--- .../forms/checkbox/checkbox-container.spec.ts | 6 +- .../src/forms/checkbox/checkbox-container.ts | 32 +++-- .../forms/checkbox/toggle-container.spec.ts | 6 +- .../src/forms/checkbox/toggle-wrapper.spec.ts | 4 +- .../src/forms/common/abstract-container.ts | 29 ++-- .../clr-angular/src/forms/common/all.spec.ts | 8 +- .../src/forms/common/common.module.ts | 15 +- .../forms/common/control-container.spec.ts | 2 + .../src/forms/common/control-container.ts | 29 +++- .../src/forms/common/error.spec.ts | 25 ++-- .../if-control-state/abstract-if-state.ts | 47 +++++++ .../if-control-state.service.spec.ts | 128 ++++++++++++++++++ .../if-control-state.service.ts | 60 ++++++++ .../if-error.spec.ts | 22 +-- .../forms/common/if-control-state/if-error.ts | 44 ++++++ .../if-control-state/if-success.spec.ts | 108 +++++++++++++++ .../common/if-control-state/if-success.ts | 39 ++++++ .../common/if-error/if-error.service.spec.ts | 71 ---------- .../forms/common/if-error/if-error.service.ts | 65 --------- .../src/forms/common/if-error/if-error.ts | 62 --------- .../clr-angular/src/forms/common/index.ts | 4 +- .../providers/control-class.service.spec.ts | 19 ++- .../common/providers/control-class.service.ts | 15 +- .../src/forms/common/success.spec.ts | 42 ++++++ .../clr-angular/src/forms/common/success.ts | 20 +++ .../src/forms/common/wrapped-control.spec.ts | 12 +- .../src/forms/common/wrapped-control.ts | 38 ++++-- .../forms/datalist/datalist-container.spec.ts | 2 + .../src/forms/datalist/datalist-container.ts | 33 +++-- .../src/forms/datalist/datalist-input.spec.ts | 4 +- .../forms/datepicker/date-container.spec.ts | 8 +- .../src/forms/datepicker/date-container.ts | 46 +++++-- .../src/forms/datepicker/date-input.spec.ts | 12 +- .../src/forms/input/input-container.spec.ts | 2 + .../src/forms/input/input-container.ts | 29 +++- .../forms/password/password-container.spec.ts | 7 +- .../src/forms/password/password-container.ts | 33 +++-- .../src/forms/password/password.spec.ts | 6 +- .../src/forms/radio/radio-container.spec.ts | 6 +- .../src/forms/radio/radio-container.ts | 32 +++-- .../src/forms/range/range-container.spec.ts | 2 + .../src/forms/range/range-container.ts | 33 +++-- .../src/forms/select/select-container.spec.ts | 4 + .../src/forms/select/select-container.ts | 33 +++-- .../src/forms/styles/_containers.clarity.scss | 17 ++- .../src/forms/styles/_properties.forms.scss | 2 + .../src/forms/styles/_variables.forms.scss | 2 + .../src/forms/tests/container.spec.ts | 49 +++++-- .../src/forms/tests/control.spec.ts | 22 +-- .../src/forms/tests/wrapper.spec.ts | 10 +- .../forms/textarea/textarea-container.spec.ts | 2 + .../src/forms/textarea/textarea-container.ts | 29 +++- .../src/utils/_overwrites.clarity.scss | 2 + .../src/utils/_theme.dark.clarity.scss | 2 + .../dev/src/app/forms/forms.demo.module.ts | 3 + .../dev/src/app/forms/forms.demo.routing.ts | 2 + .../projects/dev/src/app/forms/forms.demo.ts | 1 + .../dev/src/app/forms/reset/reset.html | 3 +- .../src/app/forms/validation/validation.html | 121 +++++++++++++++++ .../src/app/forms/validation/validation.ts | 34 +++++ 61 files changed, 1177 insertions(+), 427 deletions(-) create mode 100644 packages/angular/projects/clr-angular/src/forms/common/if-control-state/abstract-if-state.ts create mode 100644 packages/angular/projects/clr-angular/src/forms/common/if-control-state/if-control-state.service.spec.ts create mode 100644 packages/angular/projects/clr-angular/src/forms/common/if-control-state/if-control-state.service.ts rename packages/angular/projects/clr-angular/src/forms/common/{if-error => if-control-state}/if-error.spec.ts (87%) create mode 100644 packages/angular/projects/clr-angular/src/forms/common/if-control-state/if-error.ts create mode 100644 packages/angular/projects/clr-angular/src/forms/common/if-control-state/if-success.spec.ts create mode 100644 packages/angular/projects/clr-angular/src/forms/common/if-control-state/if-success.ts delete mode 100644 packages/angular/projects/clr-angular/src/forms/common/if-error/if-error.service.spec.ts delete mode 100644 packages/angular/projects/clr-angular/src/forms/common/if-error/if-error.service.ts delete mode 100644 packages/angular/projects/clr-angular/src/forms/common/if-error/if-error.ts create mode 100644 packages/angular/projects/clr-angular/src/forms/common/success.spec.ts create mode 100644 packages/angular/projects/clr-angular/src/forms/common/success.ts create mode 100644 packages/angular/projects/dev/src/app/forms/validation/validation.html create mode 100644 packages/angular/projects/dev/src/app/forms/validation/validation.ts diff --git a/packages/angular/golden/clr-angular.d.ts b/packages/angular/golden/clr-angular.d.ts index ce18eab56e..04352102b7 100644 --- a/packages/angular/golden/clr-angular.d.ts +++ b/packages/angular/golden/clr-angular.d.ts @@ -49,13 +49,15 @@ export declare abstract class ClrAbstractContainer implements DynamicWrapper, On _dynamic: boolean; control: NgControl; protected controlClassService: ControlClassService; - protected ifErrorService: IfErrorService; - invalid: boolean; + protected ifControlStateService: IfControlStateService; label: ClrLabel; protected layoutService: LayoutService; protected ngControlService: NgControlService; + get showHelper(): boolean; + get showInvalid(): boolean; + get showValid(): boolean; protected subscriptions: Subscription[]; - constructor(ifErrorService: IfErrorService, layoutService: LayoutService, controlClassService: ControlClassService, ngControlService: NgControlService); + constructor(ifControlStateService: IfControlStateService, layoutService: LayoutService, controlClassService: ControlClassService, ngControlService: NgControlService); addGrid(): boolean; controlClass(): string; ngOnDestroy(): void; @@ -261,10 +263,11 @@ export declare class ClrCheckboxContainer extends ClrAbstractContainer { set clrInline(value: boolean | string); get clrInline(): boolean | string; protected controlClassService: ControlClassService; - protected ifErrorService: IfErrorService; + controlSuccessComponent: ClrControlSuccess; + protected ifControlStateService: IfControlStateService; protected layoutService: LayoutService; protected ngControlService: NgControlService; - constructor(ifErrorService: IfErrorService, layoutService: LayoutService, controlClassService: ControlClassService, ngControlService: NgControlService); + constructor(layoutService: LayoutService, controlClassService: ControlClassService, ngControlService: NgControlService, ifControlStateService: IfControlStateService); } export declare class ClrCheckboxModule { @@ -367,6 +370,7 @@ export declare class ClrControl extends WrappedFormControl } export declare class ClrControlContainer extends ClrAbstractContainer { + controlSuccessComponent: ClrControlSuccess; } export declare class ClrControlError { @@ -379,6 +383,11 @@ export declare class ClrControlHelper { constructor(controlIdService: ControlIdService); } +export declare class ClrControlSuccess { + controlIdService: ControlIdService; + constructor(controlIdService: ControlIdService); +} + export declare class ClrDatagrid implements AfterContentInit, AfterViewInit, OnDestroy { SELECTION_TYPE: typeof SelectionType; _calculationRows: ViewContainerRef; @@ -734,8 +743,10 @@ export declare class ClrDatalist implements AfterContentInit { } export declare class ClrDatalistContainer extends ClrAbstractContainer { + controlSuccessComponent: ClrControlSuccess; focus: boolean; - constructor(controlClassService: ControlClassService, layoutService: LayoutService, ifErrorService: IfErrorService, ngControlService: NgControlService, focusService: FocusService); + protected ifControlStateService: IfControlStateService; + constructor(controlClassService: ControlClassService, layoutService: LayoutService, ngControlService: NgControlService, focusService: FocusService, ifControlStateService: IfControlStateService); } export declare class ClrDatalistInput extends WrappedFormControl implements AfterContentInit { @@ -758,6 +769,7 @@ export declare class ClrDateContainer implements DynamicWrapper, OnDestroy, Afte set clrPosition(position: string); commonStrings: ClrCommonStringsService; control: NgControl; + controlSuccessComponent: ClrControlSuccess; focus: boolean; invalid: boolean; get isEnabled(): boolean; @@ -765,7 +777,10 @@ export declare class ClrDateContainer implements DynamicWrapper, OnDestroy, Afte label: ClrLabel; get open(): boolean; get popoverPosition(): ClrPopoverPosition; - constructor(toggleService: ClrPopoverToggleService, dateNavigationService: DateNavigationService, datepickerEnabledService: DatepickerEnabledService, dateFormControlService: DateFormControlService, commonStrings: ClrCommonStringsService, ifErrorService: IfErrorService, focusService: FocusService, viewManagerService: ViewManagerService, controlClassService: ControlClassService, layoutService: LayoutService, ngControlService: NgControlService); + showHelper: boolean; + state: CONTROL_STATE; + valid: boolean; + constructor(toggleService: ClrPopoverToggleService, dateNavigationService: DateNavigationService, datepickerEnabledService: DatepickerEnabledService, dateFormControlService: DateFormControlService, commonStrings: ClrCommonStringsService, focusService: FocusService, viewManagerService: ViewManagerService, controlClassService: ControlClassService, layoutService: LayoutService, ngControlService: NgControlService, ifControlStateService: IfControlStateService); addGrid(): boolean; controlClass(): string; ngAfterViewInit(): void; @@ -1015,10 +1030,10 @@ export declare class ClrIfDragged implements OnDestroy { ngOnDestroy(): void; } -export declare class ClrIfError { +export declare class ClrIfError extends AbstractIfState { error: string; - constructor(ifErrorService: IfErrorService, ngControlService: NgControlService, template: TemplateRef, container: ViewContainerRef); - ngOnDestroy(): void; + constructor(ifControlStateService: IfControlStateService, ngControlService: NgControlService, template: TemplateRef, container: ViewContainerRef); + protected handleState(state: CONTROL_STATE): void; } export declare class ClrIfExpanded implements OnInit, OnDestroy { @@ -1040,12 +1055,18 @@ export declare class ClrIfOpen implements OnDestroy { static ngAcceptInputType_open: boolean | ''; } +export declare class ClrIfSuccess extends AbstractIfState { + constructor(ifControlStateService: IfControlStateService, ngControlService: NgControlService, template: TemplateRef, container: ViewContainerRef); + protected handleState(state: CONTROL_STATE): void; +} + export declare class ClrInput extends WrappedFormControl { protected index: number; constructor(vcr: ViewContainerRef, injector: Injector, control: NgControl, renderer: Renderer2, el: ElementRef); } export declare class ClrInputContainer extends ClrAbstractContainer { + controlSuccessComponent: ClrControlSuccess; } export declare class ClrInputModule { @@ -1181,10 +1202,11 @@ export declare class ClrPasswordContainer extends ClrAbstractContainer { set clrToggle(state: boolean); get clrToggle(): boolean; commonStrings: ClrCommonStringsService; + controlSuccessComponent: ClrControlSuccess; focus: boolean; focusService: FocusService; show: boolean; - constructor(ifErrorService: IfErrorService, layoutService: LayoutService, controlClassService: ControlClassService, ngControlService: NgControlService, focusService: FocusService, toggleService: BehaviorSubject, commonStrings: ClrCommonStringsService); + constructor(ifControlStateService: IfControlStateService, layoutService: LayoutService, controlClassService: ControlClassService, ngControlService: NgControlService, focusService: FocusService, toggleService: BehaviorSubject, commonStrings: ClrCommonStringsService); toggle(): void; } @@ -1297,10 +1319,11 @@ export declare class ClrRadioContainer extends ClrAbstractContainer { set clrInline(value: boolean | string); get clrInline(): boolean | string; protected controlClassService: ControlClassService; - protected ifErrorService: IfErrorService; + controlSuccessComponent: ClrControlSuccess; + protected ifControlStateService: IfControlStateService; protected layoutService: LayoutService; protected ngControlService: NgControlService; - constructor(ifErrorService: IfErrorService, layoutService: LayoutService, controlClassService: ControlClassService, ngControlService: NgControlService); + constructor(layoutService: LayoutService, controlClassService: ControlClassService, ngControlService: NgControlService, ifControlStateService: IfControlStateService); } export declare class ClrRadioModule { @@ -1317,9 +1340,11 @@ export declare class ClrRange extends WrappedFormControl { } export declare class ClrRangeContainer extends ClrAbstractContainer { + controlSuccessComponent: ClrControlSuccess; set hasProgress(val: boolean); get hasProgress(): boolean; - constructor(ifErrorService: IfErrorService, layoutService: LayoutService, controlClassService: ControlClassService, ngControlService: NgControlService, renderer: Renderer2, idService: ControlIdService); + protected ifControlStateService: IfControlStateService; + constructor(layoutService: LayoutService, controlClassService: ControlClassService, ngControlService: NgControlService, renderer: Renderer2, idService: ControlIdService, ifControlStateService: IfControlStateService); getRangeProgressFillWidth(): string; } @@ -1346,11 +1371,12 @@ export declare class ClrSelect extends WrappedFormControl { export declare class ClrSelectContainer extends ClrAbstractContainer { protected controlClassService: ControlClassService; - protected ifErrorService: IfErrorService; + controlSuccessComponent: ClrControlSuccess; + protected ifControlStateService: IfControlStateService; protected layoutService: LayoutService; multiple: SelectMultipleControlValueAccessor; protected ngControlService: NgControlService; - constructor(ifErrorService: IfErrorService, layoutService: LayoutService, controlClassService: ControlClassService, ngControlService: NgControlService); + constructor(layoutService: LayoutService, controlClassService: ControlClassService, ngControlService: NgControlService, ifControlStateService: IfControlStateService); ngOnInit(): void; wrapperClass(): "clr-multiselect-wrapper" | "clr-select-wrapper"; } @@ -1595,6 +1621,7 @@ export declare class ClrTextarea extends WrappedFormControl `, @@ -47,6 +47,7 @@ class NoLabelTest {} There was an error Helper text + Valid `, }) @@ -70,6 +71,7 @@ class TemplateDrivenTest { There was an error Helper text + Valid `, }) @@ -102,7 +104,7 @@ export default function (): void { TestBed.configureTestingModule({ imports: [ClrIconModule, ClrCommonFormsModule, FormsModule], declarations: [ClrCheckboxContainer, ClrCheckboxWrapper, ClrCheckbox, TemplateDrivenTest], - providers: [NgControl, NgControlService, IfErrorService, LayoutService], + providers: [IfControlStateService, NgControl, NgControlService, LayoutService], }); fixture = TestBed.createComponent(TemplateDrivenTest); diff --git a/packages/angular/projects/clr-angular/src/forms/checkbox/checkbox-container.ts b/packages/angular/projects/clr-angular/src/forms/checkbox/checkbox-container.ts index cff5a159ce..394a046788 100644 --- a/packages/angular/projects/clr-angular/src/forms/checkbox/checkbox-container.ts +++ b/packages/angular/projects/clr-angular/src/forms/checkbox/checkbox-container.ts @@ -4,13 +4,14 @@ * The full license information can be found in LICENSE in the root directory of this project. */ -import { Component, Input, Optional } from '@angular/core'; +import { Component, Input, Optional, ContentChild } from '@angular/core'; -import { IfErrorService } from '../common/if-error/if-error.service'; import { ControlClassService } from '../common/providers/control-class.service'; import { NgControlService } from '../common/providers/ng-control.service'; import { ClrAbstractContainer } from '../common/abstract-container'; import { LayoutService } from '../common/providers/layout.service'; +import { ClrControlSuccess } from '../common/success'; +import { IfControlStateService } from '../common/if-control-state/if-control-state.service'; @Component({ selector: 'clr-checkbox-container,clr-toggle-container', @@ -20,9 +21,21 @@ import { LayoutService } from '../common/providers/layout.service';
- - - + + + + +
`, @@ -31,18 +44,19 @@ import { LayoutService } from '../common/providers/layout.service'; '[class.clr-form-control-disabled]': 'control?.disabled', '[class.clr-row]': 'addGrid()', }, - providers: [NgControlService, ControlClassService, IfErrorService], + providers: [IfControlStateService, NgControlService, ControlClassService], }) export class ClrCheckboxContainer extends ClrAbstractContainer { private inline = false; + @ContentChild(ClrControlSuccess) controlSuccessComponent: ClrControlSuccess; constructor( - protected ifErrorService: IfErrorService, @Optional() protected layoutService: LayoutService, protected controlClassService: ControlClassService, - protected ngControlService: NgControlService + protected ngControlService: NgControlService, + protected ifControlStateService: IfControlStateService ) { - super(ifErrorService, layoutService, controlClassService, ngControlService); + super(ifControlStateService, layoutService, controlClassService, ngControlService); } /* diff --git a/packages/angular/projects/clr-angular/src/forms/checkbox/toggle-container.spec.ts b/packages/angular/projects/clr-angular/src/forms/checkbox/toggle-container.spec.ts index 794a677b02..1233d58298 100644 --- a/packages/angular/projects/clr-angular/src/forms/checkbox/toggle-container.spec.ts +++ b/packages/angular/projects/clr-angular/src/forms/checkbox/toggle-container.spec.ts @@ -10,9 +10,9 @@ import { By } from '@angular/platform-browser'; import { ClrIconModule } from '../../icon/icon.module'; import { ClrCommonFormsModule } from '../common/common.module'; -import { IfErrorService } from '../common/if-error/if-error.service'; import { NgControlService } from '../common/providers/ng-control.service'; import { LayoutService } from '../common/providers/layout.service'; +import { IfControlStateService } from '../common/if-control-state/if-control-state.service'; import { ClrCheckbox } from './checkbox'; import { ClrCheckboxContainer } from './checkbox-container'; @@ -39,6 +39,7 @@ class NoLabelTest {} There was an error Helper text + Valid `, }) @@ -62,6 +63,7 @@ class TemplateDrivenTest { There was an error Helper text + Valid `, }) @@ -94,7 +96,7 @@ export default function (): void { TestBed.configureTestingModule({ imports: [ClrIconModule, ClrCommonFormsModule, FormsModule], declarations: [ClrCheckboxContainer, ClrCheckboxWrapper, ClrCheckbox, TemplateDrivenTest], - providers: [NgControl, NgControlService, IfErrorService, LayoutService], + providers: [IfControlStateService, NgControl, NgControlService, LayoutService], }); fixture = TestBed.createComponent(TemplateDrivenTest); diff --git a/packages/angular/projects/clr-angular/src/forms/checkbox/toggle-wrapper.spec.ts b/packages/angular/projects/clr-angular/src/forms/checkbox/toggle-wrapper.spec.ts index 0b1adc2ee8..cd0589abf2 100644 --- a/packages/angular/projects/clr-angular/src/forms/checkbox/toggle-wrapper.spec.ts +++ b/packages/angular/projects/clr-angular/src/forms/checkbox/toggle-wrapper.spec.ts @@ -10,7 +10,6 @@ import { By } from '@angular/platform-browser'; import { ClrIconModule } from '../../icon/icon.module'; import { ClrCommonFormsModule } from '../common/common.module'; -import { IfErrorService } from '../common/if-error/if-error.service'; import { NgControlService } from '../common/providers/ng-control.service'; import { LayoutService } from '../common/providers/layout.service'; @@ -18,6 +17,7 @@ import { WrapperFullSpec, WrapperNoLabelSpec, WrapperContainerSpec } from '../te import { ClrCheckbox } from './checkbox'; import { ClrCheckboxWrapper } from './checkbox-wrapper'; import { ClrCheckboxContainer } from './checkbox-container'; +import { IfControlStateService } from '../common/if-control-state/if-control-state.service'; @Component({ template: ` @@ -63,7 +63,7 @@ export default function (): void { TestBed.configureTestingModule({ imports: [ClrIconModule, ClrCommonFormsModule, FormsModule], declarations: [ClrCheckboxWrapper, ClrCheckbox, FullTest], - providers: [NgControl, NgControlService, IfErrorService, LayoutService], + providers: [IfControlStateService, NgControl, NgControlService, LayoutService], }); fixture = TestBed.createComponent(FullTest); diff --git a/packages/angular/projects/clr-angular/src/forms/common/abstract-container.ts b/packages/angular/projects/clr-angular/src/forms/common/abstract-container.ts index 67503d798d..7b7eb6a8d7 100644 --- a/packages/angular/projects/clr-angular/src/forms/common/abstract-container.ts +++ b/packages/angular/projects/clr-angular/src/forms/common/abstract-container.ts @@ -5,36 +5,49 @@ */ import { ContentChild, Directive, OnDestroy, Optional } from '@angular/core'; -import { Subscription } from 'rxjs'; import { NgControl } from '@angular/forms'; -import { IfErrorService } from './if-error/if-error.service'; import { NgControlService } from './providers/ng-control.service'; import { LayoutService } from './providers/layout.service'; import { DynamicWrapper } from '../../utils/host-wrapping/dynamic-wrapper'; import { ClrLabel } from './label'; import { ControlClassService } from './providers/control-class.service'; +import { Subscription } from 'rxjs'; +import { IfControlStateService, CONTROL_STATE } from './if-control-state/if-control-state.service'; @Directive() export abstract class ClrAbstractContainer implements DynamicWrapper, OnDestroy { protected subscriptions: Subscription[] = []; - invalid = false; _dynamic = false; @ContentChild(ClrLabel, { static: false }) label: ClrLabel; control: NgControl; + private state: CONTROL_STATE; + + get showHelper(): boolean { + return this.state === CONTROL_STATE.NONE; + } + + get showValid(): boolean { + return this.state === CONTROL_STATE.VALID; + } + + get showInvalid(): boolean { + return this.state === CONTROL_STATE.INVALID; + } constructor( - protected ifErrorService: IfErrorService, + protected ifControlStateService: IfControlStateService, @Optional() protected layoutService: LayoutService, protected controlClassService: ControlClassService, protected ngControlService: NgControlService ) { this.subscriptions.push( - this.ifErrorService.statusChanges.subscribe(invalid => { - this.invalid = invalid; + this.ifControlStateService.statusChanges.subscribe((state: CONTROL_STATE) => { + this.state = state; }) ); + this.subscriptions.push( this.ngControlService.controlChanges.subscribe(control => { this.control = control; @@ -43,7 +56,7 @@ export abstract class ClrAbstractContainer implements DynamicWrapper, OnDestroy } controlClass() { - return this.controlClassService.controlClass(this.invalid, this.addGrid()); + return this.controlClassService.controlClass(this.state, this.addGrid()); } addGrid() { @@ -51,6 +64,6 @@ export abstract class ClrAbstractContainer implements DynamicWrapper, OnDestroy } ngOnDestroy() { - this.subscriptions.map(sub => sub.unsubscribe()); + this.subscriptions.forEach(subscription => subscription.unsubscribe()); } } diff --git a/packages/angular/projects/clr-angular/src/forms/common/all.spec.ts b/packages/angular/projects/clr-angular/src/forms/common/all.spec.ts index 10780fed7d..3df549cfae 100644 --- a/packages/angular/projects/clr-angular/src/forms/common/all.spec.ts +++ b/packages/angular/projects/clr-angular/src/forms/common/all.spec.ts @@ -6,11 +6,11 @@ import CommonSpecs from './common.spec'; import ErrorSpecs from './error.spec'; +import SuccessSpec from './success.spec'; import HelperSpecs from './helper.spec'; import FormSpecs from './form.spec'; -import ControlStatusServiceSpecs from './if-error/if-error.service.spec'; import LayoutSpecs from './layout.spec'; -import IfErrorSpecs from './if-error/if-error.spec'; +import IfControlStateSpecs from './if-control-state/if-control-state.service.spec'; import LabelSpecs from './label.spec'; import ControlClassServiceSpecs from './providers/control-class.service.spec'; import ControlIdServiceSpecs from './providers/control-id.service.spec'; @@ -22,16 +22,16 @@ import ControlContainerSpecs from './control-container.spec'; describe('Forms common utilities', function () { ControlClassServiceSpecs(); ControlIdServiceSpecs(); - ControlStatusServiceSpecs(); ControlContainerSpecs(); NgControlServiceSpecs(); LayoutServiceSpecs(); LayoutSpecs(); FormSpecs(); LabelSpecs(); - IfErrorSpecs(); + IfControlStateSpecs(); WrappedControlSpecs(); CommonSpecs(); ErrorSpecs(); + SuccessSpec(); HelperSpecs(); }); diff --git a/packages/angular/projects/clr-angular/src/forms/common/common.module.ts b/packages/angular/projects/clr-angular/src/forms/common/common.module.ts index c98e0e1baa..69ff6016bb 100644 --- a/packages/angular/projects/clr-angular/src/forms/common/common.module.ts +++ b/packages/angular/projects/clr-angular/src/forms/common/common.module.ts @@ -7,23 +7,26 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { ClrIconModule } from '../../icon/icon.module'; - +import { ClrControl } from './control'; +import { ClrControlContainer } from './control-container'; import { ClrControlError } from './error'; +import { ClrForm } from './form'; import { ClrControlHelper } from './helper'; -import { ClrIfError } from './if-error/if-error'; +import { ClrIfError } from './if-control-state/if-error'; +import { ClrIfSuccess } from './if-control-state/if-success'; import { ClrLabel } from './label'; -import { ClrForm } from './form'; import { ClrLayout } from './layout'; -import { ClrControlContainer } from './control-container'; -import { ClrControl } from './control'; +import { ClrControlSuccess } from './success'; @NgModule({ imports: [CommonModule, ClrIconModule], declarations: [ ClrLabel, ClrControlError, + ClrControlSuccess, ClrControlHelper, ClrIfError, + ClrIfSuccess, ClrForm, ClrLayout, ClrControlContainer, @@ -32,8 +35,10 @@ import { ClrControl } from './control'; exports: [ ClrLabel, ClrControlError, + ClrControlSuccess, ClrControlHelper, ClrIfError, + ClrIfSuccess, ClrForm, ClrLayout, ClrControlContainer, diff --git a/packages/angular/projects/clr-angular/src/forms/common/control-container.spec.ts b/packages/angular/projects/clr-angular/src/forms/common/control-container.spec.ts index 216e801382..2574aca886 100644 --- a/packages/angular/projects/clr-angular/src/forms/common/control-container.spec.ts +++ b/packages/angular/projects/clr-angular/src/forms/common/control-container.spec.ts @@ -18,6 +18,7 @@ import { ContainerNoLabelSpec, ReactiveSpec, TemplateDrivenSpec } from '../tests Helper text Must be at least 5 characters + Valid `, }) @@ -42,6 +43,7 @@ class NoLabelTest { Helper text Must be at least 5 characters + Valid `, }) diff --git a/packages/angular/projects/clr-angular/src/forms/common/control-container.ts b/packages/angular/projects/clr-angular/src/forms/common/control-container.ts index e2c6bf0792..977be5ffd3 100644 --- a/packages/angular/projects/clr-angular/src/forms/common/control-container.ts +++ b/packages/angular/projects/clr-angular/src/forms/common/control-container.ts @@ -4,13 +4,14 @@ * The full license information can be found in LICENSE in the root directory of this project. */ -import { Component } from '@angular/core'; +import { Component, ContentChild } from '@angular/core'; import { ClrAbstractContainer } from '../common/abstract-container'; -import { IfErrorService } from './if-error/if-error.service'; import { NgControlService } from './providers/ng-control.service'; import { ControlIdService } from './providers/control-id.service'; import { ControlClassService } from './providers/control-class.service'; +import { IfControlStateService } from './if-control-state/if-control-state.service'; +import { ClrControlSuccess } from '../common/success'; @Component({ selector: 'clr-control-container', @@ -20,10 +21,22 @@ import { ControlClassService } from './providers/control-class.service';
- + +
- - + + +
`, host: { @@ -31,6 +44,8 @@ import { ControlClassService } from './providers/control-class.service'; '[class.clr-form-control-disabled]': 'control?.disabled', '[class.clr-row]': 'addGrid()', }, - providers: [IfErrorService, NgControlService, ControlIdService, ControlClassService], + providers: [IfControlStateService, NgControlService, ControlIdService, ControlClassService], }) -export class ClrControlContainer extends ClrAbstractContainer {} +export class ClrControlContainer extends ClrAbstractContainer { + @ContentChild(ClrControlSuccess) controlSuccessComponent: ClrControlSuccess; +} diff --git a/packages/angular/projects/clr-angular/src/forms/common/error.spec.ts b/packages/angular/projects/clr-angular/src/forms/common/error.spec.ts index a3abe4f976..a93583ead3 100644 --- a/packages/angular/projects/clr-angular/src/forms/common/error.spec.ts +++ b/packages/angular/projects/clr-angular/src/forms/common/error.spec.ts @@ -4,46 +4,39 @@ * The full license information can be found in LICENSE in the root directory of this project. */ import { Component } from '@angular/core'; -import { TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; - import { ClrControlError } from './error'; import { ControlIdService } from './providers/control-id.service'; @Component({ template: `Test error` }) class SimpleTest {} -@Component({ template: `` }) -class ExplicitAriaTest {} - export default function (): void { describe('ClrControlError', () => { - let fixture; + let fixture: ComponentFixture; + let element: HTMLElement; beforeEach(function () { TestBed.configureTestingModule({ - declarations: [ClrControlError, SimpleTest, ExplicitAriaTest], + declarations: [ClrControlError, SimpleTest], providers: [ControlIdService], }); fixture = TestBed.createComponent(SimpleTest); fixture.detectChanges(); + element = fixture.debugElement.query(By.directive(ClrControlError)).nativeElement; }); it('projects content', function () { - expect(fixture.debugElement.query(By.directive(ClrControlError)).nativeElement.innerText).toContain('Test error'); + expect(element.innerText).toContain('Test error'); }); it('adds the .clr-subtext class to host', function () { - expect( - fixture.debugElement.query(By.directive(ClrControlError)).nativeElement.classList.contains('clr-subtext') - ).toBeTrue(); + expect(element.classList.contains('clr-subtext')).toBeTrue(); }); - it('leaves the for aria-describedby untouched if it exists', function () { - const explicitFixture = TestBed.createComponent(ExplicitAriaTest); - explicitFixture.detectChanges(); - const message = explicitFixture.nativeElement.querySelector('clr-control-error'); - expect(message.getAttribute('aria-describedby')).toBe('hello'); + it('should add id to host', function () { + expect(element.getAttribute('id')).toContain('-error'); }); }); } diff --git a/packages/angular/projects/clr-angular/src/forms/common/if-control-state/abstract-if-state.ts b/packages/angular/projects/clr-angular/src/forms/common/if-control-state/abstract-if-state.ts new file mode 100644 index 0000000000..ecef0d15d8 --- /dev/null +++ b/packages/angular/projects/clr-angular/src/forms/common/if-control-state/abstract-if-state.ts @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2016-2020 VMware, Inc. All Rights Reserved. + * This software is released under MIT license. + * The full license information can be found in LICENSE in the root directory of this project. + */ +import { Directive, Optional } from '@angular/core'; +import { Subscription } from 'rxjs'; + +import { NgControlService } from '../providers/ng-control.service'; +import { NgControl } from '@angular/forms'; +import { IfControlStateService, CONTROL_STATE } from './if-control-state.service'; + +@Directive() +export abstract class AbstractIfState { + protected subscriptions: Subscription[] = []; + protected displayedContent = false; + protected control: NgControl; + + constructor( + @Optional() protected ifControlStateService: IfControlStateService, + @Optional() protected ngControlService: NgControlService + ) { + if (ngControlService) { + this.subscriptions.push( + this.ngControlService.controlChanges.subscribe(control => { + this.control = control; + }) + ); + } + + if (ifControlStateService) { + this.subscriptions.push( + this.ifControlStateService.statusChanges.subscribe((state: CONTROL_STATE) => { + this.handleState(state); + }) + ); + } + } + + ngOnDestroy() { + this.subscriptions.forEach(sub => sub.unsubscribe()); + } + + protected handleState(state: CONTROL_STATE): void { + /* overwrite in implementation to handle status change */ + } +} diff --git a/packages/angular/projects/clr-angular/src/forms/common/if-control-state/if-control-state.service.spec.ts b/packages/angular/projects/clr-angular/src/forms/common/if-control-state/if-control-state.service.spec.ts new file mode 100644 index 0000000000..e006495973 --- /dev/null +++ b/packages/angular/projects/clr-angular/src/forms/common/if-control-state/if-control-state.service.spec.ts @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2016-2020 VMware, Inc. All Rights Reserved. + * This software is released under MIT license. + * The full license information can be found in LICENSE in the root directory of this project. + */ +import { FormControl } from '@angular/forms'; +import { NgControlService } from '../providers/ng-control.service'; +import { IfControlStateService, CONTROL_STATE } from './if-control-state.service'; + +export default function (): void { + describe('IfControlStateService', function () { + let service, ngControlService, testControl; + + beforeEach(() => { + testControl = new FormControl(); + ngControlService = new NgControlService(); + service = new IfControlStateService(ngControlService); + }); + + it('subscribes to the statusChanges when the control is emitted', () => { + spyOn(testControl.statusChanges, 'subscribe').and.callThrough(); + ngControlService.setControl(testControl); + expect(testControl.statusChanges.subscribe).toHaveBeenCalled(); + }); + + it('provides observable for statusChanges, return valid when touched and no rules added', () => { + const cb = jasmine.createSpy('cb'); + const sub = service.statusChanges.subscribe((control: CONTROL_STATE) => cb(control)); + ngControlService.setControl(testControl); + // Change the state of the input to trigger statusChange + testControl.markAsTouched(); + testControl.updateValueAndValidity(); + expect(cb).toHaveBeenCalledWith(CONTROL_STATE.VALID); + sub.unsubscribe(); + }); + + it('should allow a manual trigger of status observable, return NONE', () => { + const cb = jasmine.createSpy('cb'); + const sub = service.statusChanges.subscribe((control: CONTROL_STATE) => cb(control)); + ngControlService.setControl(testControl); + // Manually trigger status check + service.triggerStatusChange(); + expect(cb).toHaveBeenCalledWith(CONTROL_STATE.NONE); + sub.unsubscribe(); + }); + + it('should return state TOUCHED', () => { + const cb = jasmine.createSpy('cb'); + const sub = service.statusChanges.subscribe((control: CONTROL_STATE) => cb(control)); + const fakeControl = { + statusChanges: { + subscribe: () => { + return function unsubscribe() { + // Do nothing + }; + }, + }, + /* Disabled is not implemented yet so we could use it to test uncovered case */ + status: 'DISABLED', + touched: true, + }; + ngControlService.setControl(fakeControl); + service.triggerStatusChange(); + expect(cb).toHaveBeenCalledWith(CONTROL_STATE.NONE); + sub.unsubscribe(); + }); + + it('should return state NONE', () => { + const cb = jasmine.createSpy('cb'); + const sub = service.statusChanges.subscribe((control: CONTROL_STATE) => cb(control)); + const fakeControl = { + statusChanges: { + subscribe: () => { + return function unsubscribe() { + // Do nothing + }; + }, + }, + status: 'INVALID', + touched: false, + }; + ngControlService.setControl(fakeControl); + service.triggerStatusChange(); + expect(cb).toHaveBeenCalledWith(CONTROL_STATE.NONE); + sub.unsubscribe(); + }); + + it('should return state INVALID', () => { + const cb = jasmine.createSpy('cb'); + const sub = service.statusChanges.subscribe((control: CONTROL_STATE) => cb(control)); + const fakeControl = { + statusChanges: { + subscribe: () => { + return function unsubscribe() { + // Do nothing + }; + }, + }, + status: 'INVALID', + touched: true, + }; + ngControlService.setControl(fakeControl); + service.triggerStatusChange(); + expect(cb).toHaveBeenCalledWith(CONTROL_STATE.INVALID); + sub.unsubscribe(); + }); + + it('should return state VALID', () => { + const cb = jasmine.createSpy('cb'); + const sub = service.statusChanges.subscribe((control: CONTROL_STATE) => cb(control)); + const fakeControl = { + statusChanges: { + subscribe: () => { + return function unsubscribe() { + // Do nothing + }; + }, + }, + status: 'VALID', + touched: true, + }; + ngControlService.setControl(fakeControl); + service.triggerStatusChange(); + expect(cb).toHaveBeenCalledWith(CONTROL_STATE.VALID); + sub.unsubscribe(); + }); + }); +} diff --git a/packages/angular/projects/clr-angular/src/forms/common/if-control-state/if-control-state.service.ts b/packages/angular/projects/clr-angular/src/forms/common/if-control-state/if-control-state.service.ts new file mode 100644 index 0000000000..d41284109e --- /dev/null +++ b/packages/angular/projects/clr-angular/src/forms/common/if-control-state/if-control-state.service.ts @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2016-2020 VMware, Inc. All Rights Reserved. + * This software is released under MIT license. + * The full license information can be found in LICENSE in the root directory of this project. + */ + +import { Injectable, OnDestroy } from '@angular/core'; +import { NgControl } from '@angular/forms'; +import { Observable, BehaviorSubject, Subscription } from 'rxjs'; +import { NgControlService } from '../providers/ng-control.service'; + +export enum CONTROL_STATE { + NONE = 'NONE', + VALID = 'VALID', + INVALID = 'INVALID', +} + +@Injectable() +export class IfControlStateService implements OnDestroy { + private subscriptions: Subscription[] = []; + private control: NgControl; + + // Implement our own status changes observable, since Angular controls don't + private _statusChanges: BehaviorSubject = new BehaviorSubject(CONTROL_STATE.NONE); + get statusChanges(): Observable { + return this._statusChanges.asObservable(); + } + + constructor(private ngControlService: NgControlService) { + // Wait for the control to be available + this.subscriptions.push( + this.ngControlService.controlChanges.subscribe(control => { + if (control) { + this.control = control; + // Subscribe to the status change events, only after touched + // and emit the control + this.subscriptions.push( + this.control.statusChanges.subscribe(() => { + this.triggerStatusChange(); + }) + ); + } + }) + ); + } + + triggerStatusChange() { + // These status values are mutually exclusive, so a control + // cannot be both valid AND invalid or invalid AND disabled. + const status = CONTROL_STATE[this.control.status]; + this._statusChanges.next( + this.control.touched && ['VALID', 'INVALID'].includes(status) ? status : CONTROL_STATE.NONE + ); + } + + // Clean up subscriptions + ngOnDestroy() { + this.subscriptions.forEach(subscription => subscription.unsubscribe()); + } +} diff --git a/packages/angular/projects/clr-angular/src/forms/common/if-error/if-error.spec.ts b/packages/angular/projects/clr-angular/src/forms/common/if-control-state/if-error.spec.ts similarity index 87% rename from packages/angular/projects/clr-angular/src/forms/common/if-error/if-error.spec.ts rename to packages/angular/projects/clr-angular/src/forms/common/if-control-state/if-error.spec.ts index 35660b15d2..77c1d74a40 100644 --- a/packages/angular/projects/clr-angular/src/forms/common/if-error/if-error.spec.ts +++ b/packages/angular/projects/clr-angular/src/forms/common/if-control-state/if-error.spec.ts @@ -14,7 +14,7 @@ import { ClrControlError } from '../error'; import { NgControlService } from '../providers/ng-control.service'; import { ClrIfError } from './if-error'; -import { IfErrorService } from './if-error.service'; +import { IfControlStateService } from './if-control-state.service'; const errorMessage = 'ERROR_MESSAGE'; const minLengthMessage = 'MIN_LENGTH_MESSAGE'; @@ -25,7 +25,7 @@ class InvalidUseTest {} @Component({ template: ` ${errorMessage} `, - providers: [IfErrorService, NgControlService], + providers: [IfControlStateService, NgControlService], }) class GeneralErrorTest {} @@ -37,7 +37,7 @@ class GeneralErrorTest {} ${maxLengthMessage}-{{ err.requiredLength }}-{{ err.actualLength }} `, - providers: [IfErrorService, NgControlService], + providers: [IfControlStateService, NgControlService], }) class SpecificErrorTest {} @@ -54,7 +54,7 @@ export default function (): void { }); describe('general error', () => { - let fixture, ifErrorService, ngControlService; + let fixture, ifControlStateService, ngControlService; beforeEach(() => { TestBed.configureTestingModule({ @@ -64,7 +64,7 @@ export default function (): void { fixture = TestBed.createComponent(GeneralErrorTest); fixture.detectChanges(); ngControlService = fixture.debugElement.injector.get(NgControlService); - ifErrorService = fixture.debugElement.injector.get(IfErrorService); + ifControlStateService = fixture.debugElement.injector.get(IfControlStateService); }); it('hides the error initially', () => { @@ -76,14 +76,14 @@ export default function (): void { const control = new FormControl('', Validators.required); control.markAsTouched(); ngControlService.setControl(control); - ifErrorService.triggerStatusChange(); + ifControlStateService.triggerStatusChange(); fixture.detectChanges(); expect(fixture.nativeElement.innerHTML).toContain(errorMessage); }); }); describe('specific error', () => { - let fixture, ifErrorService, ngControlService; + let fixture, ifControlStateService, ngControlService; beforeEach(() => { TestBed.configureTestingModule({ @@ -93,7 +93,7 @@ export default function (): void { fixture = TestBed.createComponent(SpecificErrorTest); fixture.detectChanges(); ngControlService = fixture.debugElement.injector.get(NgControlService); - ifErrorService = fixture.debugElement.injector.get(IfErrorService); + ifControlStateService = fixture.debugElement.injector.get(IfControlStateService); }); it('hides the error initially', () => { @@ -105,7 +105,7 @@ export default function (): void { const control = new FormControl('', [Validators.required, Validators.minLength(5)]); control.markAsTouched(); ngControlService.setControl(control); - ifErrorService.triggerStatusChange(); + ifControlStateService.triggerStatusChange(); fixture.detectChanges(); expect(fixture.nativeElement.innerHTML).toContain(errorMessage); }); @@ -114,7 +114,7 @@ export default function (): void { expect(fixture.nativeElement.innerHTML).not.toContain(errorMessage); const control = new FormControl('abc', [Validators.required, Validators.minLength(5)]); ngControlService.setControl(control); - ifErrorService.triggerStatusChange(); + ifControlStateService.triggerStatusChange(); fixture.detectChanges(); expect(fixture.nativeElement.innerHTML).not.toContain(errorMessage); expect(fixture.nativeElement.innerHTML).toContain(minLengthMessage); @@ -123,7 +123,7 @@ export default function (): void { it('displays the error message with values from error object in context', () => { const control = new FormControl('abcdef', [Validators.maxLength(5)]); ngControlService.setControl(control); - ifErrorService.triggerStatusChange(); + ifControlStateService.triggerStatusChange(); fixture.detectChanges(); expect(fixture.nativeElement.innerHTML).toContain(`${maxLengthMessage}-5-6`); }); diff --git a/packages/angular/projects/clr-angular/src/forms/common/if-control-state/if-error.ts b/packages/angular/projects/clr-angular/src/forms/common/if-control-state/if-error.ts new file mode 100644 index 0000000000..6673cdf850 --- /dev/null +++ b/packages/angular/projects/clr-angular/src/forms/common/if-control-state/if-error.ts @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2016-2020 VMware, Inc. All Rights Reserved. + * This software is released under MIT license. + * The full license information can be found in LICENSE in the root directory of this project. + */ +import { Directive, Input, Optional, TemplateRef, ViewContainerRef } from '@angular/core'; +import { NgControlService } from '../providers/ng-control.service'; +import { IfControlStateService, CONTROL_STATE } from './if-control-state.service'; +import { AbstractIfState } from './abstract-if-state'; + +@Directive({ selector: '[clrIfError]' }) +export class ClrIfError extends AbstractIfState { + @Input('clrIfError') error: string; + + constructor( + @Optional() ifControlStateService: IfControlStateService, + @Optional() ngControlService: NgControlService, + private template: TemplateRef, + private container: ViewContainerRef + ) { + super(ifControlStateService, ngControlService); + + if (!this.ifControlStateService) { + throw new Error('clrIfError can only be used within a form control container element like clr-input-container'); + } + } + /** + * @param state CONTROL_STATE + */ + protected handleState(state: CONTROL_STATE) { + const isInvalid = CONTROL_STATE.INVALID === state; + + if (isInvalid && this.displayedContent === false) { + let options = {}; + if (this.error && this.control && this.control.hasError(this.error)) { + options = { error: this.control.getError(this.error) }; + } + this.container.createEmbeddedView(this.template, options); + } else if (!isInvalid) { + this.container.clear(); + } + this.displayedContent = isInvalid; + } +} diff --git a/packages/angular/projects/clr-angular/src/forms/common/if-control-state/if-success.spec.ts b/packages/angular/projects/clr-angular/src/forms/common/if-control-state/if-success.spec.ts new file mode 100644 index 0000000000..618f82cf53 --- /dev/null +++ b/packages/angular/projects/clr-angular/src/forms/common/if-control-state/if-success.spec.ts @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2016-2020 VMware, Inc. All Rights Reserved. + * This software is released under MIT license. + * The full license information can be found in LICENSE in the root directory of this project. + */ +import { Component } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { FormControl, FormsModule, Validators } from '@angular/forms'; +import { ClrIconModule } from '../../../icon/icon.module'; +import { ClrInput } from '../../input/input'; +import { ClrInputContainer } from '../../input/input-container'; +import { NgControlService } from '../providers/ng-control.service'; +import { ClrControlSuccess } from '../success'; +import { ClrIfSuccess } from './if-success'; +import { IfControlStateService } from './if-control-state.service'; + +const successMessage = 'SUCCESS_MESSAGE'; +const minLengthMessage = 'MIN_LENGTH_MESSAGE'; + +@Component({ template: `
` }) +class InvalidUseTest {} + +@Component({ + template: ` ${successMessage} `, + providers: [IfControlStateService, NgControlService], +}) +class GeneralSuccessTest {} + +@Component({ + template: ` + ${successMessage} + ${minLengthMessage} + `, + providers: [IfControlStateService, NgControlService], +}) +class SpecificSuccessTest {} + +export default function (): void { + describe('ClrIfSuccess', () => { + describe('invalid use', () => { + it('throws error when used outside of a control container', () => { + TestBed.configureTestingModule({ declarations: [ClrIfSuccess, InvalidUseTest] }); + expect(() => { + const fixture = TestBed.createComponent(InvalidUseTest); + fixture.detectChanges(); + }).toThrow(); + }); + }); + + describe('general success', () => { + let fixture, ifControlStateService, ngControlService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ClrIconModule, FormsModule], + declarations: [ClrInput, ClrControlSuccess, ClrInputContainer, ClrIfSuccess, GeneralSuccessTest], + }); + fixture = TestBed.createComponent(GeneralSuccessTest); + fixture.detectChanges(); + ngControlService = fixture.debugElement.injector.get(NgControlService); + ifControlStateService = fixture.debugElement.injector.get(IfControlStateService); + }); + + it('hides the success initially', () => { + expect(fixture.nativeElement.innerHTML).not.toContain(successMessage); + }); + + it('displays the success message after touched', () => { + expect(fixture.nativeElement.innerHTML).not.toContain(successMessage); + const control = new FormControl('abc', Validators.required); + control.markAsTouched(); + ngControlService.setControl(control); + ifControlStateService.triggerStatusChange(); + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML).toContain(successMessage); + }); + }); + + describe('specific success', () => { + let fixture, ifControlStateService, ngControlService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ClrIconModule, FormsModule], + declarations: [ClrInput, ClrControlSuccess, ClrInputContainer, ClrIfSuccess, SpecificSuccessTest], + }); + fixture = TestBed.createComponent(SpecificSuccessTest); + fixture.detectChanges(); + ngControlService = fixture.debugElement.injector.get(NgControlService); + ifControlStateService = fixture.debugElement.injector.get(IfControlStateService); + }); + + it('hides the success initially', () => { + expect(fixture.nativeElement.innerHTML).not.toContain(successMessage); + }); + + it('displays the success when the specific success is defined', () => { + expect(fixture.nativeElement.innerHTML).not.toContain(successMessage); + const control = new FormControl('abcde', [Validators.required, Validators.minLength(5)]); + control.markAsTouched(); + ngControlService.setControl(control); + ifControlStateService.triggerStatusChange(); + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML).toContain(successMessage); + }); + }); + }); +} diff --git a/packages/angular/projects/clr-angular/src/forms/common/if-control-state/if-success.ts b/packages/angular/projects/clr-angular/src/forms/common/if-control-state/if-success.ts new file mode 100644 index 0000000000..70685ec6c5 --- /dev/null +++ b/packages/angular/projects/clr-angular/src/forms/common/if-control-state/if-success.ts @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2016-2020 VMware, Inc. All Rights Reserved. + * This software is released under MIT license. + * The full license information can be found in LICENSE in the root directory of this project. + */ +import { Directive, Optional, TemplateRef, ViewContainerRef } from '@angular/core'; +import { NgControlService } from '../providers/ng-control.service'; +import { IfControlStateService, CONTROL_STATE } from './if-control-state.service'; +import { AbstractIfState } from './abstract-if-state'; + +@Directive({ selector: '[clrIfSuccess]' }) +export class ClrIfSuccess extends AbstractIfState { + constructor( + @Optional() ifControlStateService: IfControlStateService, + @Optional() ngControlService: NgControlService, + private template: TemplateRef, + private container: ViewContainerRef + ) { + super(ifControlStateService, ngControlService); + + if (!ifControlStateService) { + throw new Error('ClrIfSuccess can only be used within a form control container element like clr-input-container'); + } + } + + /** + * @param state CONTROL_STATE + */ + protected handleState(state: CONTROL_STATE) { + const isValid = CONTROL_STATE.VALID === state; + + if (isValid && !this.displayedContent) { + this.container.createEmbeddedView(this.template); + } else if (!isValid) { + this.container.clear(); + } + this.displayedContent = isValid; + } +} diff --git a/packages/angular/projects/clr-angular/src/forms/common/if-error/if-error.service.spec.ts b/packages/angular/projects/clr-angular/src/forms/common/if-error/if-error.service.spec.ts deleted file mode 100644 index 1633825297..0000000000 --- a/packages/angular/projects/clr-angular/src/forms/common/if-error/if-error.service.spec.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (c) 2016-2020 VMware, Inc. All Rights Reserved. - * This software is released under MIT license. - * The full license information can be found in LICENSE in the root directory of this project. - */ -import { FormControl } from '@angular/forms'; - -import { NgControlService } from '../providers/ng-control.service'; - -import { IfErrorService } from './if-error.service'; - -export default function (): void { - describe('IfErrorService', function () { - let service, ngControlService, testControl; - - beforeEach(() => { - testControl = new FormControl(true); - ngControlService = new NgControlService(); - service = new IfErrorService(ngControlService); - }); - - it('subscribes to the statusChanges when the control is emitted', () => { - spyOn(testControl.statusChanges, 'subscribe').and.callThrough(); - ngControlService.setControl(testControl); - expect(testControl.statusChanges.subscribe).toHaveBeenCalled(); - }); - - it('provides observable for statusChanges, passing the invalid state', () => { - const cb = jasmine.createSpy('cb'); - const sub = service.statusChanges.subscribe(control => cb(control)); - ngControlService.setControl(testControl); - // Change the state of the input to trigger statusChange - testControl.markAsTouched(); - testControl.updateValueAndValidity(); - expect(cb).toHaveBeenCalled(); - expect(cb).toHaveBeenCalledWith(false); - sub.unsubscribe(); - }); - - it('should allow a manual trigger of status observable', () => { - const cb = jasmine.createSpy('cb'); - const sub = service.statusChanges.subscribe(control => cb(control)); - ngControlService.setControl(testControl); - // Manually trigger status check - service.triggerStatusChange(); - expect(cb).toHaveBeenCalled(); - expect(cb).toHaveBeenCalledWith(false); - sub.unsubscribe(); - }); - - it('should return invalid state', () => { - const cb = jasmine.createSpy('cb'); - const sub = service.statusChanges.subscribe(control => cb(control)); - const fakeControl = { - statusChanges: { - subscribe: () => { - return function unsubscribe() { - // Do nothing - }; - }, - }, - touched: true, - invalid: true, - }; - ngControlService.setControl(fakeControl); - service.triggerStatusChange(); - expect(cb).toHaveBeenCalledWith(true); - sub.unsubscribe(); - }); - }); -} diff --git a/packages/angular/projects/clr-angular/src/forms/common/if-error/if-error.service.ts b/packages/angular/projects/clr-angular/src/forms/common/if-error/if-error.service.ts deleted file mode 100644 index 229e98b674..0000000000 --- a/packages/angular/projects/clr-angular/src/forms/common/if-error/if-error.service.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (c) 2016-2019 VMware, Inc. All Rights Reserved. - * This software is released under MIT license. - * The full license information can be found in LICENSE in the root directory of this project. - */ - -import { Injectable, OnDestroy } from '@angular/core'; -import { NgControl } from '@angular/forms'; -import { Observable, Subject, Subscription } from 'rxjs'; - -import { NgControlService } from '../providers/ng-control.service'; - -@Injectable() -export class IfErrorService implements OnDestroy { - // Implement our own status changes observable, since Angular controls don't - // fire on events like blur, and we want to return the boolean state instead of a string - private _statusChanges: Subject = new Subject(); - get statusChanges(): Observable { - return this._statusChanges.asObservable(); - } - - private subscriptions: Subscription[] = []; - private control: NgControl; - - constructor(private ngControlService: NgControlService) { - // Wait for the control to be available - this.subscriptions.push( - this.ngControlService.controlChanges.subscribe(control => { - if (control) { - this.control = control; - this.listenForChanges(); - } - }) - ); - } - - // Subscribe to the status change events, only after touched and emit the control - private listenForChanges() { - this.subscriptions.push( - this.control.statusChanges.subscribe(() => { - this.sendValidity(); - }) - ); - } - - private sendValidity() { - if (this.control.touched && this.control.invalid) { - this._statusChanges.next(true); - } else { - this._statusChanges.next(false); - } - } - - // Allows a control to push a status check upstream, such as on blur - triggerStatusChange() { - if (this.control) { - this.sendValidity(); - } - } - - // Clean up subscriptions - ngOnDestroy() { - this.subscriptions.forEach(sub => sub.unsubscribe()); - } -} diff --git a/packages/angular/projects/clr-angular/src/forms/common/if-error/if-error.ts b/packages/angular/projects/clr-angular/src/forms/common/if-error/if-error.ts deleted file mode 100644 index 868ffc9f40..0000000000 --- a/packages/angular/projects/clr-angular/src/forms/common/if-error/if-error.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (c) 2016-2020 VMware, Inc. All Rights Reserved. - * This software is released under MIT license. - * The full license information can be found in LICENSE in the root directory of this project. - */ -import { Directive, Input, Optional, TemplateRef, ViewContainerRef } from '@angular/core'; -import { Subscription } from 'rxjs'; - -import { IfErrorService } from './if-error.service'; -import { NgControlService } from '../providers/ng-control.service'; -import { NgControl } from '@angular/forms'; - -@Directive({ selector: '[clrIfError]' }) -export class ClrIfError { - constructor( - @Optional() private ifErrorService: IfErrorService, - @Optional() private ngControlService: NgControlService, - private template: TemplateRef, - private container: ViewContainerRef - ) { - if (!this.ifErrorService) { - throw new Error('clrIfError can only be used within a form control container element like clr-input-container'); - } else { - this.displayError(false); - } - this.subscriptions.push( - this.ngControlService.controlChanges.subscribe(control => { - this.control = control; - }) - ); - this.subscriptions.push( - this.ifErrorService.statusChanges.subscribe(invalid => { - // If there is a specific error to track, check it, otherwise check overall validity - if (this.error && this.control) { - this.displayError(this.control.hasError(this.error)); - } else { - this.displayError(invalid); - } - }) - ); - } - - @Input('clrIfError') error: string; - - private subscriptions: Subscription[] = []; - private displayed = false; - private control: NgControl; - - ngOnDestroy() { - this.subscriptions.forEach(sub => sub.unsubscribe()); - } - - private displayError(invalid: boolean) { - if (invalid && !this.displayed) { - this.container.createEmbeddedView(this.template, { error: this.control.getError(this.error) }); - this.displayed = true; - } else if (!invalid) { - this.container.clear(); - this.displayed = false; - } - } -} diff --git a/packages/angular/projects/clr-angular/src/forms/common/index.ts b/packages/angular/projects/clr-angular/src/forms/common/index.ts index b7f5a09dfe..3049d42049 100644 --- a/packages/angular/projects/clr-angular/src/forms/common/index.ts +++ b/packages/angular/projects/clr-angular/src/forms/common/index.ts @@ -4,8 +4,10 @@ * The full license information can be found in LICENSE in the root directory of this project. */ -export * from './if-error/if-error'; +export * from './if-control-state/if-error'; +export * from './if-control-state/if-success'; export * from './error'; +export * from './success'; export * from './form'; export * from './helper'; export * from './label'; diff --git a/packages/angular/projects/clr-angular/src/forms/common/providers/control-class.service.spec.ts b/packages/angular/projects/clr-angular/src/forms/common/providers/control-class.service.spec.ts index 866d6cacc3..fd35b7bedd 100644 --- a/packages/angular/projects/clr-angular/src/forms/common/providers/control-class.service.spec.ts +++ b/packages/angular/projects/clr-angular/src/forms/common/providers/control-class.service.spec.ts @@ -5,6 +5,7 @@ */ import { ControlClassService } from './control-class.service'; import { LayoutService } from './layout.service'; +import { CONTROL_STATE } from '../if-control-state/if-control-state.service'; export default function (): void { describe('ControlClassService', function () { @@ -20,20 +21,24 @@ export default function (): void { }); it('should return clr-error when invalid', function () { - expect(controlClassService.controlClass(true)).toBe('clr-error'); + expect(controlClassService.controlClass(CONTROL_STATE.INVALID)).toBe('clr-error'); + }); + + it('should return clr-success when valid', function () { + expect(controlClassService.controlClass(CONTROL_STATE.VALID)).toBe('clr-success'); }); it('should return grid classes when using grid', function () { - expect(controlClassService.controlClass(false, true)).toBe('clr-col-md-10 clr-col-12'); + expect(controlClassService.controlClass(CONTROL_STATE.NONE, true)).toBe('clr-col-md-10 clr-col-12'); }); it('should return error and grid classes when invalid and using grid', function () { - expect(controlClassService.controlClass(true, true)).toBe('clr-error clr-col-md-10 clr-col-12'); + expect(controlClassService.controlClass(CONTROL_STATE.INVALID, true)).toBe('clr-error clr-col-md-10 clr-col-12'); }); it('should not add grid classes if already present ', function () { controlClassService.className = 'clr-col-md-3 clr-col-12'; - expect(controlClassService.controlClass(false, true)).toBe('clr-col-md-3 clr-col-12'); + expect(controlClassService.controlClass(CONTROL_STATE.NONE, true)).toBe('clr-col-md-3 clr-col-12'); }); it('should init the control class', function () { @@ -53,19 +58,19 @@ export default function (): void { it('should return any classes provided by default', function () { controlClassService.className = 'test-class'; - expect(controlClassService.controlClass(false, false)).toContain('test-class'); + expect(controlClassService.controlClass(CONTROL_STATE.NONE, false)).toContain('test-class'); }); it('should return any additional classes passed by the control', function () { controlClassService.className = 'test-class'; layoutService.labelSize = 2; - expect(controlClassService.controlClass(false, false, 'extra-class')).toBe('test-class extra-class'); + expect(controlClassService.controlClass(CONTROL_STATE.NONE, false, 'extra-class')).toBe('test-class extra-class'); }); it('should return appropiate col size classes for grid layouts', function () { controlClassService.className = 'test-class'; layoutService.labelSize = 3; - expect(controlClassService.controlClass(false, true, 'extra-class')).toBe( + expect(controlClassService.controlClass(CONTROL_STATE.NONE, true, 'extra-class')).toBe( 'test-class extra-class clr-col-md-9 clr-col-12' ); }); diff --git a/packages/angular/projects/clr-angular/src/forms/common/providers/control-class.service.ts b/packages/angular/projects/clr-angular/src/forms/common/providers/control-class.service.ts index 8c7ba63b65..b49fd35994 100644 --- a/packages/angular/projects/clr-angular/src/forms/common/providers/control-class.service.ts +++ b/packages/angular/projects/clr-angular/src/forms/common/providers/control-class.service.ts @@ -6,6 +6,10 @@ import { Injectable, Optional, Renderer2 } from '@angular/core'; import { LayoutService } from './layout.service'; +import { CONTROL_STATE } from '../if-control-state/if-control-state.service'; + +const CLASS_ERROR = 'clr-error'; +const CLASS_SUCCESS = 'clr-success'; @Injectable() export class ControlClassService { @@ -13,11 +17,16 @@ export class ControlClassService { constructor(@Optional() private layoutService: LayoutService) {} - controlClass(invalid = false, grid = false, additional = '') { + controlClass(state: CONTROL_STATE = CONTROL_STATE.NONE, grid = false, additional = '') { const controlClasses = [this.className, additional]; - if (invalid) { - controlClasses.push('clr-error'); + if (state === CONTROL_STATE.VALID) { + controlClasses.push(CLASS_SUCCESS); + } + + if (state === CONTROL_STATE.INVALID) { + controlClasses.push(CLASS_ERROR); } + if (grid && this.layoutService && this.className.indexOf('clr-col') === -1) { controlClasses.push(`clr-col-md-${this.layoutService.maxLabelSize - this.layoutService.labelSize} clr-col-12`); } diff --git a/packages/angular/projects/clr-angular/src/forms/common/success.spec.ts b/packages/angular/projects/clr-angular/src/forms/common/success.spec.ts new file mode 100644 index 0000000000..4800a66aa2 --- /dev/null +++ b/packages/angular/projects/clr-angular/src/forms/common/success.spec.ts @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2016-2020 VMware, Inc. All Rights Reserved. + * This software is released under MIT license. + * The full license information can be found in LICENSE in the root directory of this project. + */ +import { Component } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { ControlIdService } from './providers/control-id.service'; +import { ClrControlSuccess } from './success'; + +@Component({ template: `Test success message` }) +class SimpleTest {} + +export default function (): void { + describe('ClrControlSuccess', () => { + let fixture: ComponentFixture; + let element: HTMLElement; + + beforeEach(function () { + TestBed.configureTestingModule({ + declarations: [ClrControlSuccess, SimpleTest], + providers: [ControlIdService], + }); + fixture = TestBed.createComponent(SimpleTest); + fixture.detectChanges(); + element = fixture.debugElement.query(By.directive(ClrControlSuccess)).nativeElement; + }); + + it('projects content', function () { + expect(element.innerText).toContain('Test success message'); + }); + + it('adds the .clr-subtext class to host', function () { + expect(element.classList.contains('clr-subtext')).toBeTrue(); + }); + + it('should add id to host', function () { + expect(element.getAttribute('id')).toContain('-success'); + }); + }); +} diff --git a/packages/angular/projects/clr-angular/src/forms/common/success.ts b/packages/angular/projects/clr-angular/src/forms/common/success.ts new file mode 100644 index 0000000000..5eabc98f1e --- /dev/null +++ b/packages/angular/projects/clr-angular/src/forms/common/success.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2016-2020 VMware, Inc. All Rights Reserved. + * This software is released under MIT license. + * The full license information can be found in LICENSE in the root directory of this project. + */ + +import { Component, Optional } from '@angular/core'; +import { ControlIdService } from './providers/control-id.service'; + +@Component({ + selector: 'clr-control-success', + template: ` `, + host: { + '[class.clr-subtext]': 'true', + '[id]': 'controlIdService?.id + "-success"', + }, +}) +export class ClrControlSuccess { + constructor(@Optional() public controlIdService: ControlIdService) {} +} diff --git a/packages/angular/projects/clr-angular/src/forms/common/wrapped-control.spec.ts b/packages/angular/projects/clr-angular/src/forms/common/wrapped-control.spec.ts index c67d49370d..62e8549d79 100644 --- a/packages/angular/projects/clr-angular/src/forms/common/wrapped-control.spec.ts +++ b/packages/angular/projects/clr-angular/src/forms/common/wrapped-control.spec.ts @@ -14,11 +14,11 @@ import { ClrHostWrappingModule } from '../../utils/host-wrapping/host-wrapping.m import { ControlIdService } from './providers/control-id.service'; import { NgControlService } from './providers/ng-control.service'; -import { IfErrorService } from './if-error/if-error.service'; import { ControlClassService } from './providers/control-class.service'; import { MarkControlService } from './providers/mark-control.service'; import { WrappedFormControl } from './wrapped-control'; import { LayoutService } from './providers/layout.service'; +import { IfControlStateService } from './if-control-state/if-control-state.service'; /* * Components using the WrappedFormControl we want to test. @@ -59,7 +59,7 @@ class TestControl2 extends WrappedFormControl { ControlIdService, MarkControlService, NgControlService, - IfErrorService, + IfControlStateService, ControlClassService, LayoutService, ], @@ -115,7 +115,7 @@ interface TestContext { controlClassService?: ControlClassService; markControlService?: MarkControlService; ngControlService?: NgControlService; - ifErrorService?: IfErrorService; + ifControlStateService: IfControlStateService; layoutService?: LayoutService; } @@ -140,7 +140,7 @@ export default function (): void { testContext.markControlService = wrapperDebugElement.injector.get(MarkControlService); testContext.controlClassService = wrapperDebugElement.injector.get(ControlClassService); testContext.ngControlService = wrapperDebugElement.injector.get(NgControlService); - testContext.ifErrorService = wrapperDebugElement.injector.get(IfErrorService); + testContext.ifControlStateService = wrapperDebugElement.injector.get(IfControlStateService); testContext.layoutService = wrapperDebugElement.injector.get(LayoutService); } catch (error) { // Swallow errors @@ -229,12 +229,12 @@ export default function (): void { }); it('triggers status changes on blur', function (this: TestContext) { - spyOn(IfErrorService.prototype, 'triggerStatusChange').and.callThrough(); + spyOn(IfControlStateService.prototype, 'triggerStatusChange').and.callThrough(); setupTest(this, WithControl, TestControl3); this.input.focus(); this.input.blur(); this.fixture.detectChanges(); - expect(IfErrorService.prototype.triggerStatusChange).toHaveBeenCalled(); + expect(IfControlStateService.prototype.triggerStatusChange).toHaveBeenCalled(); }); it('implements ngOnDestroy', function (this: TestContext) { diff --git a/packages/angular/projects/clr-angular/src/forms/common/wrapped-control.ts b/packages/angular/projects/clr-angular/src/forms/common/wrapped-control.ts index febc9a0905..c915bac8f4 100644 --- a/packages/angular/projects/clr-angular/src/forms/common/wrapped-control.ts +++ b/packages/angular/projects/clr-angular/src/forms/common/wrapped-control.ts @@ -25,15 +25,15 @@ import { DynamicWrapper } from '../../utils/host-wrapping/dynamic-wrapper'; import { ControlIdService } from './providers/control-id.service'; import { NgControlService } from './providers/ng-control.service'; -import { IfErrorService } from './if-error/if-error.service'; import { NgControl } from '@angular/forms'; import { ControlClassService } from './providers/control-class.service'; import { MarkControlService } from './providers/mark-control.service'; +import { IfControlStateService, CONTROL_STATE } from './if-control-state/if-control-state.service'; @Directive() export class WrappedFormControl implements OnInit, OnDestroy { protected ngControlService: NgControlService; - private ifErrorService: IfErrorService; + private ifControlStateService: IfControlStateService; private controlClassService: ControlClassService; private markControlService: MarkControlService; protected renderer: Renderer2; @@ -59,7 +59,7 @@ export class WrappedFormControl implements OnInit, OnD this.el = el; try { this.ngControlService = injector.get(NgControlService); - this.ifErrorService = injector.get(IfErrorService); + this.ifControlStateService = injector.get(IfControlStateService); this.controlClassService = injector.get(ControlClassService); this.markControlService = injector.get(MarkControlService); } catch (e) { @@ -93,8 +93,8 @@ export class WrappedFormControl implements OnInit, OnD @HostListener('blur') triggerValidation() { - if (this.ifErrorService) { - this.ifErrorService.triggerStatusChange(); + if (this.ifControlStateService) { + this.ifControlStateService.triggerStatusChange(); } } @@ -134,24 +134,36 @@ export class WrappedFormControl implements OnInit, OnD } private listenForErrorStateChanges() { - if (this.ifErrorService) { + if (this.ifControlStateService) { this.subscriptions.push( - this.ifErrorService.statusChanges + this.ifControlStateService.statusChanges .pipe( - startWith(false), + startWith(CONTROL_STATE.NONE), filter(() => this.renderer && !!this.el), distinctUntilChanged() ) - .subscribe(error => this.setAriaDescribedBy(error)) + .subscribe(state => this.setAriaDescribedBy(state)) ); } } - private setAriaDescribedBy(error: boolean) { - this.renderer.setAttribute(this.el.nativeElement, 'aria-describedby', this.getAriaDescribedById(error)); + private setAriaDescribedBy(state: CONTROL_STATE) { + this.renderer.setAttribute(this.el.nativeElement, 'aria-describedby', this.getAriaDescribedById(state)); } - private getAriaDescribedById(error: boolean): string { - return this.controlIdService.id.concat(error ? '-error' : '-helper'); + private getAriaDescribedById(state: CONTROL_STATE): string { + let suffix; + + switch (state) { + case CONTROL_STATE.INVALID: + suffix = '-error'; + break; + case CONTROL_STATE.VALID: + suffix = '-success'; + break; + default: + suffix = '-helper'; + } + return this.controlIdService.id.concat(suffix); } } diff --git a/packages/angular/projects/clr-angular/src/forms/datalist/datalist-container.spec.ts b/packages/angular/projects/clr-angular/src/forms/datalist/datalist-container.spec.ts index 2b9de8d036..dfd1357765 100644 --- a/packages/angular/projects/clr-angular/src/forms/datalist/datalist-container.spec.ts +++ b/packages/angular/projects/clr-angular/src/forms/datalist/datalist-container.spec.ts @@ -24,6 +24,7 @@ import { TemplateDrivenSpec, ReactiveSpec, ContainerNoLabelSpec } from '../tests Helper text Must be at least 5 characters + Valid `, }) @@ -57,6 +58,7 @@ class NoLabelTest {} Helper text Must be at least 5 characters + Valid `, diff --git a/packages/angular/projects/clr-angular/src/forms/datalist/datalist-container.ts b/packages/angular/projects/clr-angular/src/forms/datalist/datalist-container.ts index 9ca578a8b1..8dbcfe59ba 100644 --- a/packages/angular/projects/clr-angular/src/forms/datalist/datalist-container.ts +++ b/packages/angular/projects/clr-angular/src/forms/datalist/datalist-container.ts @@ -4,15 +4,16 @@ * The full license information can be found in LICENSE in the root directory of this project. */ -import { Component, Optional } from '@angular/core'; +import { Component, Optional, ContentChild } from '@angular/core'; import { ControlClassService } from '../common/providers/control-class.service'; import { LayoutService } from '../common/providers/layout.service'; import { ControlIdService } from '../common/providers/control-id.service'; import { FocusService } from '../common/providers/focus.service'; -import { IfErrorService } from '../common/if-error/if-error.service'; import { NgControlService } from '../common/providers/ng-control.service'; import { DatalistIdService } from './providers/datalist-id.service'; import { ClrAbstractContainer } from '../common/abstract-container'; +import { ClrControlSuccess } from '../common/success'; +import { IfControlStateService } from '../common/if-control-state/if-control-state.service'; @Component({ selector: 'clr-datalist-container', @@ -25,10 +26,22 @@ import { ClrAbstractContainer } from '../common/abstract-container'; - + + - - + + + `, host: { @@ -41,22 +54,24 @@ import { ClrAbstractContainer } from '../common/abstract-container'; LayoutService, ControlIdService, FocusService, - IfErrorService, NgControlService, DatalistIdService, + IfControlStateService, ], }) export class ClrDatalistContainer extends ClrAbstractContainer { focus = false; + @ContentChild(ClrControlSuccess) controlSuccessComponent: ClrControlSuccess; + constructor( controlClassService: ControlClassService, @Optional() layoutService: LayoutService, - ifErrorService: IfErrorService, ngControlService: NgControlService, - private focusService: FocusService + private focusService: FocusService, + protected ifControlStateService: IfControlStateService ) { - super(ifErrorService, layoutService, controlClassService, ngControlService); + super(ifControlStateService, layoutService, controlClassService, ngControlService); this.subscriptions.push(this.focusService.focusChange.subscribe(state => (this.focus = state))); } diff --git a/packages/angular/projects/clr-angular/src/forms/datalist/datalist-input.spec.ts b/packages/angular/projects/clr-angular/src/forms/datalist/datalist-input.spec.ts index f4fcf71aa4..bbfc703edf 100644 --- a/packages/angular/projects/clr-angular/src/forms/datalist/datalist-input.spec.ts +++ b/packages/angular/projects/clr-angular/src/forms/datalist/datalist-input.spec.ts @@ -14,9 +14,9 @@ import { ReactiveSpec, TemplateDrivenSpec } from '../tests/control.spec'; import { TestBed } from '@angular/core/testing'; import { LayoutService } from '../common/providers/layout.service'; import { ClrIconModule } from '../../icon/icon.module'; -import { IfErrorService } from '../common/if-error/if-error.service'; import { ClrCommonFormsModule } from '../common/common.module'; import { NgControlService } from '../common/providers/ng-control.service'; +import { IfControlStateService } from '../common/if-control-state/if-control-state.service'; @Component({ template: ` `, @@ -80,7 +80,7 @@ export default function (): void { TestBed.configureTestingModule({ imports: [ClrIconModule, ClrCommonFormsModule, FormsModule], declarations: [ClrDatalistContainer, ClrDatalistInput, TemplateDrivenTest], - providers: [NgControl, NgControlService, IfErrorService, LayoutService], + providers: [IfControlStateService, NgControl, NgControlService, LayoutService], }); fixture = TestBed.createComponent(TemplateDrivenTest); containerDE = fixture.debugElement.query(By.directive(ClrDatalistContainer)); diff --git a/packages/angular/projects/clr-angular/src/forms/datepicker/date-container.spec.ts b/packages/angular/projects/clr-angular/src/forms/datepicker/date-container.spec.ts index 8929d24ebc..695c64ca04 100644 --- a/packages/angular/projects/clr-angular/src/forms/datepicker/date-container.spec.ts +++ b/packages/angular/projects/clr-angular/src/forms/datepicker/date-container.spec.ts @@ -10,7 +10,6 @@ import { TestBed, async } from '@angular/core/testing'; import { TestContext } from '../../data/datagrid/helpers.spec'; import { ClrPopoverToggleService } from '../../utils/popover/providers/popover-toggle.service'; -import { IfErrorService } from '../common/if-error/if-error.service'; import { ControlClassService } from '../common/providers/control-class.service'; import { ControlIdService } from '../common/providers/control-id.service'; import { FocusService } from '../common/providers/focus.service'; @@ -30,6 +29,7 @@ import { ClrPopoverPositions } from '../../utils/popover/enums/positions.enum'; import { ClrPopoverEventsService } from '../../utils/popover/providers/popover-events.service'; import { ClrPopoverPositionService } from '../../utils/popover/providers/popover-position.service'; import { ViewManagerService } from './providers/view-manager.service'; +import { IfControlStateService, CONTROL_STATE } from '../common/if-control-state/if-control-state.service'; const DATEPICKER_PROVIDERS: any[] = [ ClrPopoverEventsService, @@ -39,7 +39,7 @@ const DATEPICKER_PROVIDERS: any[] = [ ViewManagerService, LocaleHelperService, ControlClassService, - IfErrorService, + IfControlStateService, FocusService, LayoutService, NgControlService, @@ -189,12 +189,12 @@ export default function () { expect(context.clarityDirective.controlClass()).toContain('clr-col-md-10'); expect(context.clarityDirective.controlClass()).toContain('clr-col-12'); expect(context.clarityDirective.controlClass()).not.toContain('clr-error'); - context.clarityDirective.invalid = true; + context.clarityDirective.state = CONTROL_STATE.INVALID; expect(context.clarityDirective.controlClass()).toContain('clr-error'); const controlClassService = context.getClarityProvider(ControlClassService); const layoutService = context.getClarityProvider(LayoutService); layoutService.layout = Layouts.VERTICAL; - context.clarityDirective.invalid = false; + context.clarityDirective.state = CONTROL_STATE.VALID; expect(context.clarityDirective.controlClass()).not.toContain('clr-error'); expect(context.clarityDirective.controlClass()).not.toContain('clr-col-md-10'); controlClassService.className = 'clr-col-2'; diff --git a/packages/angular/projects/clr-angular/src/forms/datepicker/date-container.ts b/packages/angular/projects/clr-angular/src/forms/datepicker/date-container.ts index ef74326387..57b7e3647d 100644 --- a/packages/angular/projects/clr-angular/src/forms/datepicker/date-container.ts +++ b/packages/angular/projects/clr-angular/src/forms/datepicker/date-container.ts @@ -13,12 +13,10 @@ import { ElementRef, Input, } from '@angular/core'; -import { Subscription } from 'rxjs'; import { NgControl } from '@angular/forms'; import { ClrPopoverToggleService } from '../../utils/popover/providers/popover-toggle.service'; import { DynamicWrapper } from '../../utils/host-wrapping/dynamic-wrapper'; -import { IfErrorService } from '../common/if-error/if-error.service'; import { ControlClassService } from '../common/providers/control-class.service'; import { ControlIdService } from '../common/providers/control-id.service'; import { FocusService } from '../common/providers/focus.service'; @@ -37,6 +35,9 @@ import { ClrPopoverPosition } from '../../utils/popover/interfaces/popover-posit import { ClrPopoverEventsService } from '../../utils/popover/providers/popover-events.service'; import { ClrPopoverPositionService } from '../../utils/popover/providers/popover-position.service'; import { ViewManagerService } from './providers/view-manager.service'; +import { ClrControlSuccess } from '../common/success'; +import { Subscription } from 'rxjs'; +import { IfControlStateService, CONTROL_STATE } from '../common/if-control-state/if-control-state.service'; @Component({ selector: 'clr-date-container', @@ -65,9 +66,16 @@ import { ViewManagerService } from './providers/view-manager.service'; > + - + + `, providers: [ @@ -76,7 +84,6 @@ import { ViewManagerService } from './providers/view-manager.service'; ClrPopoverEventsService, ClrPopoverPositionService, LocaleHelperService, - IfErrorService, ControlClassService, FocusService, NgControlService, @@ -85,6 +92,7 @@ import { ViewManagerService } from './providers/view-manager.service'; DatepickerEnabledService, DateFormControlService, ViewManagerService, + IfControlStateService, ], host: { '[class.clr-form-control-disabled]': 'isInputDateDisabled', @@ -95,9 +103,14 @@ import { ViewManagerService } from './providers/view-manager.service'; export class ClrDateContainer implements DynamicWrapper, OnDestroy, AfterViewInit { _dynamic = false; invalid = false; + showHelper = false; focus = false; + valid = false; + state: CONTROL_STATE; control: NgControl; @ContentChild(ClrLabel) label: ClrLabel; + @ContentChild(ClrControlSuccess) controlSuccessComponent: ClrControlSuccess; + @Input('clrPosition') set clrPosition(position: string) { if (position && (ClrPopoverPositions as Record)[position]) { @@ -126,20 +139,26 @@ export class ClrDateContainer implements DynamicWrapper, OnDestroy, AfterViewIni private datepickerEnabledService: DatepickerEnabledService, private dateFormControlService: DateFormControlService, public commonStrings: ClrCommonStringsService, - private ifErrorService: IfErrorService, private focusService: FocusService, private viewManagerService: ViewManagerService, private controlClassService: ControlClassService, @Optional() private layoutService: LayoutService, - private ngControlService: NgControlService + private ngControlService: NgControlService, + private ifControlStateService: IfControlStateService ) { this.subscriptions.push( this.focusService.focusChange.subscribe(state => { this.focus = state; - }), + }) + ); + + this.subscriptions.push( this.ngControlService.controlChanges.subscribe(control => { this.control = control; - }), + }) + ); + + this.subscriptions.push( this.toggleService.openChange.subscribe(() => { this.dateFormControlService.markAsTouched(); }) @@ -148,8 +167,11 @@ export class ClrDateContainer implements DynamicWrapper, OnDestroy, AfterViewIni ngOnInit() { this.subscriptions.push( - this.ifErrorService.statusChanges.subscribe(invalid => { - this.invalid = invalid; + this.ifControlStateService.statusChanges.subscribe((state: CONTROL_STATE) => { + this.state = state; + this.valid = CONTROL_STATE.VALID === state; + this.invalid = CONTROL_STATE.INVALID === state; + this.showHelper = CONTROL_STATE.NONE === state; }) ); } @@ -170,7 +192,7 @@ export class ClrDateContainer implements DynamicWrapper, OnDestroy, AfterViewIni * Returns the classes to apply to the control */ controlClass() { - return this.controlClassService.controlClass(this.invalid, this.addGrid()); + return this.controlClassService.controlClass(this.state, this.addGrid()); } /** @@ -208,6 +230,6 @@ export class ClrDateContainer implements DynamicWrapper, OnDestroy, AfterViewIni * Unsubscribe from subscriptions. */ ngOnDestroy() { - this.subscriptions.map(sub => sub.unsubscribe()); + this.subscriptions.forEach(subscription => subscription.unsubscribe()); } } diff --git a/packages/angular/projects/clr-angular/src/forms/datepicker/date-input.spec.ts b/packages/angular/projects/clr-angular/src/forms/datepicker/date-input.spec.ts index 3582609e36..b314ed9f7a 100644 --- a/packages/angular/projects/clr-angular/src/forms/datepicker/date-input.spec.ts +++ b/packages/angular/projects/clr-angular/src/forms/datepicker/date-input.spec.ts @@ -12,7 +12,6 @@ import { By } from '@angular/platform-browser'; import { TestContext } from '../../data/datagrid/helpers.spec'; import { ClrFormsModule } from '../../forms/forms.module'; import { ClrPopoverToggleService } from '../../utils/popover/providers/popover-toggle.service'; -import { IfErrorService } from '../common/if-error/if-error.service'; import { ControlClassService } from '../common/providers/control-class.service'; import { ControlIdService } from '../common/providers/control-id.service'; import { FocusService } from '../common/providers/focus.service'; @@ -32,6 +31,7 @@ import { ViewManagerService } from './providers/view-manager.service'; import { ClrPopoverEventsService } from '../../utils/popover/providers/popover-events.service'; import { ClrPopoverPositionService } from '../../utils/popover/providers/popover-position.service'; import { LayoutService } from '../common/providers/layout.service'; +import { IfControlStateService } from '../common/if-control-state/if-control-state.service'; export default function () { describe('Date Input Component', () => { @@ -40,10 +40,10 @@ export default function () { let dateIOService: DateIOService; let dateNavigationService: DateNavigationService; let dateFormControlService: DateFormControlService; - let ifErrorService: IfErrorService; let focusService: FocusService; let controlClassService: ControlClassService; let datepickerFocusService: DatepickerFocusService; + let ifControlStateService: IfControlStateService; const setControlSpy = jasmine.createSpy(); @Injectable() @@ -57,7 +57,7 @@ export default function () { { provide: NgControlService, useClass: MockNgControlService }, NgControl, LayoutService, - IfErrorService, + IfControlStateService, ClrPopoverToggleService, ClrPopoverEventsService, ClrPopoverPositionService, @@ -88,12 +88,12 @@ export default function () { dateNavigationService = context.fixture.debugElement .query(By.directive(ClrDateContainer)) .injector.get(DateNavigationService); - ifErrorService = context.fixture.debugElement.injector.get(IfErrorService); + ifControlStateService = context.fixture.debugElement.injector.get(IfControlStateService); focusService = context.fixture.debugElement.injector.get(FocusService); controlClassService = context.fixture.debugElement.injector.get(ControlClassService); datepickerFocusService = context.fixture.debugElement.injector.get(DatepickerFocusService); - spyOn(ifErrorService, 'triggerStatusChange'); + spyOn(ifControlStateService, 'triggerStatusChange'); spyOn(datepickerFocusService, 'focusInput'); }); @@ -127,7 +127,7 @@ export default function () { context.clarityElement.dispatchEvent(new Event('input')); context.clarityElement.dispatchEvent(new Event('blur')); context.detectChanges(); - expect(ifErrorService.triggerStatusChange).toHaveBeenCalled(); + expect(ifControlStateService.triggerStatusChange).toHaveBeenCalled(); expect(focusState).toEqual(false); sub.unsubscribe(); }); diff --git a/packages/angular/projects/clr-angular/src/forms/input/input-container.spec.ts b/packages/angular/projects/clr-angular/src/forms/input/input-container.spec.ts index 5e89f90f52..e4d9061310 100644 --- a/packages/angular/projects/clr-angular/src/forms/input/input-container.spec.ts +++ b/packages/angular/projects/clr-angular/src/forms/input/input-container.spec.ts @@ -18,6 +18,7 @@ import { TemplateDrivenSpec, ReactiveSpec, ContainerNoLabelSpec } from '../tests Helper text Must be at least 5 characters + Valid `, }) @@ -42,6 +43,7 @@ class NoLabelTest { Helper text Must be at least 5 characters + Valid `, }) diff --git a/packages/angular/projects/clr-angular/src/forms/input/input-container.ts b/packages/angular/projects/clr-angular/src/forms/input/input-container.ts index e745203e95..faf81f1dac 100644 --- a/packages/angular/projects/clr-angular/src/forms/input/input-container.ts +++ b/packages/angular/projects/clr-angular/src/forms/input/input-container.ts @@ -4,13 +4,14 @@ * The full license information can be found in LICENSE in the root directory of this project. */ -import { Component } from '@angular/core'; +import { Component, ContentChild } from '@angular/core'; -import { IfErrorService } from '../common/if-error/if-error.service'; import { NgControlService } from '../common/providers/ng-control.service'; import { ControlIdService } from '../common/providers/control-id.service'; import { ControlClassService } from '../common/providers/control-class.service'; import { ClrAbstractContainer } from '../common/abstract-container'; +import { ClrControlSuccess } from '../common/success'; +import { IfControlStateService } from '../common/if-control-state/if-control-state.service'; @Component({ selector: 'clr-input-container', @@ -20,10 +21,22 @@ import { ClrAbstractContainer } from '../common/abstract-container';
- + +
- - + + +
`, host: { @@ -31,6 +44,8 @@ import { ClrAbstractContainer } from '../common/abstract-container'; '[class.clr-form-control-disabled]': 'control?.disabled', '[class.clr-row]': 'addGrid()', }, - providers: [IfErrorService, NgControlService, ControlIdService, ControlClassService], + providers: [IfControlStateService, NgControlService, ControlIdService, ControlClassService], }) -export class ClrInputContainer extends ClrAbstractContainer {} +export class ClrInputContainer extends ClrAbstractContainer { + @ContentChild(ClrControlSuccess) controlSuccessComponent: ClrControlSuccess; +} diff --git a/packages/angular/projects/clr-angular/src/forms/password/password-container.spec.ts b/packages/angular/projects/clr-angular/src/forms/password/password-container.spec.ts index 59dbe09f36..b4962b50e6 100644 --- a/packages/angular/projects/clr-angular/src/forms/password/password-container.spec.ts +++ b/packages/angular/projects/clr-angular/src/forms/password/password-container.spec.ts @@ -10,13 +10,12 @@ import { By } from '@angular/platform-browser'; import { ClrIconModule } from '../../icon/icon.module'; import { ClrCommonFormsModule } from '../common/common.module'; -import { IfErrorService } from '../common/if-error/if-error.service'; - import { ClrPassword } from './password'; import { ClrPasswordContainer } from './password-container'; import { NgControlService } from '../common/providers/ng-control.service'; import { LayoutService } from '../common/providers/layout.service'; import { ContainerNoLabelSpec, ReactiveSpec, TemplateDrivenSpec } from '../tests/container.spec'; +import { IfControlStateService } from '../common/if-control-state/if-control-state.service'; @Component({ template: ` @@ -25,6 +24,7 @@ import { ContainerNoLabelSpec, ReactiveSpec, TemplateDrivenSpec } from '../tests Helper text Must be at least 5 characters + Valid `, }) @@ -48,6 +48,7 @@ class NoLabelTest {} Helper text Must be at least 5 characters + Valid `, }) @@ -70,7 +71,7 @@ export default function (): void { TestBed.configureTestingModule({ imports: [ClrIconModule, ClrCommonFormsModule, FormsModule], declarations: [ClrPasswordContainer, ClrPassword, TemplateDrivenTest], - providers: [NgControl, NgControlService, IfErrorService, LayoutService], + providers: [IfControlStateService, NgControl, NgControlService, LayoutService], }); fixture = TestBed.createComponent(TemplateDrivenTest); diff --git a/packages/angular/projects/clr-angular/src/forms/password/password-container.ts b/packages/angular/projects/clr-angular/src/forms/password/password-container.ts index 0e387884ae..66f2468b42 100644 --- a/packages/angular/projects/clr-angular/src/forms/password/password-container.ts +++ b/packages/angular/projects/clr-angular/src/forms/password/password-container.ts @@ -4,10 +4,9 @@ * The full license information can be found in LICENSE in the root directory of this project. */ -import { Component, Inject, InjectionToken, Input, Optional } from '@angular/core'; +import { Component, Inject, InjectionToken, Input, Optional, ContentChild } from '@angular/core'; import { BehaviorSubject } from 'rxjs'; -import { IfErrorService } from '../common/if-error/if-error.service'; import { ControlClassService } from '../common/providers/control-class.service'; import { ControlIdService } from '../common/providers/control-id.service'; import { FocusService } from '../common/providers/focus.service'; @@ -15,6 +14,8 @@ import { LayoutService } from '../common/providers/layout.service'; import { NgControlService } from '../common/providers/ng-control.service'; import { ClrCommonStringsService } from '../../utils/i18n/common-strings.service'; import { ClrAbstractContainer } from '../common/abstract-container'; +import { ClrControlSuccess } from '../common/success'; +import { IfControlStateService } from '../common/if-control-state/if-control-state.service'; export const TOGGLE_SERVICE = new InjectionToken>(undefined); export function ToggleServiceFactory() { @@ -44,10 +45,22 @@ export const TOGGLE_SERVICE_PROVIDER = { provide: TOGGLE_SERVICE, useFactory: To > - + + - - + + + `, host: { @@ -56,12 +69,12 @@ export const TOGGLE_SERVICE_PROVIDER = { provide: TOGGLE_SERVICE, useFactory: To '[class.clr-row]': 'addGrid()', }, providers: [ - IfErrorService, NgControlService, ControlIdService, ControlClassService, FocusService, TOGGLE_SERVICE_PROVIDER, + IfControlStateService, ], }) export class ClrPasswordContainer extends ClrAbstractContainer { @@ -69,6 +82,8 @@ export class ClrPasswordContainer extends ClrAbstractContainer { focus = false; private _toggle = true; + @ContentChild(ClrControlSuccess) controlSuccessComponent: ClrControlSuccess; + @Input('clrToggle') set clrToggle(state: boolean) { this._toggle = state; @@ -81,7 +96,7 @@ export class ClrPasswordContainer extends ClrAbstractContainer { } constructor( - ifErrorService: IfErrorService, + ifControlStateService: IfControlStateService, @Optional() layoutService: LayoutService, controlClassService: ControlClassService, ngControlService: NgControlService, @@ -89,7 +104,9 @@ export class ClrPasswordContainer extends ClrAbstractContainer { @Inject(TOGGLE_SERVICE) private toggleService: BehaviorSubject, public commonStrings: ClrCommonStringsService ) { - super(ifErrorService, layoutService, controlClassService, ngControlService); + super(ifControlStateService, layoutService, controlClassService, ngControlService); + + /* The unsubscribe is handle inside the ClrAbstractContainer */ this.subscriptions.push( this.focusService.focusChange.subscribe(state => { this.focus = state; diff --git a/packages/angular/projects/clr-angular/src/forms/password/password.spec.ts b/packages/angular/projects/clr-angular/src/forms/password/password.spec.ts index a2aca107d9..8ac6fffee5 100644 --- a/packages/angular/projects/clr-angular/src/forms/password/password.spec.ts +++ b/packages/angular/projects/clr-angular/src/forms/password/password.spec.ts @@ -13,9 +13,9 @@ import { ReactiveSpec, TemplateDrivenSpec } from '../tests/control.spec'; import { TestBed } from '@angular/core/testing'; import { LayoutService } from '../common/providers/layout.service'; import { ClrIconModule } from '../../icon/icon.module'; -import { IfErrorService } from '../common/if-error/if-error.service'; import { ClrCommonFormsModule } from '../common/common.module'; import { NgControlService } from '../common/providers/ng-control.service'; +import { IfControlStateService } from '../common/if-control-state/if-control-state.service'; @Component({ template: ` `, @@ -26,6 +26,7 @@ class InvalidUseTest {} template: ` + Valid `, }) @@ -36,6 +37,7 @@ class TemplateDrivenTest {}
+ Valid
`, @@ -69,7 +71,7 @@ export default function (): void { TestBed.configureTestingModule({ imports: [ClrIconModule, ClrCommonFormsModule, FormsModule], declarations: [ClrPasswordContainer, ClrPassword, TemplateDrivenTest], - providers: [NgControl, NgControlService, IfErrorService, LayoutService], + providers: [IfControlStateService, NgControl, NgControlService, LayoutService], }); fixture = TestBed.createComponent(TemplateDrivenTest); containerDE = fixture.debugElement.query(By.directive(ClrPasswordContainer)); diff --git a/packages/angular/projects/clr-angular/src/forms/radio/radio-container.spec.ts b/packages/angular/projects/clr-angular/src/forms/radio/radio-container.spec.ts index 9d50dc86a2..6699a2103f 100644 --- a/packages/angular/projects/clr-angular/src/forms/radio/radio-container.spec.ts +++ b/packages/angular/projects/clr-angular/src/forms/radio/radio-container.spec.ts @@ -10,7 +10,6 @@ import { By } from '@angular/platform-browser'; import { ClrIconModule } from '../../icon/icon.module'; import { ClrCommonFormsModule } from '../common/common.module'; -import { IfErrorService } from '../common/if-error/if-error.service'; import { NgControlService } from '../common/providers/ng-control.service'; import { LayoutService } from '../common/providers/layout.service'; @@ -19,6 +18,7 @@ import { ClrRadioContainer } from './radio-container'; import { ClrRadioWrapper } from './radio-wrapper'; import { ContainerNoLabelSpec, TemplateDrivenSpec, ReactiveSpec } from '../tests/container.spec'; +import { IfControlStateService } from '../common/if-control-state/if-control-state.service'; @Component({ template: ` `, @@ -39,6 +39,7 @@ class NoLabelTest {} There was an error Helper text + Valid `, }) @@ -62,6 +63,7 @@ class TemplateDrivenTest { There was an error Helper text + Valid `, }) @@ -89,7 +91,7 @@ export default function (): void { TestBed.configureTestingModule({ imports: [ClrIconModule, ClrCommonFormsModule, FormsModule], declarations: [ClrRadioContainer, ClrRadioWrapper, ClrRadio, TemplateDrivenTest], - providers: [NgControl, NgControlService, IfErrorService, LayoutService], + providers: [IfControlStateService, NgControl, NgControlService, LayoutService], }); fixture = TestBed.createComponent(TemplateDrivenTest); diff --git a/packages/angular/projects/clr-angular/src/forms/radio/radio-container.ts b/packages/angular/projects/clr-angular/src/forms/radio/radio-container.ts index b4f9c3a428..a27c03536b 100644 --- a/packages/angular/projects/clr-angular/src/forms/radio/radio-container.ts +++ b/packages/angular/projects/clr-angular/src/forms/radio/radio-container.ts @@ -4,13 +4,14 @@ * The full license information can be found in LICENSE in the root directory of this project. */ -import { Component, Input, Optional } from '@angular/core'; +import { Component, Input, Optional, ContentChild } from '@angular/core'; -import { IfErrorService } from '../common/if-error/if-error.service'; import { ControlClassService } from '../common/providers/control-class.service'; import { NgControlService } from '../common/providers/ng-control.service'; import { ClrAbstractContainer } from '../common/abstract-container'; import { LayoutService } from '../common/providers/layout.service'; +import { ClrControlSuccess } from '../common/success'; +import { IfControlStateService } from '../common/if-control-state/if-control-state.service'; @Component({ selector: 'clr-radio-container', @@ -20,9 +21,21 @@ import { LayoutService } from '../common/providers/layout.service';
- - - + + + + +
`, @@ -31,18 +44,19 @@ import { LayoutService } from '../common/providers/layout.service'; '[class.clr-form-control-disabled]': 'control?.disabled', '[class.clr-row]': 'addGrid()', }, - providers: [NgControlService, ControlClassService, IfErrorService], + providers: [NgControlService, IfControlStateService, ControlClassService], }) export class ClrRadioContainer extends ClrAbstractContainer { private inline = false; + @ContentChild(ClrControlSuccess) controlSuccessComponent: ClrControlSuccess; constructor( - protected ifErrorService: IfErrorService, @Optional() protected layoutService: LayoutService, protected controlClassService: ControlClassService, - protected ngControlService: NgControlService + protected ngControlService: NgControlService, + protected ifControlStateService: IfControlStateService ) { - super(ifErrorService, layoutService, controlClassService, ngControlService); + super(ifControlStateService, layoutService, controlClassService, ngControlService); } /* diff --git a/packages/angular/projects/clr-angular/src/forms/range/range-container.spec.ts b/packages/angular/projects/clr-angular/src/forms/range/range-container.spec.ts index a8778debed..7d35aa4961 100644 --- a/packages/angular/projects/clr-angular/src/forms/range/range-container.spec.ts +++ b/packages/angular/projects/clr-angular/src/forms/range/range-container.spec.ts @@ -18,6 +18,7 @@ import { TemplateDrivenSpec, ReactiveSpec, ContainerNoLabelSpec } from '../tests Helper text Must be at least 5 characters + Valid `, }) @@ -40,6 +41,7 @@ class NoLabelTest {} Helper text Must be at least 5 characters + Valid `, }) diff --git a/packages/angular/projects/clr-angular/src/forms/range/range-container.ts b/packages/angular/projects/clr-angular/src/forms/range/range-container.ts index 5dab1cc120..c2d11d47dc 100644 --- a/packages/angular/projects/clr-angular/src/forms/range/range-container.ts +++ b/packages/angular/projects/clr-angular/src/forms/range/range-container.ts @@ -4,14 +4,15 @@ * The full license information can be found in LICENSE in the root directory of this project. */ -import { Component, Input, Optional, Renderer2 } from '@angular/core'; +import { Component, Input, Optional, Renderer2, ContentChild } from '@angular/core'; -import { IfErrorService } from '../common/if-error/if-error.service'; import { NgControlService } from '../common/providers/ng-control.service'; import { LayoutService } from '../common/providers/layout.service'; import { ControlIdService } from '../common/providers/control-id.service'; import { ControlClassService } from '../common/providers/control-class.service'; import { ClrAbstractContainer } from '../common/abstract-container'; +import { ClrControlSuccess } from '../common/success'; +import { IfControlStateService } from '../common/if-control-state/if-control-state.service'; @Component({ selector: 'clr-range-container', @@ -22,10 +23,22 @@ import { ClrAbstractContainer } from '../common/abstract-container';
- + +
- - + + + `, host: { @@ -33,11 +46,13 @@ import { ClrAbstractContainer } from '../common/abstract-container'; '[class.clr-form-control-disabled]': 'control?.disabled', '[class.clr-row]': 'addGrid()', }, - providers: [IfErrorService, NgControlService, ControlIdService, ControlClassService], + providers: [IfControlStateService, NgControlService, ControlIdService, ControlClassService], }) export class ClrRangeContainer extends ClrAbstractContainer { private _hasProgress = false; + @ContentChild(ClrControlSuccess) controlSuccessComponent: ClrControlSuccess; + @Input('clrRangeHasProgress') set hasProgress(val: boolean) { const valBool = !!val; @@ -51,14 +66,14 @@ export class ClrRangeContainer extends ClrAbstractContainer { } constructor( - ifErrorService: IfErrorService, @Optional() layoutService: LayoutService, controlClassService: ControlClassService, ngControlService: NgControlService, private renderer: Renderer2, - private idService: ControlIdService + private idService: ControlIdService, + protected ifControlStateService: IfControlStateService ) { - super(ifErrorService, layoutService, controlClassService, ngControlService); + super(ifControlStateService, layoutService, controlClassService, ngControlService); } getRangeProgressFillWidth(): string { diff --git a/packages/angular/projects/clr-angular/src/forms/select/select-container.spec.ts b/packages/angular/projects/clr-angular/src/forms/select/select-container.spec.ts index 8fe82f61b6..1460beded5 100644 --- a/packages/angular/projects/clr-angular/src/forms/select/select-container.spec.ts +++ b/packages/angular/projects/clr-angular/src/forms/select/select-container.spec.ts @@ -31,6 +31,7 @@ class NoLabelTest {} Helper text Must be at least 5 characters + Valid `, }) @@ -49,6 +50,7 @@ class TemplateDrivenTest { Helper text Must be at least 5 characters + Valid `, }) @@ -67,6 +69,7 @@ class TemplateDrivenMultipleTest { Helper text Must be at least 5 characters + Valid `, }) @@ -87,6 +90,7 @@ class ReactiveTest { Helper text Must be at least 5 characters + Valid `, }) diff --git a/packages/angular/projects/clr-angular/src/forms/select/select-container.ts b/packages/angular/projects/clr-angular/src/forms/select/select-container.ts index e5ed0bb837..6e953af16a 100644 --- a/packages/angular/projects/clr-angular/src/forms/select/select-container.ts +++ b/packages/angular/projects/clr-angular/src/forms/select/select-container.ts @@ -7,12 +7,13 @@ import { Component, ContentChild, Optional } from '@angular/core'; import { SelectMultipleControlValueAccessor } from '@angular/forms'; -import { IfErrorService } from '../common/if-error/if-error.service'; import { NgControlService } from '../common/providers/ng-control.service'; import { ControlIdService } from '../common/providers/control-id.service'; import { ControlClassService } from '../common/providers/control-class.service'; import { ClrAbstractContainer } from '../common/abstract-container'; import { LayoutService } from '../common/providers/layout.service'; +import { ClrControlSuccess } from '../common/success'; +import { IfControlStateService } from '../common/if-control-state/if-control-state.service'; @Component({ selector: 'clr-select-container', @@ -22,10 +23,22 @@ import { LayoutService } from '../common/providers/layout.service';
- + +
- - + + +
`, host: { @@ -33,23 +46,27 @@ import { LayoutService } from '../common/providers/layout.service'; '[class.clr-form-control-disabled]': 'control?.disabled', '[class.clr-row]': 'addGrid()', }, - providers: [IfErrorService, NgControlService, ControlIdService, ControlClassService], + providers: [IfControlStateService, NgControlService, ControlIdService, ControlClassService], }) export class ClrSelectContainer extends ClrAbstractContainer { + @ContentChild(ClrControlSuccess) + controlSuccessComponent: ClrControlSuccess; + @ContentChild(SelectMultipleControlValueAccessor, { static: false }) multiple: SelectMultipleControlValueAccessor; private multi = false; constructor( - protected ifErrorService: IfErrorService, @Optional() protected layoutService: LayoutService, protected controlClassService: ControlClassService, - protected ngControlService: NgControlService + protected ngControlService: NgControlService, + protected ifControlStateService: IfControlStateService ) { - super(ifErrorService, layoutService, controlClassService, ngControlService); + super(ifControlStateService, layoutService, controlClassService, ngControlService); } ngOnInit() { + /* The unsubscribe is handle inside the ClrAbstractContainer */ this.subscriptions.push( this.ngControlService.controlChanges.subscribe(control => { if (control) { diff --git a/packages/angular/projects/clr-angular/src/forms/styles/_containers.clarity.scss b/packages/angular/projects/clr-angular/src/forms/styles/_containers.clarity.scss index 4b529645a0..64a5eb47e7 100644 --- a/packages/angular/projects/clr-angular/src/forms/styles/_containers.clarity.scss +++ b/packages/angular/projects/clr-angular/src/forms/styles/_containers.clarity.scss @@ -64,6 +64,20 @@ margin-left: -1 * $clr_baselineRem_1; } + .clr-success { + .clr-input { + @include css-var(border-bottom-color, clr-forms-valid-color, $clr-forms-valid-color, $clr-use-custom-properties); + } + .clr-validate-icon { + display: inline-block; + @include css-var(color, clr-forms-valid-color, $clr-forms-valid-color, $clr-use-custom-properties); + margin-left: -0.2rem; + } + .clr-subtext { + @include css-var(color, clr-forms-valid-text-color, $clr-forms-valid-text-color, $clr-use-custom-properties); + } + } + .clr-error { .clr-validate-icon { margin-left: -1 * $clr_baselineRem_4px; // @TODO Figure out why there is this 4px gap between elements @@ -122,7 +136,8 @@ margin-left: $clr-forms-icon-size; } - .clr-error { + .clr-error, + .clr-success { .clr-subtext { margin-left: 0rem; } diff --git a/packages/angular/projects/clr-angular/src/forms/styles/_properties.forms.scss b/packages/angular/projects/clr-angular/src/forms/styles/_properties.forms.scss index 18fc241529..7dfc926939 100644 --- a/packages/angular/projects/clr-angular/src/forms/styles/_properties.forms.scss +++ b/packages/angular/projects/clr-angular/src/forms/styles/_properties.forms.scss @@ -12,6 +12,8 @@ --clr-forms-label-color: var(--clr-color-neutral-800); --clr-forms-text-color: var(--clr-color-neutral-1000); --clr-forms-invalid-color: var(--clr-color-danger-800); + --clr-forms-valid-color: var(--clr-color-success-700); + --clr-forms-valid-text-color: var(--clr-color-success-900); --clr-forms-subtext-color: var(--clr-color-neutral-600); --clr-forms-border-color: var(--clr-color-neutral-500); --clr-forms-focused-color: var(--clr-color-action-600); diff --git a/packages/angular/projects/clr-angular/src/forms/styles/_variables.forms.scss b/packages/angular/projects/clr-angular/src/forms/styles/_variables.forms.scss index ebaaee0d00..ae805ae194 100644 --- a/packages/angular/projects/clr-angular/src/forms/styles/_variables.forms.scss +++ b/packages/angular/projects/clr-angular/src/forms/styles/_variables.forms.scss @@ -11,6 +11,8 @@ $clr-form-disabled-background-color: $clr-color-neutral-400 !default; $clr-forms-label-color: $clr-color-neutral-800 !default; $clr-forms-text-color: $clr-color-neutral-1000 !default; $clr-forms-invalid-color: $clr-color-danger-800 !default; +$clr-forms-valid-color: $clr-color-success-700 !default; +$clr-forms-valid-text-color: $clr-color-success-900 !default; $clr-forms-subtext-color: $clr-color-neutral-600 !default; $clr-forms-border-color: $clr-color-neutral-500 !default; $clr-forms-focused-color: $clr-color-action-600 !default; diff --git a/packages/angular/projects/clr-angular/src/forms/tests/container.spec.ts b/packages/angular/projects/clr-angular/src/forms/tests/container.spec.ts index 7202953449..d6cc813c4c 100644 --- a/packages/angular/projects/clr-angular/src/forms/tests/container.spec.ts +++ b/packages/angular/projects/clr-angular/src/forms/tests/container.spec.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2019 VMware, Inc. All Rights Reserved. + * Copyright (c) 2016-2020 VMware, Inc. All Rights Reserved. * This software is released under MIT license. * The full license information can be found in LICENSE in the root directory of this project. */ @@ -9,13 +9,13 @@ import { By } from '@angular/platform-browser'; import { ClrIconModule } from '../../icon/icon.module'; import { ClrCommonFormsModule } from '../common/common.module'; -import { IfErrorService } from '../common/if-error/if-error.service'; import { NgControlService } from '../common/providers/ng-control.service'; import { Layouts, LayoutService } from '../common/providers/layout.service'; import { MarkControlService } from '../common/providers/mark-control.service'; import { ControlIdService } from '../common/providers/control-id.service'; import { DatalistIdService } from '../datalist/providers/datalist-id.service'; +import { IfControlStateService, CONTROL_STATE } from '../common/if-control-state/if-control-state.service'; export function ContainerNoLabelSpec(testContainer, testControl, testComponent): void { describe('no label', () => { @@ -24,7 +24,7 @@ export function ContainerNoLabelSpec(testContainer, testControl, testComponent): TestBed.configureTestingModule({ imports: [ClrIconModule, ClrCommonFormsModule, FormsModule], declarations: [testContainer, testControl, testComponent], - providers: [NgControl, NgControlService, IfErrorService, LayoutService, MarkControlService], + providers: [NgControl, NgControlService, IfControlStateService, LayoutService, MarkControlService], }); fixture = TestBed.createComponent(testComponent); @@ -58,7 +58,13 @@ export function ReactiveSpec(testContainer, testControl, testComponent, wrapperC function fullSpec(description, testContainer, directives: any | any[], testComponent, wrapperClass) { describe(description, () => { - let fixture, containerDE, container, containerEl, ifErrorService, layoutService, markControlService; + let fixture, + containerDE, + container, + containerEl, + ifControlStateService: IfControlStateService, + layoutService, + markControlService; if (!Array.isArray(directives)) { directives = [directives]; } @@ -69,11 +75,11 @@ function fullSpec(description, testContainer, directives: any | any[], testCompo providers: [ NgControl, NgControlService, - IfErrorService, LayoutService, MarkControlService, ControlIdService, DatalistIdService, + IfControlStateService, ], }); fixture = TestBed.createComponent(testComponent); @@ -81,7 +87,7 @@ function fullSpec(description, testContainer, directives: any | any[], testCompo containerDE = fixture.debugElement.query(By.directive(testContainer)); container = containerDE.componentInstance; containerEl = containerDE.nativeElement; - ifErrorService = containerDE.injector.get(IfErrorService); + ifControlStateService = containerDE.injector.get(IfControlStateService); markControlService = containerDE.injector.get(MarkControlService); layoutService = containerDE.injector.get(LayoutService); fixture.detectChanges(); @@ -92,8 +98,8 @@ function fullSpec(description, testContainer, directives: any | any[], testCompo expect(layoutService.layout).toEqual(Layouts.HORIZONTAL); }); - it('injects the ifErrorService and subscribes', () => { - expect(ifErrorService).toBeTruthy(); + it('injects the IfControlStateService and subscribes', () => { + expect(ifControlStateService).toBeTruthy(); expect(container.subscriptions[0]).toBeTruthy(); }); @@ -113,7 +119,7 @@ function fullSpec(description, testContainer, directives: any | any[], testCompo it("doesn't display the helper text when invalid", () => { expect(containerEl.querySelector('clr-control-helper')).toBeTruthy(); - container.invalid = true; + container.state = CONTROL_STATE.INVALID; fixture.detectChanges(); expect(containerEl.querySelector('clr-control-helper')).toBeFalsy(); }); @@ -121,7 +127,7 @@ function fullSpec(description, testContainer, directives: any | any[], testCompo it('sets error classes and displays the icon when invalid', () => { expect(containerEl.querySelector('.clr-control-container').classList.contains('clr-error')).toBeFalse(); expect(containerEl.querySelector('.clr-validate-icon')).toBeFalsy(); - container.invalid = true; + container.state = CONTROL_STATE.INVALID; fixture.detectChanges(); expect(containerEl.querySelector('.clr-control-container').classList.contains('clr-error')).toBeTrue(); expect(containerEl.querySelector('.clr-validate-icon')).toBeTruthy(); @@ -129,11 +135,26 @@ function fullSpec(description, testContainer, directives: any | any[], testCompo it('projects the error helper when invalid', () => { expect(containerEl.querySelector('clr-control-error')).toBeFalsy(); - container.invalid = true; + container.state = CONTROL_STATE.INVALID; fixture.detectChanges(); expect(containerEl.querySelector('clr-control-error')).toBeTruthy(); }); + it('projects the success content when valid', () => { + container.state = CONTROL_STATE.VALID; + fixture.detectChanges(); + expect(containerEl.querySelector('clr-control-helper')).toBeFalsy(); + expect(containerEl.querySelector('clr-control-error')).toBeFalsy(); + expect(containerEl.querySelector('clr-control-success')).toBeTruthy(); + }); + + it('should have the success icon when valid', () => { + container.state = CONTROL_STATE.VALID; + fixture.detectChanges(); + const icon: HTMLElement = containerEl.querySelector('clr-icon[shape=check-circle]'); + expect(icon).toBeTruthy(); + }); + it('adds the .clr-form-control class to the host', () => { expect(containerEl.classList).toContain('clr-form-control'); }); @@ -150,7 +171,7 @@ function fullSpec(description, testContainer, directives: any | any[], testCompo it('adds the error class for the control container', () => { expect(container.controlClass()).not.toContain('clr-error'); - container.invalid = true; + container.state = CONTROL_STATE.INVALID; expect(container.controlClass()).toContain('clr-error'); }); @@ -161,10 +182,10 @@ function fullSpec(description, testContainer, directives: any | any[], testCompo }); it('tracks the validity of the form control', () => { - expect(container.invalid).toBeFalse(); + expect(container.showInvalid).toBeFalse(); markControlService.markAsTouched(); fixture.detectChanges(); - expect(container.invalid).toBeTrue(); + expect(container.showInvalid).toBeTrue(); }); it('tracks the disabled state', async(() => { diff --git a/packages/angular/projects/clr-angular/src/forms/tests/control.spec.ts b/packages/angular/projects/clr-angular/src/forms/tests/control.spec.ts index 9f3a9d196d..b306a8aea0 100644 --- a/packages/angular/projects/clr-angular/src/forms/tests/control.spec.ts +++ b/packages/angular/projects/clr-angular/src/forms/tests/control.spec.ts @@ -8,7 +8,6 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { By } from '@angular/platform-browser'; import { ClrIconModule } from '../../icon/icon.module'; -import { IfErrorService } from '../common/if-error/if-error.service'; import { NgControlService } from '../common/providers/ng-control.service'; import { ClrCommonFormsModule } from '../common/common.module'; @@ -18,6 +17,7 @@ import { ControlClassService } from '../common/providers/control-class.service'; import { MarkControlService } from '../common/providers/mark-control.service'; import { LayoutService } from '../common/providers/layout.service'; import { DatalistIdService } from '../datalist/providers/datalist-id.service'; +import { IfControlStateService } from '../common/if-control-state/if-control-state.service'; export function ControlStandaloneSpec(testComponent): void { describe('standalone use', () => { @@ -44,7 +44,13 @@ export function ReactiveSpec(testContainer, testControl, testComponent, controlC function fullTest(description, testContainer, testControl, testComponent, controlClass) { describe(description, () => { - let control, fixture, ifErrorService, controlClassService, markControlService, controlIdService, datalistIdService; + let control, + fixture, + ifControlStateService, + controlClassService, + markControlService, + controlIdService, + datalistIdService; beforeEach(() => { spyOn(WrappedFormControl.prototype, 'ngOnInit'); @@ -53,7 +59,7 @@ function fullTest(description, testContainer, testControl, testComponent, contro imports: [FormsModule, ClrIconModule, ClrCommonFormsModule, ReactiveFormsModule], declarations: [testContainer, testControl, testComponent], providers: [ - IfErrorService, + IfControlStateService, NgControlService, ControlIdService, ControlClassService, @@ -65,11 +71,11 @@ function fullTest(description, testContainer, testControl, testComponent, contro fixture = TestBed.createComponent(testComponent); control = fixture.debugElement.query(By.directive(testControl)); controlClassService = control.injector.get(ControlClassService); - ifErrorService = control.injector.get(IfErrorService); + ifControlStateService = control.injector.get(IfControlStateService); markControlService = control.injector.get(MarkControlService); controlIdService = control.injector.get(ControlIdService); datalistIdService = control.injector.get(DatalistIdService); - spyOn(ifErrorService, 'triggerStatusChange'); + spyOn(ifControlStateService, 'triggerStatusChange'); fixture.detectChanges(); }); @@ -85,8 +91,8 @@ function fullTest(description, testContainer, testControl, testComponent, contro expect(control.nativeElement.classList.contains(controlClass)); }); - it('should have the IfErrorService', () => { - expect(ifErrorService).toBeTruthy(); + it('should have the IfControlStateService', () => { + expect(ifControlStateService).toBeTruthy(); }); it('should have the MarkControlService', () => { @@ -110,7 +116,7 @@ function fullTest(description, testContainer, testControl, testComponent, contro control.nativeElement.dispatchEvent(new Event('input')); control.nativeElement.dispatchEvent(new Event('blur')); fixture.detectChanges(); - expect(ifErrorService.triggerStatusChange).toHaveBeenCalled(); + expect(ifControlStateService.triggerStatusChange).toHaveBeenCalled(); }); it('should have the MarkControlService', () => { diff --git a/packages/angular/projects/clr-angular/src/forms/tests/wrapper.spec.ts b/packages/angular/projects/clr-angular/src/forms/tests/wrapper.spec.ts index 591d866ab4..f32be22540 100644 --- a/packages/angular/projects/clr-angular/src/forms/tests/wrapper.spec.ts +++ b/packages/angular/projects/clr-angular/src/forms/tests/wrapper.spec.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2018 VMware, Inc. All Rights Reserved. + * Copyright (c) 2016-2020 VMware, Inc. All Rights Reserved. * This software is released under MIT license. * The full license information can be found in LICENSE in the root directory of this project. */ @@ -9,10 +9,10 @@ import { By } from '@angular/platform-browser'; import { ClrIconModule } from '../../icon/icon.module'; import { ClrCommonFormsModule } from '../common/common.module'; -import { IfErrorService } from '../common/if-error/if-error.service'; import { NgControlService } from '../common/providers/ng-control.service'; import { LayoutService } from '../common/providers/layout.service'; +import { IfControlStateService } from '../common/if-control-state/if-control-state.service'; export function WrapperNoLabelSpec(testContainer, testControl, testComponent): void { describe('no label', () => { @@ -21,7 +21,7 @@ export function WrapperNoLabelSpec(testContainer, testControl, testComponent): v TestBed.configureTestingModule({ imports: [ClrIconModule, ClrCommonFormsModule, FormsModule], declarations: [testContainer, testControl, testComponent], - providers: [NgControl, NgControlService, IfErrorService, LayoutService], + providers: [IfControlStateService, NgControl, NgControlService, LayoutService], }); fixture = TestBed.createComponent(testComponent); @@ -44,7 +44,7 @@ export function WrapperFullSpec(testContainer, testControl, testComponent, wrapp TestBed.configureTestingModule({ imports: [ClrIconModule, ClrCommonFormsModule, FormsModule], declarations: [testContainer, testControl, testComponent], - providers: [NgControl, NgControlService, IfErrorService, LayoutService], + providers: [IfControlStateService, NgControl, NgControlService, LayoutService], }); fixture = TestBed.createComponent(testComponent); @@ -82,7 +82,7 @@ export function WrapperContainerSpec(testContainer, testWrapper, testControl, te TestBed.configureTestingModule({ imports: [ClrIconModule, ClrCommonFormsModule, FormsModule], declarations: [testContainer, testWrapper, testControl, testComponent], - providers: [NgControl, NgControlService, IfErrorService, LayoutService], + providers: [IfControlStateService, NgControl, NgControlService, LayoutService], }); fixture = TestBed.createComponent(testComponent); diff --git a/packages/angular/projects/clr-angular/src/forms/textarea/textarea-container.spec.ts b/packages/angular/projects/clr-angular/src/forms/textarea/textarea-container.spec.ts index 34902f3a96..111e923d93 100644 --- a/packages/angular/projects/clr-angular/src/forms/textarea/textarea-container.spec.ts +++ b/packages/angular/projects/clr-angular/src/forms/textarea/textarea-container.spec.ts @@ -18,6 +18,7 @@ import { ContainerNoLabelSpec, TemplateDrivenSpec, ReactiveSpec } from '../tests Helper text Must be at least 5 characters + Valid `, }) @@ -40,6 +41,7 @@ class NoLabelTest {} Helper text Must be at least 5 characters + Valid `, }) diff --git a/packages/angular/projects/clr-angular/src/forms/textarea/textarea-container.ts b/packages/angular/projects/clr-angular/src/forms/textarea/textarea-container.ts index fe679a0a67..8032243172 100644 --- a/packages/angular/projects/clr-angular/src/forms/textarea/textarea-container.ts +++ b/packages/angular/projects/clr-angular/src/forms/textarea/textarea-container.ts @@ -4,13 +4,14 @@ * The full license information can be found in LICENSE in the root directory of this project. */ -import { Component } from '@angular/core'; +import { Component, ContentChild } from '@angular/core'; -import { IfErrorService } from '../common/if-error/if-error.service'; import { NgControlService } from '../common/providers/ng-control.service'; import { ControlIdService } from '../common/providers/control-id.service'; import { ControlClassService } from '../common/providers/control-class.service'; import { ClrAbstractContainer } from '../common/abstract-container'; +import { ClrControlSuccess } from '../common/success'; +import { IfControlStateService } from '../common/if-control-state/if-control-state.service'; @Component({ selector: 'clr-textarea-container', @@ -20,10 +21,22 @@ import { ClrAbstractContainer } from '../common/abstract-container';
- + +
- - + + +
`, host: { @@ -31,6 +44,8 @@ import { ClrAbstractContainer } from '../common/abstract-container'; '[class.clr-form-control-disabled]': 'control?.disabled', '[class.clr-row]': 'addGrid()', }, - providers: [IfErrorService, NgControlService, ControlIdService, ControlClassService], + providers: [IfControlStateService, NgControlService, ControlIdService, ControlClassService], }) -export class ClrTextareaContainer extends ClrAbstractContainer {} +export class ClrTextareaContainer extends ClrAbstractContainer { + @ContentChild(ClrControlSuccess) controlSuccessComponent: ClrControlSuccess; +} diff --git a/packages/angular/projects/clr-angular/src/utils/_overwrites.clarity.scss b/packages/angular/projects/clr-angular/src/utils/_overwrites.clarity.scss index d321462b77..1b188bd735 100644 --- a/packages/angular/projects/clr-angular/src/utils/_overwrites.clarity.scss +++ b/packages/angular/projects/clr-angular/src/utils/_overwrites.clarity.scss @@ -446,6 +446,8 @@ $clr-form-field-disabled-color: null; // $clr-color-neutral-600; $clr-forms-label-color: null; // $clr-color-neutral-800 $clr-forms-text-color: null; // $clr-color-neutral-1000 $clr-forms-invalid-color: null; // $clr-color-danger-800 +$clr-forms-valid-color: null; // $clr-color-success-700 +$clr-forms-valid-text-color: null; // $clr-color-success-900 $clr-forms-subtext-color: null; // $clr-color-neutral-600 $clr-forms-border-color: null; // $clr-color-neutral-500 $clr-forms-focused-color: null; // $clr-color-action-600 diff --git a/packages/angular/projects/clr-angular/src/utils/_theme.dark.clarity.scss b/packages/angular/projects/clr-angular/src/utils/_theme.dark.clarity.scss index 4f6fe2a55f..999b8d368d 100644 --- a/packages/angular/projects/clr-angular/src/utils/_theme.dark.clarity.scss +++ b/packages/angular/projects/clr-angular/src/utils/_theme.dark.clarity.scss @@ -415,6 +415,8 @@ $clr-calendar-active-cell-color: hsl(0, 0%, 100%); $clr-forms-label-color: hsl(203, 16%, 72%); $clr-forms-text-color: hsl(210, 16%, 93%); // No label, no wrapper $clr-forms-invalid-color: hsl(3, 90%, 62%); +$clr-forms-valid-color: hsl(92, 79%, 40%); +$clr-forms-valid-text-color: hsl(92, 79%, 49%); $clr-forms-subtext-color: hsl(203, 16%, 72%); $clr-forms-border-color: hsl(203, 16%, 72%); $clr-forms-focused-color: hsl(198, 65%, 57%); // Vertical no wrapper, no label diff --git a/packages/angular/projects/dev/src/app/forms/forms.demo.module.ts b/packages/angular/projects/dev/src/app/forms/forms.demo.module.ts index 223668d631..9e64d0389d 100644 --- a/packages/angular/projects/dev/src/app/forms/forms.demo.module.ts +++ b/packages/angular/projects/dev/src/app/forms/forms.demo.module.ts @@ -36,6 +36,7 @@ import { FormsA11yDemo } from './a11y/a11y'; import { FormsLayoutHorizontalAngularGridDemo } from './layout-angular/layout-horizontal-angular-grid'; import { FormsLayoutCompactAngularGridDemo } from './layout-angular/layout-compact-angular-grid'; import { FormsGenericContainerDemo } from './generic-container/generic-container'; +import { FormsValidationDemo } from './validation/validation'; @NgModule({ imports: [CommonModule, FormsModule, ReactiveFormsModule, ClarityModule, ROUTING], @@ -65,6 +66,7 @@ import { FormsGenericContainerDemo } from './generic-container/generic-container FormsResetDemo, FormsA11yDemo, FormsGenericContainerDemo, + FormsValidationDemo, ], exports: [ FormsDemo, @@ -90,6 +92,7 @@ import { FormsGenericContainerDemo } from './generic-container/generic-container FormsResetDemo, FormsA11yDemo, FormsGenericContainerDemo, + FormsValidationDemo, ], }) export class FormsDemoModule {} diff --git a/packages/angular/projects/dev/src/app/forms/forms.demo.routing.ts b/packages/angular/projects/dev/src/app/forms/forms.demo.routing.ts index c6f4c80c33..e5daa7270e 100644 --- a/packages/angular/projects/dev/src/app/forms/forms.demo.routing.ts +++ b/packages/angular/projects/dev/src/app/forms/forms.demo.routing.ts @@ -31,6 +31,7 @@ import { FormsA11yDemo } from './a11y/a11y'; import { FormsLayoutCompactAngularGridDemo } from './layout-angular/layout-compact-angular-grid'; import { FormsLayoutHorizontalAngularGridDemo } from './layout-angular/layout-horizontal-angular-grid'; import { FormsGenericContainerDemo } from './generic-container/generic-container'; +import { FormsValidationDemo } from './validation/validation'; const ROUTES: Routes = [ { @@ -62,6 +63,7 @@ const ROUTES: Routes = [ { path: 'reset', component: FormsResetDemo }, { path: 'a11y', component: FormsA11yDemo }, { path: 'generic-container', component: FormsGenericContainerDemo }, + { path: 'validation', component: FormsValidationDemo }, ], }, ]; diff --git a/packages/angular/projects/dev/src/app/forms/forms.demo.ts b/packages/angular/projects/dev/src/app/forms/forms.demo.ts index 5341bcb92c..b0b2eac9dd 100644 --- a/packages/angular/projects/dev/src/app/forms/forms.demo.ts +++ b/packages/angular/projects/dev/src/app/forms/forms.demo.ts @@ -42,6 +42,7 @@ import { Component } from '@angular/core';
  • Reset
  • a11y
  • Generic Container
  • +
  • Validation
  • `, diff --git a/packages/angular/projects/dev/src/app/forms/reset/reset.html b/packages/angular/projects/dev/src/app/forms/reset/reset.html index 5d4d1ce922..c8f0d5f883 100644 --- a/packages/angular/projects/dev/src/app/forms/reset/reset.html +++ b/packages/angular/projects/dev/src/app/forms/reset/reset.html @@ -1,5 +1,5 @@ @@ -14,6 +14,7 @@

    Reset and Validation

    This is a required field Must be at least 5 characters It must match 'asdfasdf' + That's correct the input must be 'asdfasdf' diff --git a/packages/angular/projects/dev/src/app/forms/validation/validation.html b/packages/angular/projects/dev/src/app/forms/validation/validation.html new file mode 100644 index 0000000000..e4d997be54 --- /dev/null +++ b/packages/angular/projects/dev/src/app/forms/validation/validation.html @@ -0,0 +1,121 @@ + + +

    Validation

    + + +
    + + + + Helper text + This is a required field + Must be at least 5 characters + It must match 'asdfasdf' + That's correct the input must be 'asdfasdf' + + + + + + Helper text + This is a required field + Must be at least 5 characters + It must match 'asdfasdf' + That's correct the input must be 'asdfasdf' + + + + + + Helper text + This is a required field + Must be at least 5 characters + It must match 'asdfasdf' + That's correct the input must be 'asdfasdf' + + + + + + + + + + + + + Helper text + This field is required! + Good job, mate! + + + + + + + + + + + + + Helper text + This field is required! + Good job, mate! + + + + + + Helper text + This field is required! + Good job, mate! + + + + + + Helper text + This field is required! + Good job, mate! + + + + + + + + + Helper text + This field is required! + Good job, mate! + +
    +
    + + + + +

    Vertical

    +
    + +
    + +

    Horizontal

    +
    + +
    + +

    Compact

    +
    + +
    +No newline at end of file diff --git a/packages/angular/projects/dev/src/app/forms/validation/validation.ts b/packages/angular/projects/dev/src/app/forms/validation/validation.ts new file mode 100644 index 0000000000..8352dbcf61 --- /dev/null +++ b/packages/angular/projects/dev/src/app/forms/validation/validation.ts @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2016-2020 VMware, Inc. All Rights Reserved. + * This software is released under MIT license. + * The full license information can be found in LICENSE in the root directory of this project. + */ +import '@clr/icons/shapes/social-shapes'; +import '@clr/icons/shapes/essential-shapes'; +import { Component, ViewChildren } from '@angular/core'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { ClrForm } from '@clr/angular'; + +@Component({ templateUrl: './validation.html' }) +export class FormsValidationDemo { + @ViewChildren(ClrForm) forms: ClrForm[]; + + items = ['one', 'two']; + + model = new FormGroup({ + input: new FormControl('', [Validators.required, Validators.minLength(6), Validators.pattern(/asdfasdf/)]), + textarea: new FormControl('', [Validators.required, Validators.minLength(6), Validators.pattern(/asdfasdf/)]), + password: new FormControl('', [Validators.required, Validators.minLength(6), Validators.pattern(/asdfasdf/)]), + options: new FormControl('', [Validators.required]), + checkbox: new FormControl('', [Validators.required]), + select: new FormControl('', [Validators.required]), + range: new FormControl('', [Validators.required]), + datalist: new FormControl('', [Validators.required]), + }); + + validate() { + this.forms.forEach(f => { + f.markAsTouched(); + }); + } +}