diff --git a/lib/core/src/lib/comments/comments.component.html b/lib/core/src/lib/comments/comments.component.html index d8252851d3..4a1a69e58b 100644 --- a/lib/core/src/lib/comments/comments.component.html +++ b/lib/core/src/lib/comments/comments.component.html @@ -8,22 +8,22 @@ matInput id="comment-input" class="adf-text-text-area" - [placeholder]='"COMMENTS.ADD" | translate' + [placeholder]='("COMMENTS.ADD" | translate) + "*"' [attr.aria-label]="'COMMENTS.ADD' | translate" - [(ngModel)]="message" + [formControl]="commentControl" (keydown.escape)="clearMessage($event)" > + {{ 'COMMENTS.EMPTY_ERROR' | translate }}
diff --git a/lib/core/src/lib/comments/comments.component.scss b/lib/core/src/lib/comments/comments.component.scss index 532fc767b5..0b1b628e72 100644 --- a/lib/core/src/lib/comments/comments.component.scss +++ b/lib/core/src/lib/comments/comments.component.scss @@ -12,10 +12,15 @@ adf-comments { #{$mat-form-field} { width: 100%; - } - #{$mat-form-field-subscript-wrapper} { - display: none; + &#{$mat-form-field-invalid} { + #{$mat-input-element} { + &::placeholder, + &:focus::placeholder { + color: var(--theme-warn-color); + } + } + } } #{$mat-form-field-wrapper} { diff --git a/lib/core/src/lib/comments/comments.component.spec.ts b/lib/core/src/lib/comments/comments.component.spec.ts index 32cc76f97b..fc3c65eb66 100644 --- a/lib/core/src/lib/comments/comments.component.spec.ts +++ b/lib/core/src/lib/comments/comments.component.spec.ts @@ -25,6 +25,7 @@ import { CommentsService } from './interfaces/comments-service.interface'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { NoopTranslateModule } from '../testing/noop-translate.module'; import { UnitTestingUtils } from '../testing/unit-testing-utils'; +import { MatError } from '@angular/material/form-field'; describe('CommentsComponent', () => { let component: CommentsComponent; @@ -156,6 +157,10 @@ describe('CommentsComponent', () => { }); describe('Add comment', () => { + const getError = (): string => testingUtils.getByDirective(MatError)?.nativeElement?.textContent; + + const getAddCommentButton = (): HTMLButtonElement => testingUtils.getByDataAutomationId('comments-input-add').nativeElement; + beforeEach(() => { component.id = '123'; fixture.detectChanges(); @@ -163,8 +168,8 @@ describe('CommentsComponent', () => { }); it('should normalize comment when user input contains spaces sequence', async () => { - component.message = 'test comment'; - testingUtils.clickByCSS('.adf-comments-input-add'); + component.commentControl.setValue('test comment'); + getAddCommentButton().dispatchEvent(new Event('click')); fixture.detectChanges(); await fixture.whenStable(); @@ -184,8 +189,8 @@ describe('CommentsComponent', () => { getCommentSpy.and.returnValue(of([])); addCommentSpy.and.returnValue(commentsResponseMock.addComment(commentText)); - component.message = commentText; - testingUtils.clickByCSS('.adf-comments-input-add'); + component.commentControl.setValue(commentText); + getAddCommentButton().dispatchEvent(new Event('click')); fixture.detectChanges(); await fixture.whenStable(); @@ -195,9 +200,10 @@ describe('CommentsComponent', () => { }); it('should call service to add a comment when add button is pressed', async () => { - component.message = 'Test Comment'; - addCommentSpy.and.returnValue(commentsResponseMock.addComment(component.message)); - testingUtils.clickByCSS('.adf-comments-input-add'); + const comment = 'Test Comment'; + component.commentControl.setValue(comment); + addCommentSpy.and.returnValue(commentsResponseMock.addComment(comment)); + getAddCommentButton().dispatchEvent(new Event('click')); fixture.detectChanges(); await fixture.whenStable(); @@ -209,8 +215,8 @@ describe('CommentsComponent', () => { }); it('should not call service to add a comment when comment is empty', async () => { - component.message = ''; - testingUtils.clickByCSS('.adf-comments-input-add'); + component.commentControl.setValue(''); + getAddCommentButton().dispatchEvent(new Event('click')); fixture.detectChanges(); await fixture.whenStable(); @@ -231,7 +237,7 @@ describe('CommentsComponent', () => { it('should emit an error when an error occurs adding the comment', () => { const emitSpy = spyOn(component.error, 'emit'); addCommentSpy.and.returnValue(throwError(() => new Error('error'))); - component.message = 'Test comment'; + component.commentControl.setValue('Test comment'); component.addComment(); expect(emitSpy).toHaveBeenCalled(); }); @@ -255,9 +261,85 @@ describe('CommentsComponent', () => { }); it('should not add comment if message is empty', () => { - component.message = ''; + component.commentControl.setValue(''); component.addComment(); expect(addCommentSpy).not.toHaveBeenCalled(); }); + + it('should not display error message initially', () => { + expect(getError()).toBeUndefined(); + }); + + it('should display error message when comment is empty and was touched', () => { + component.commentControl.setValue(''); + component.commentControl.markAsTouched(); + + fixture.detectChanges(); + expect(getError()).toBe('COMMENTS.EMPTY_ERROR'); + }); + + it('should display error message when comment has only spaces and was touched', () => { + component.commentControl.setValue(' '); + component.commentControl.markAsTouched(); + + fixture.detectChanges(); + expect(getError()).toBe('COMMENTS.EMPTY_ERROR'); + }); + + it('should not display error message when comment is empty but was not touched', () => { + component.commentControl.setValue(''); + + fixture.detectChanges(); + expect(getError()).toBeUndefined(); + }); + + it('should not display error message when comment has only spaces but was not touched', () => { + component.commentControl.setValue(' '); + + fixture.detectChanges(); + expect(getError()).toBeUndefined(); + }); + + it('should not display error message when comment is not empty and was touched', () => { + component.commentControl.setValue('Some comment'); + component.commentControl.markAsTouched(); + + fixture.detectChanges(); + expect(getError()).toBeUndefined(); + }); + + it('should not display error message when comment is not empty and was not touched', () => { + component.commentControl.setValue('Some comment'); + + fixture.detectChanges(); + expect(getError()).toBeUndefined(); + }); + + it('should disable add button initially', () => { + expect(getAddCommentButton().disabled).toBeTrue(); + }); + + it('should disable add button when comment is empty', () => { + component.commentControl.setValue('Some comment'); + component.commentControl.setValue(''); + + fixture.detectChanges(); + expect(getAddCommentButton().disabled).toBeTrue(); + }); + + it('should disable add button when comment has only spaces', () => { + component.commentControl.setValue('Some comment'); + component.commentControl.setValue(' '); + + fixture.detectChanges(); + expect(getAddCommentButton().disabled).toBeTrue(); + }); + + it('should enable add button when comment is not empty', () => { + component.commentControl.setValue('Some comment'); + + fixture.detectChanges(); + expect(getAddCommentButton().disabled).toBeFalse(); + }); }); }); diff --git a/lib/core/src/lib/comments/comments.component.ts b/lib/core/src/lib/comments/comments.component.ts index 3c1791fb10..544eae4ada 100644 --- a/lib/core/src/lib/comments/comments.component.ts +++ b/lib/core/src/lib/comments/comments.component.ts @@ -23,14 +23,23 @@ import { CommonModule } from '@angular/common'; import { TranslateModule } from '@ngx-translate/core'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; -import { FormsModule } from '@angular/forms'; +import { FormControl, FormsModule, ReactiveFormsModule, ValidationErrors } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { CommentListComponent } from './comment-list'; @Component({ selector: 'adf-comments', standalone: true, - imports: [CommonModule, TranslateModule, MatFormFieldModule, MatInputModule, FormsModule, MatButtonModule, CommentListComponent], + imports: [ + CommonModule, + TranslateModule, + MatFormFieldModule, + MatInputModule, + FormsModule, + MatButtonModule, + CommentListComponent, + ReactiveFormsModule + ], templateUrl: './comments.component.html', styleUrls: ['./comments.component.scss'], encapsulation: ViewEncapsulation.None @@ -49,11 +58,16 @@ export class CommentsComponent implements OnChanges { error = new EventEmitter(); comments: CommentModel[] = []; - message: string; beingAdded: boolean = false; private commentsService = inject(ADF_COMMENTS_SERVICE); + private readonly _commentControl = new FormControl('', [this.validateEmptyComment]); + + get commentControl(): FormControl { + return this._commentControl; + } + ngOnChanges(changes: SimpleChanges): void { this.id = null; @@ -95,10 +109,10 @@ export class CommentsComponent implements OnChanges { this.beingAdded = true; - this.commentsService.add(this.id, this.message).subscribe({ + this.commentsService.add(this.id, this.commentControl.value).subscribe({ next: (res) => { this.addToComments(res); - this.resetMessage(); + this.commentControl.reset(); }, error: (err) => { this.error.emit(err); @@ -111,19 +125,15 @@ export class CommentsComponent implements OnChanges { clearMessage(event: Event): void { event.stopPropagation(); - this.resetMessage(); + this.commentControl.reset(); } private addToComments(comment: CommentModel): void { this.comments.unshift(comment); } - private resetMessage(): void { - this.message = ''; - } - private canAddComment(): boolean { - return this.hasId() && this.message && this.message.trim() && !this.beingAdded; + return this.hasId() && this.commentControl.value?.trim() && !this.beingAdded; } private hasId(): boolean { @@ -146,4 +156,8 @@ export class CommentsComponent implements OnChanges { private resetComments(): void { this.comments = []; } + + private validateEmptyComment(commentControl: FormControl): ValidationErrors { + return commentControl.value?.trim() ? null : { emptyComment: true }; + } } diff --git a/lib/core/src/lib/i18n/en.json b/lib/core/src/lib/i18n/en.json index b0bfd60c59..d7746c7351 100644 --- a/lib/core/src/lib/i18n/en.json +++ b/lib/core/src/lib/i18n/en.json @@ -306,6 +306,7 @@ "HEADER": "Comments ({{ count }})", "CREATED_BY_HEADER": "Created by", "MESSAGE_HEADER": "Message", + "EMPTY_ERROR": "Comment can't be empty", "DIALOG": { "TITLE": "New comment", "LABELS": {