diff --git a/packages/mosaic-dev/validation/module.ts b/packages/mosaic-dev/validation/module.ts index ac3346b86..7b4014a88 100644 --- a/packages/mosaic-dev/validation/module.ts +++ b/packages/mosaic-dev/validation/module.ts @@ -1,5 +1,5 @@ /* tslint:disable:no-console no-reserved-keywords */ -import { Component, NgModule, ViewEncapsulation } from '@angular/core'; +import { ChangeDetectorRef, Component, NgModule, ViewChild, ViewEncapsulation } from '@angular/core'; import { AbstractControl, FormBuilder, FormControl, @@ -122,10 +122,7 @@ export function ldapLoginValidator(loginRegex: RegExp): ValidatorFn { selector: 'app', template: require('./template.html'), encapsulation: ViewEncapsulation.None, - styleUrls: ['./styles.scss'], - providers: [ - // { provide: MC_VALIDATION, useValue: { useValidation: false } } - ] + styleUrls: ['./styles.scss'] }) export class DemoComponent { reactiveTypeaheadItems: string[] = []; @@ -134,6 +131,10 @@ export class DemoComponent { treeSelectValue: string = ''; typeaheadItems: string[] = []; + control = new FormControl('', [Validators.pattern('[a-zA-Z]*')]); + + value = ''; + reactiveForm: FormGroup; formWithCustomValidator = new FormGroup( @@ -155,7 +156,7 @@ export class DemoComponent { dataSource: McTreeFlatDataSource; - constructor(private formBuilder: FormBuilder) { + constructor(private formBuilder: FormBuilder, public changeDetectorRef: ChangeDetectorRef) { this.treeFlattener = new McTreeFlattener( this.transformer, this.getLevel, this.isExpandable, this.getChildren ); @@ -176,6 +177,9 @@ export class DemoComponent { reactiveTypeaheadValue: new FormControl([], [Validators.required]) }); + this.control.valueChanges.subscribe((value) => { + console.log('valueChanges: ', value); // tslint:disable-line:no-console + }); } onSubmitReactiveForm(form: FormGroup) { diff --git a/packages/mosaic-dev/validation/styles.scss b/packages/mosaic-dev/validation/styles.scss index ab5768011..fe232daf8 100644 --- a/packages/mosaic-dev/validation/styles.scss +++ b/packages/mosaic-dev/validation/styles.scss @@ -8,15 +8,15 @@ display: flex; flex-direction: row; - & form { - margin-left: 50px; - } - & .mc-form-field { width: 200px; } } +.margin { + margin-left: 50px; +} + header { $config: mc-typography-config(); diff --git a/packages/mosaic-dev/validation/template.html b/packages/mosaic-dev/validation/template.html index be6490fc7..8033915ea 100644 --- a/packages/mosaic-dev/validation/template.html +++ b/packages/mosaic-dev/validation/template.html @@ -1,5 +1,5 @@
-
+
Reactive form

@@ -17,15 +17,6 @@ Focused Selected Selected1 - Selected2 - Selected3 - Selected4 - Selected5 - Selected6 - Selected7 - Selected8 - Selected9 - Selected10 @@ -75,7 +66,7 @@

- +
Template form

@@ -84,7 +75,7 @@ name="input" mcInput [(ngModel)]="inputValue" - placeholder="required pattern=[a-zA-Z]*" required pattern="[a-zA-Z]*"> + placeholder="required pattern=[a-zA-Z]*" required>

@@ -97,15 +88,6 @@ Focused Selected Selected1 - Selected2 - Selected3 - Selected4 - Selected5 - Selected6 - Selected7 - Selected8 - Selected9 - Selected10 @@ -158,7 +140,7 @@

-
formGroup with custom validator
@@ -178,4 +160,26 @@
+ +
+
formControl
+ +
+ + + + + +

+ +
+ + + + +

+ + +
+
diff --git a/packages/mosaic/core/validation/validation.ts b/packages/mosaic/core/validation/validation.ts index 5069d6d52..ac77f10e7 100644 --- a/packages/mosaic/core/validation/validation.ts +++ b/packages/mosaic/core/validation/validation.ts @@ -45,13 +45,13 @@ export function setMosaicValidation(component) { if (parentForm) { parentForm.ngSubmit.subscribe(() => { // tslint:disable-next-line: no-unnecessary-type-assertion - ngControl.control!.updateValueAndValidity(); + ngControl.control!.updateValueAndValidity({ emitEvent: false }); }); } if (component.ngModel) { setMosaicValidationForModelControl(component, component.rawValidators, parentForm); - } else if (component.formControlName) { + } else if (component.formControlName || component.ngControl) { setMosaicValidationForFormControl(component, parentForm, ngControl); } } @@ -92,7 +92,7 @@ export function setMosaicValidationForFormControl(component, parentForm: NgForm, ngControl.statusChanges! .subscribe(() => { // changed required validation logic - if (ngControl.invalid && !parentForm.submitted && ngControl.errors!.required) { + if (ngControl.invalid && (parentForm && !parentForm.submitted) && ngControl.errors!.required) { setValidState(ngControl.control!, originalValidator!); } diff --git a/packages/mosaic/input/input.spec.ts b/packages/mosaic/input/input.spec.ts index 859400276..6e6f923c4 100644 --- a/packages/mosaic/input/input.spec.ts +++ b/packages/mosaic/input/input.spec.ts @@ -1,14 +1,17 @@ -import { Component, Provider, Type } from '@angular/core'; +import { Component, Provider, Type, ViewChild } from '@angular/core'; import { ComponentFixture, fakeAsync, TestBed, - ComponentFixtureAutoDetect + ComponentFixtureAutoDetect, + flush } from '@angular/core/testing'; -import { FormsModule } from '@angular/forms'; +import { FormsModule, NgForm } from '@angular/forms'; import { By } from '@angular/platform-browser'; import { ESCAPE } from '@ptsecurity/cdk/keycodes'; import { + createMouseEvent, + dispatchEvent, dispatchFakeEvent, dispatchKeyboardEvent } from '@ptsecurity/cdk/testing'; @@ -123,7 +126,34 @@ class McFormFieldWithCleaner { ` }) -class McFormFieldWithoutBorders { +class McFormFieldWithoutBorders {} + +@Component({ + template: ` + + + + ` +}) +class McFormFieldWithStandaloneNgModel { + value: string = ''; +} + +@Component({ + template: ` +
+ + + + + +
+ ` +}) +class McFormFieldWithNgModelInForm { + @ViewChild('form', { static: false }) form: NgForm; + + value: string = ''; } // tslint:enable no-unnecessary-class @@ -132,7 +162,6 @@ class McFormFieldWithoutBorders { describe('McInput', () => { describe('basic behaviors', () => { - it('should change state "disable"', fakeAsync(() => { const fixture = createComponent(McInputForBehaviors); fixture.detectChanges(); @@ -141,16 +170,14 @@ describe('McInput', () => { fixture.debugElement.query(By.directive(McFormField)).nativeElement; const inputElement = fixture.debugElement.query(By.directive(McInput)).nativeElement; - expect(formFieldElement.classList.contains('mc-disabled')) - .toBe(false); + expect(formFieldElement.classList.contains('mc-disabled')).toBe(false); expect(inputElement.disabled).toBe(false); fixture.componentInstance.disabled = true; fixture.detectChanges(); fixture.whenStable().then(() => { - expect(formFieldElement.classList.contains('mc-disabled')) - .toBe(true); + expect(formFieldElement.classList.contains('mc-disabled')).toBe(true); expect(inputElement.disabled).toBe(true); }); })); @@ -245,8 +272,136 @@ describe('McInput', () => { }); }); - describe('apperance', () => { + describe('validation', () => { + describe('ngModel', () => { + describe('standalone', () => { + it('should run validation (required)', fakeAsync(() => { + const fixture = createComponent(McFormFieldWithStandaloneNgModel); + fixture.detectChanges(); + + const formFieldElement = fixture.debugElement.query(By.directive(McFormField)).nativeElement; + expect(formFieldElement.classList.contains('ng-invalid')).toBe(true); + })); + }); + + describe('in form', () => { + it('should not run validation (required)', fakeAsync(() => { + const fixture = createComponent(McFormFieldWithNgModelInForm); + fixture.detectChanges(); + + const formFieldElement = fixture.debugElement.query(By.directive(McFormField)).nativeElement; + expect(formFieldElement.classList.contains('ng-valid')).toBe(true); + })); + + it('should run validation after submit (required)', fakeAsync(() => { + const fixture = createComponent(McFormFieldWithNgModelInForm); + fixture.detectChanges(); + const formFieldElement = fixture.debugElement.query(By.directive(McFormField)).nativeElement; + const submitButton = fixture.debugElement.query(By.css('button')).nativeElement; + + expect(formFieldElement.classList.contains('ng-valid')).toBe(true); + + const event = createMouseEvent('click'); + dispatchEvent(submitButton, event); + flush(); + expect(formFieldElement.classList.contains('ng-invalid')).toBe(true); + })); + }); + }); + + + it('should has placeholder', fakeAsync(() => { + const fixture = createComponent(McInputForBehaviors); + fixture.detectChanges(); + + const testComponent = fixture.debugElement.componentInstance; + + const inputElement = fixture.debugElement.query(By.directive(McInput)).nativeElement; + + expect(inputElement.getAttribute('placeholder')).toBe(null); + + testComponent.placeholder = 'placeholder'; + fixture.detectChanges(); + + expect(inputElement.getAttribute('placeholder')).toBe('placeholder'); + + testComponent.placeholder = ''; + fixture.detectChanges(); + + expect(inputElement.getAttribute('placeholder')).toBe(''); + + })); + + it('should has cleaner', () => { + const fixture = createComponent(McFormFieldWithCleaner, [ + McIconModule + ]); + fixture.detectChanges(); + + const testComponent = fixture.debugElement.componentInstance; + + const formFieldElement = + fixture.debugElement.query(By.directive(McFormField)).nativeElement; + const inputElementDebug = fixture.debugElement.query(By.directive(McInput)); + const inputElement = inputElementDebug.nativeElement; + + + expect(formFieldElement.querySelectorAll('.mc-form-field__cleaner').length) + .toBe(0); + + inputElement.value = 'test'; + dispatchFakeEvent(inputElement, 'input'); + + fixture.detectChanges(); + + expect(formFieldElement.querySelectorAll('.mc-form-field__cleaner').length) + .toBe(1); + + const mcCleaner = fixture.debugElement.query(By.css('.mc-form-field__cleaner')); + const mcCleanerElement = mcCleaner.nativeElement; + mcCleanerElement.click(); + + fixture.detectChanges(); + + expect(formFieldElement.querySelectorAll('.mc-form-field__cleaner').length) + .toBe(0); + expect(testComponent.value).toBe(null); + }); + + it('with cleaner on keydown "ESC" should clear field', () => { + const fixture = createComponent(McFormFieldWithCleaner, [ + McIconModule + ]); + const mcFormFieldDebug = fixture.debugElement.query(By.directive(McFormField)); + const formFieldElement = mcFormFieldDebug.nativeElement; + const inputElementDebug = fixture.debugElement.query(By.directive(McInput)); + const inputElement = inputElementDebug.nativeElement; + + fixture.detectChanges(); + + const testComponent = fixture.debugElement.componentInstance; + + inputElement.value = 'test'; + dispatchFakeEvent(inputElement, 'input'); + dispatchFakeEvent(inputElement, 'focus'); + + fixture.detectChanges(); + + expect(formFieldElement.querySelectorAll('.mc-form-field__cleaner').length) + .toBe(1); + + dispatchKeyboardEvent(mcFormFieldDebug.nativeElement, 'keydown', ESCAPE); + + fixture.detectChanges(); + + expect(formFieldElement.querySelectorAll('.mc-form-field__cleaner').length) + .toBe(0); + expect(testComponent.value).toBe(null); + }); + }); + + describe('apperance', () => { it('should change font to monospace', () => { const fixture = createComponent(McInputWithMcInputMonospace); fixture.detectChanges(); @@ -261,13 +416,11 @@ describe('McInput', () => { const fixture = createComponent(McInputInvalid); fixture.detectChanges(); - const formFieldElement = - fixture.debugElement.query(By.directive(McFormField)).nativeElement; + const formFieldElement = fixture.debugElement.query(By.directive(McFormField)).nativeElement; const inputElementDebug = fixture.debugElement.query(By.directive(McInput)); const inputElement = inputElementDebug.nativeElement; - expect(formFieldElement.classList.contains('ng-invalid')) - .toBe(true); + expect(formFieldElement.classList.contains('ng-invalid')).toBe(true); inputElement.value = 'four'; dispatchFakeEvent(inputElement, 'input'); diff --git a/packages/mosaic/input/input.ts b/packages/mosaic/input/input.ts index fe1ae5709..dff0e73c0 100644 --- a/packages/mosaic/input/input.ts +++ b/packages/mosaic/input/input.ts @@ -18,6 +18,7 @@ import { NG_VALIDATORS, NgControl, NgForm, + NgModel, Validator } from '@angular/forms'; import { @@ -228,6 +229,7 @@ export class McInput extends McInputMixinBase implements McFormFieldControl @Optional() @Inject(MC_VALIDATION) private mcValidation: McValidationOptions, @Optional() @Self() ngControl: NgControl, @Optional() @Self() public numberInput: McNumberInput, + @Optional() @Self() public ngModel: NgModel, @Optional() @Self() public formControlName: FormControlName, @Optional() parentForm: NgForm, @Optional() parentFormGroup: FormGroupDirective, @@ -285,7 +287,7 @@ export class McInput extends McInputMixinBase implements McFormFieldControl this.focusChanged(false); if (this.ngControl) { - this.ngControl.control!.updateValueAndValidity(); + this.ngControl.control!.updateValueAndValidity({ emitEvent: false }); } }