From 25f098fe14e3f0a5ad88fe7459d64e38b0fe3650 Mon Sep 17 00:00:00 2001 From: Eero Suvanto Date: Tue, 3 Dec 2024 16:15:05 +0200 Subject: [PATCH 1/4] DS-404: change ErrorMessage component to directive --- ...omponent.ts => error-message.directive.ts} | 11 ++---- .../error-message/error-message.stories.ts | 37 +++++++++++++++---- .../error-message/error-message/readme.mdx | 16 +++----- .../ngx-fudis/src/lib/ngx-fudis.module.ts | 6 +-- .../projects/ngx-fudis/src/public-api.ts | 2 +- 5 files changed, 43 insertions(+), 29 deletions(-) rename ngx-fudis/projects/ngx-fudis/src/lib/components/form/error-message/error-message/{error-message.component.ts => error-message.directive.ts} (96%) diff --git a/ngx-fudis/projects/ngx-fudis/src/lib/components/form/error-message/error-message/error-message.component.ts b/ngx-fudis/projects/ngx-fudis/src/lib/components/form/error-message/error-message/error-message.directive.ts similarity index 96% rename from ngx-fudis/projects/ngx-fudis/src/lib/components/form/error-message/error-message/error-message.component.ts rename to ngx-fudis/projects/ngx-fudis/src/lib/components/form/error-message/error-message/error-message.directive.ts index edbc634bd..5ef4ff656 100644 --- a/ngx-fudis/projects/ngx-fudis/src/lib/components/form/error-message/error-message/error-message.component.ts +++ b/ngx-fudis/projects/ngx-fudis/src/lib/components/form/error-message/error-message/error-message.directive.ts @@ -1,7 +1,6 @@ import { - ChangeDetectionStrategy, - Component, DestroyRef, + Directive, EventEmitter, Host, inject, @@ -33,12 +32,10 @@ import { MultiselectComponent } from '../../select/multiselect/multiselect.compo import { FudisComponentChanges } from '../../../../types/miscellaneous'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -@Component({ +@Directive({ selector: 'fudis-error-message', - template: '', - changeDetection: ChangeDetectionStrategy.OnPush, }) -export class ErrorMessageComponent implements OnInit, OnChanges, OnDestroy { +export class ErrorMessageDirective implements OnInit, OnChanges, OnDestroy { constructor( private _errorSummaryService: FudisInternalErrorSummaryService, private _translationService: FudisTranslationService, @@ -132,7 +129,7 @@ export class ErrorMessageComponent implements OnInit, OnChanges, OnDestroy { } } - ngOnChanges(changes: FudisComponentChanges): void { + ngOnChanges(changes: FudisComponentChanges): void { const newMessage = changes.message?.currentValue; if (newMessage !== changes.message?.previousValue) { diff --git a/ngx-fudis/projects/ngx-fudis/src/lib/components/form/error-message/error-message/error-message.stories.ts b/ngx-fudis/projects/ngx-fudis/src/lib/components/form/error-message/error-message/error-message.stories.ts index acba483e3..b0e611370 100644 --- a/ngx-fudis/projects/ngx-fudis/src/lib/components/form/error-message/error-message/error-message.stories.ts +++ b/ngx-fudis/projects/ngx-fudis/src/lib/components/form/error-message/error-message/error-message.stories.ts @@ -1,10 +1,11 @@ import { StoryFn, Meta, moduleMetadata } from '@storybook/angular'; +import { action } from '@storybook/addon-actions'; import { FormControl, ReactiveFormsModule, FormsModule, FormControlOptions } from '@angular/forms'; -import { Component } from '@angular/core'; +import { Component, EventEmitter, Output } from '@angular/core'; import { BehaviorSubject, Subject } from 'rxjs'; -import { ErrorMessageComponent } from './error-message.component'; +import { ErrorMessageDirective } from './error-message.directive'; import readme from './readme.mdx'; -import { FudisValidators } from '../../../../utilities/form/validators'; +import { FudisValidationErrors, FudisValidators } from '../../../../utilities/form/validators'; import { excludeAllRegex, errorMessageExclude } from '../../../../utilities/storybook'; @Component({ @@ -17,8 +18,18 @@ import { excludeAllRegex, errorMessageExclude } from '../../../../utilities/stor [control]="control" [label]="'Focus to input'" > - - + + (); + @Output() handleRemoveError = new EventEmitter(); + toggleCustomError(): void { this._errorExists = !this._errorExists; } @@ -79,7 +93,7 @@ class TextInputWithErrorMessageComponent { export default { title: 'Components/Form/Error Message', - component: ErrorMessageComponent, + component: ErrorMessageDirective, decorators: [ moduleMetadata({ declarations: [TextInputWithErrorMessageComponent], @@ -110,6 +124,7 @@ const Template: StoryFn = (args) => ({ '', FudisValidators.required('This validation message is send by Fudis Validators'), ), + addError: action('addError'), }, template: html` ({ message. - + `, }); @@ -133,8 +148,14 @@ Example.args = { export const ExampleWithObservableError: StoryFn = (args) => ({ ...args, + props: { + handleAddError: action('handleAddError'), + handleRemoveError: action('handleRemoveError'), + }, template: ` - + `, }); diff --git a/ngx-fudis/projects/ngx-fudis/src/lib/components/form/error-message/error-message/readme.mdx b/ngx-fudis/projects/ngx-fudis/src/lib/components/form/error-message/error-message/readme.mdx index d41b9dcbe..c86bd3796 100644 --- a/ngx-fudis/projects/ngx-fudis/src/lib/components/form/error-message/error-message/readme.mdx +++ b/ngx-fudis/projects/ngx-fudis/src/lib/components/form/error-message/error-message/readme.mdx @@ -1,19 +1,19 @@ import { ArgTypes, Meta, Canvas } from "@storybook/blocks"; import * as ErrorMessageStories from "./error-message.stories"; -import { ErrorMessageComponent } from "./error-message.component"; +import { ErrorMessageDirective } from "./error-message.directive"; # Error Message -Form component's errors can be achieved in two ways: +Form field components' errors can be achieved in two ways: -- Through form control's validators: [Fudis Validators](/docs/utilities-validators--documentation). -- As an Error Message child component, for cases when using a validator or creating a custom one can be tricky. +- Through Form Control's validators: [Fudis Validators](/docs/utilities-validators--documentation). +- As an Error Message Directive, for cases when using a validator or creating a custom one can be tricky. If your error message repeats multiple times across your form or application, it is recommended to create your own [Fudis Validator](/docs/utilities-validators--documentation). -This page is documentation for the latter Error Message Component, which binds custom error message to Fudis Form components through content projection. +This page is documentation for the latter Error Message Directive, which binds custom error message to form field components through content projection. This requires the use of selector `fudis-error-message`. Both Error Message and Fudis Validator errors create the same UI result and renders errors under the Guidance Component. @@ -63,8 +63,4 @@ The following example demostrates how custom error message can be used along wit - [Error Summary](/docs/components-form-error-summary--documentation) - - -``` - -``` + diff --git a/ngx-fudis/projects/ngx-fudis/src/lib/ngx-fudis.module.ts b/ngx-fudis/projects/ngx-fudis/src/lib/ngx-fudis.module.ts index daf75cfd9..382603eb8 100644 --- a/ngx-fudis/projects/ngx-fudis/src/lib/ngx-fudis.module.ts +++ b/ngx-fudis/projects/ngx-fudis/src/lib/ngx-fudis.module.ts @@ -38,7 +38,6 @@ import { DialogComponent } from './components/dialog/dialog.component'; import { DropdownMenuComponent } from './components/dropdown-menu/dropdown-menu.component'; import { DropdownMenuItemComponent } from './components/dropdown-menu/dropdown-menu-item/dropdown-menu-item.component'; import { DropdownMenuGroupComponent } from './components/dropdown-menu/dropdown-menu-group/dropdown-menu-group.component'; -import { ErrorMessageComponent } from './components/form/error-message/error-message/error-message.component'; import { ErrorSummaryComponent } from './components/form/error-summary/error-summary.component'; import { ExpandableComponent } from './components/expandable/expandable.component'; import { FieldSetComponent } from './components/form/fieldset/fieldset.component'; @@ -92,6 +91,7 @@ import { } from './directives/dialog/dialog-directives'; import { DropdownBaseDirective } from './directives/form/dropdown-base/dropdown-base.directive'; import { DropdownItemBaseDirective } from './directives/form/dropdown-item-base/dropdown-item-base.directive'; +import { ErrorMessageDirective } from './components/form/error-message/error-message/error-message.directive'; import { FormCommonApiDirective } from './directives/form/form-common-api/form-common-api.directive'; import { FormSubmitDirective } from './directives/form/form-actions/form-actions.directive'; import { GridApiDirective } from './directives/grid/grid-api/grid-api.directive'; @@ -156,7 +156,7 @@ import { FudisTranslationService } from './services/translation/translation.serv DropdownMenuComponent, DropdownItemBaseDirective, DropdownMenuItemComponent, - ErrorMessageComponent, + ErrorMessageDirective, ErrorSummaryComponent, ExpandableComponent, FieldSetComponent, @@ -257,7 +257,7 @@ import { FudisTranslationService } from './services/translation/translation.serv DropdownMenuGroupComponent, DropdownMenuComponent, DropdownMenuItemComponent, - ErrorMessageComponent, + ErrorMessageDirective, ExpandableComponent, FooterComponent, FooterContentLeftDirective, diff --git a/ngx-fudis/projects/ngx-fudis/src/public-api.ts b/ngx-fudis/projects/ngx-fudis/src/public-api.ts index 64d990cef..802c64baf 100644 --- a/ngx-fudis/projects/ngx-fudis/src/public-api.ts +++ b/ngx-fudis/projects/ngx-fudis/src/public-api.ts @@ -48,7 +48,7 @@ export { FudisValidators } from './lib/utilities/form/validators'; export { DropdownMenuGroupComponent } from './lib/components/dropdown-menu/dropdown-menu-group/dropdown-menu-group.component'; export { DropdownMenuComponent } from './lib/components/dropdown-menu/dropdown-menu.component'; export { DropdownMenuItemComponent } from './lib/components/dropdown-menu/dropdown-menu-item/dropdown-menu-item.component'; -export { ErrorMessageComponent } from './lib/components/form/error-message/error-message/error-message.component'; +export { ErrorMessageDirective } from './lib/components/form/error-message/error-message/error-message.directive'; export { FudisErrorSummaryService } from './lib/services/form/error-summary/error-summary.service'; export { ExpandableComponent } from './lib/components/expandable/expandable.component'; export { FieldSetComponent } from './lib/components/form/fieldset/fieldset.component'; From 17124dcf44de693fd0255f1b6ea7213b5f74b89e Mon Sep 17 00:00:00 2001 From: Eero Suvanto Date: Tue, 3 Dec 2024 16:15:11 +0200 Subject: [PATCH 2/4] DS-404: update tests --- ...pec.ts => error-message.directive.spec.ts} | 58 ++++++++++++------- 1 file changed, 37 insertions(+), 21 deletions(-) rename ngx-fudis/projects/ngx-fudis/src/lib/components/form/error-message/error-message/{error-message.component.spec.ts => error-message.directive.spec.ts} (75%) diff --git a/ngx-fudis/projects/ngx-fudis/src/lib/components/form/error-message/error-message/error-message.component.spec.ts b/ngx-fudis/projects/ngx-fudis/src/lib/components/form/error-message/error-message/error-message.directive.spec.ts similarity index 75% rename from ngx-fudis/projects/ngx-fudis/src/lib/components/form/error-message/error-message/error-message.component.spec.ts rename to ngx-fudis/projects/ngx-fudis/src/lib/components/form/error-message/error-message/error-message.directive.spec.ts index 6501acc06..b72edfd57 100644 --- a/ngx-fudis/projects/ngx-fudis/src/lib/components/form/error-message/error-message/error-message.component.spec.ts +++ b/ngx-fudis/projects/ngx-fudis/src/lib/components/form/error-message/error-message/error-message.directive.spec.ts @@ -1,11 +1,11 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { FormControl, ReactiveFormsModule } from '@angular/forms'; -import { Component, ViewChild } from '@angular/core'; +import { Component, EventEmitter, Output, ViewChild } from '@angular/core'; import { MockComponents } from 'ng-mocks'; import { BehaviorSubject } from 'rxjs'; import { IconComponent } from '../../../icon/icon.component'; import { FudisValidationErrors, FudisValidators } from '../../../../utilities/form/validators'; -import { ErrorMessageComponent } from './error-message.component'; +import { ErrorMessageDirective } from './error-message.directive'; import { TextInputComponent } from '../../text-input/text-input.component'; import { LabelComponent } from '../../label/label.component'; import { GuidanceComponent } from '../../guidance/guidance.component'; @@ -45,17 +45,26 @@ const errorToRemove: FudisValidationErrors = { selector: 'fudis-mock-test-error', template: ` - + `, }) class MockTestErrorComponent { - @ViewChild('testError') testError: ErrorMessageComponent; + @ViewChild('testError') testError: ErrorMessageDirective; message = 'Test error message'; errorVisible: boolean; + @Output() handleAddError = new EventEmitter(); + @Output() handleRemoveError = new EventEmitter(); + control: FormControl = new FormControl('', FudisValidators.required('This is required')); } @@ -66,7 +75,7 @@ describe('ErrorMessageComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ declarations: [ - ErrorMessageComponent, + ErrorMessageDirective, IconComponent, MockTestErrorComponent, TextInputComponent, @@ -91,14 +100,6 @@ describe('ErrorMessageComponent', () => { fixture.detectChanges(); }); - it('should emit addError', () => { - jest.spyOn(component.testError.handleAddError, 'emit'); - component.testError.ngOnInit(); - fixture.detectChanges(); - - expect(component.testError.handleAddError.emit).toHaveBeenCalledWith(errorReceived); - }); - it('should have proper errors in control when component is created', () => { expect(component.control.errors).toEqual(controlErrors); }); @@ -110,14 +111,6 @@ describe('ErrorMessageComponent', () => { expect(component.control.errors).toEqual(controlOnlyRequiredError); }); - it('should emit removeError message when component is destroyed', () => { - jest.spyOn(component.testError.handleRemoveError, 'emit'); - component.testError.ngOnDestroy(); - fixture.detectChanges(); - - expect(component.testError.handleRemoveError.emit).toHaveBeenCalledWith(errorToRemove); - }); - it('should have first invalid and then valid control when all errors are removed', () => { expect(component.control.invalid).toBe(true); component.control.patchValue('Add value to remove required error'); @@ -139,4 +132,27 @@ describe('ErrorMessageComponent', () => { expect(component.control.errors).toEqual(controlErrors); }); }); + + describe('emitters', () => { + it('should emit addError when directive is created', () => { + jest.spyOn(component.handleAddError, 'emit'); + component.errorVisible = true; + fixture.detectChanges(); + + expect(component.handleAddError.emit).toHaveBeenCalledWith(errorReceived); + }); + + it('should emit removeError message when directive is destroyed', () => { + jest.spyOn(component.handleRemoveError, 'emit'); + component.errorVisible = true; + fixture.detectChanges(); + + expect(component.handleRemoveError.emit).not.toHaveBeenCalled(); + + component.errorVisible = false; + fixture.detectChanges(); + + expect(component.handleRemoveError.emit).toHaveBeenCalledWith(errorToRemove); + }); + }); }); From 8a5b3bafd0a9bf06b39fd77ea9f90d9aa6033090 Mon Sep 17 00:00:00 2001 From: Eero Suvanto Date: Wed, 4 Dec 2024 13:18:32 +0200 Subject: [PATCH 3/4] DS-404: adjust unit tests --- .../error-message.directive.spec.ts | 28 ++++++++----------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/ngx-fudis/projects/ngx-fudis/src/lib/components/form/error-message/error-message/error-message.directive.spec.ts b/ngx-fudis/projects/ngx-fudis/src/lib/components/form/error-message/error-message/error-message.directive.spec.ts index b72edfd57..8076a6c18 100644 --- a/ngx-fudis/projects/ngx-fudis/src/lib/components/form/error-message/error-message/error-message.directive.spec.ts +++ b/ngx-fudis/projects/ngx-fudis/src/lib/components/form/error-message/error-message/error-message.directive.spec.ts @@ -1,9 +1,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { FormControl, ReactiveFormsModule } from '@angular/forms'; import { Component, EventEmitter, Output, ViewChild } from '@angular/core'; -import { MockComponents } from 'ng-mocks'; -import { BehaviorSubject } from 'rxjs'; -import { IconComponent } from '../../../icon/icon.component'; import { FudisValidationErrors, FudisValidators } from '../../../../utilities/form/validators'; import { ErrorMessageDirective } from './error-message.directive'; import { TextInputComponent } from '../../text-input/text-input.component'; @@ -12,15 +9,9 @@ import { GuidanceComponent } from '../../guidance/guidance.component'; import { ValidatorErrorMessageComponent } from '../validator-error-message/validator-error-message.component'; import { FudisInternalErrorSummaryService } from '../../../../services/form/error-summary/internal-error-summary.service'; -const observableMessage = new BehaviorSubject('Test error message'); - const controlErrors = { - required: { - message: 'This is required', - }, - 'fudis-error-message-1': { - message: observableMessage, - }, + required: 'This is required', + 'fudis-error-message-1': 'Test error message', }; const controlOnlyRequiredError = { @@ -76,11 +67,11 @@ describe('ErrorMessageComponent', () => { TestBed.configureTestingModule({ declarations: [ ErrorMessageDirective, - IconComponent, MockTestErrorComponent, TextInputComponent, ValidatorErrorMessageComponent, - MockComponents(LabelComponent, GuidanceComponent), + LabelComponent, + GuidanceComponent, ], imports: [ReactiveFormsModule], providers: [FudisInternalErrorSummaryService], @@ -101,7 +92,10 @@ describe('ErrorMessageComponent', () => { }); it('should have proper errors in control when component is created', () => { - expect(component.control.errors).toEqual(controlErrors); + expect(component.control.errors?.['required'].message).toEqual(controlErrors.required); + expect(component.control.errors?.['fudis-error-message-1'].message.value).toEqual( + controlErrors['fudis-error-message-1'], + ); }); it('should have only required error in control when component is destroyed', () => { @@ -122,14 +116,14 @@ describe('ErrorMessageComponent', () => { expect(component.control.valid).toBe(true); }); - it('should update string message', () => { + it('should update string message', async () => { const newError = 'Error message updated'; - observableMessage.next(newError); component.message = newError; + fixture.detectChanges(); - expect(component.control.errors).toEqual(controlErrors); + expect(component.control?.errors?.['fudis-error-message-1'].message.value).toEqual(newError); }); }); From d3041f9a241316ac4b8b01e7a3acad37d74b6d53 Mon Sep 17 00:00:00 2001 From: RiinaKuu Date: Wed, 4 Dec 2024 13:56:56 +0200 Subject: [PATCH 4/4] DS-404: Change example story --- .../lib/components/form/error-message/error-message/readme.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ngx-fudis/projects/ngx-fudis/src/lib/components/form/error-message/error-message/readme.mdx b/ngx-fudis/projects/ngx-fudis/src/lib/components/form/error-message/error-message/readme.mdx index c86bd3796..27efda591 100644 --- a/ngx-fudis/projects/ngx-fudis/src/lib/components/form/error-message/error-message/readme.mdx +++ b/ngx-fudis/projects/ngx-fudis/src/lib/components/form/error-message/error-message/readme.mdx @@ -42,7 +42,7 @@ Message parameter can be passed either as a string or as an observable string. The following example demostrates how custom error message can be used along with Fudis Validator error messages. By toggling 'Switch Message Content' button will dynamically update error message content. - + ## Accessibility