From e38135ad320cf1af58dca90785039d7a7d4580b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20Nagy?= Date: Sun, 11 Jan 2026 17:40:50 +0100 Subject: [PATCH 1/4] #EX-212: Add directive to trim text inputs --- .../src/app/shared/auto-trim.directive.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 frontend/Exence/src/app/shared/auto-trim.directive.ts diff --git a/frontend/Exence/src/app/shared/auto-trim.directive.ts b/frontend/Exence/src/app/shared/auto-trim.directive.ts new file mode 100644 index 0000000..6d5d9d8 --- /dev/null +++ b/frontend/Exence/src/app/shared/auto-trim.directive.ts @@ -0,0 +1,25 @@ +import { Directive, inject } from "@angular/core"; +import { ControlValueAccessor, NgControl } from "@angular/forms"; + +@Directive({ + selector: '[autoTrim]', + standalone: true +}) +export class AutoTrimDirective { + private control = inject(NgControl); + + constructor() { + trimValueAccessor(this.control.valueAccessor!); + } +} + +function trimValueAccessor(valueAccessor: ControlValueAccessor) { + const original = valueAccessor.registerOnChange; + + // overrides angular's formControl updation function to first validate strings + valueAccessor.registerOnChange = (fn: (_: unknown) => void) => { + return original.call(valueAccessor, (value: unknown) => { + return fn(typeof value === 'string' ? value.replace(/\s+/g, ' ').trim() : value); + }); + } +} \ No newline at end of file From 806854b28d7f20485f2bbba247de932f709ba92c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20Nagy?= Date: Sun, 11 Jan 2026 17:51:59 +0100 Subject: [PATCH 2/4] #EX-212: Add autoTrim directive to input fields --- .../private/profile/profile.component.html | 4 ++-- .../app/private/profile/profile.component.ts | 20 +++++++++------- .../create-category-dialog.component.html | 1 + .../create-category-dialog.component.ts | 24 ++++++++++--------- .../create-transaction-dialog.component.html | 3 ++- .../create-transaction-dialog.component.ts | 22 +++++++++-------- .../forgot-password.component.html | 2 +- .../forgot-password.component.ts | 22 +++++++++-------- .../src/app/public/login/login.component.html | 1 + .../src/app/public/login/login.component.ts | 24 ++++++++++--------- .../registration/registration.component.html | 4 ++-- .../registration/registration.component.ts | 24 ++++++++++--------- 12 files changed, 83 insertions(+), 68 deletions(-) diff --git a/frontend/Exence/src/app/private/profile/profile.component.html b/frontend/Exence/src/app/private/profile/profile.component.html index d7fa7be..bc3afc0 100644 --- a/frontend/Exence/src/app/private/profile/profile.component.html +++ b/frontend/Exence/src/app/private/profile/profile.component.html @@ -44,7 +44,7 @@

Profile data

@let nameControl = userDataForm.controls.username; Username - + @@ -54,7 +54,7 @@

Profile data

@let emailControl = userDataForm.controls.email; Email - + diff --git a/frontend/Exence/src/app/private/profile/profile.component.ts b/frontend/Exence/src/app/private/profile/profile.component.ts index e90dd90..710e3da 100644 --- a/frontend/Exence/src/app/private/profile/profile.component.ts +++ b/frontend/Exence/src/app/private/profile/profile.component.ts @@ -15,21 +15,23 @@ import { UserService } from '../../shared/user/user.service'; import { ValidatorComponent } from '../../shared/validator/validator.component'; import { ExtraValidators } from '../../shared/validators'; import { SessionsListComponent } from '../session/sessions-list/sessions-list.component'; +import { AutoTrimDirective } from "src/app/shared/auto-trim.directive"; @Component({ selector: 'ex-profile', templateUrl: './profile.component.html', styleUrl: './profile.component.scss', imports: [ - ReactiveFormsModule, - MatFormFieldModule, - MatInputModule, - MatDividerModule, - ButtonComponent, - InputClearButtonComponent, - SessionsListComponent, - ValidatorComponent, - ], + ReactiveFormsModule, + MatFormFieldModule, + MatInputModule, + MatDividerModule, + ButtonComponent, + InputClearButtonComponent, + SessionsListComponent, + ValidatorComponent, + AutoTrimDirective +], }) export class ProfileComponent { private readonly fb = inject(NonNullableFormBuilder); diff --git a/frontend/Exence/src/app/private/transactions-and-categories/create-category-dialog/create-category-dialog.component.html b/frontend/Exence/src/app/private/transactions-and-categories/create-category-dialog/create-category-dialog.component.html index 60b8f22..ca4a66b 100644 --- a/frontend/Exence/src/app/private/transactions-and-categories/create-category-dialog/create-category-dialog.component.html +++ b/frontend/Exence/src/app/private/transactions-and-categories/create-category-dialog/create-category-dialog.component.html @@ -9,6 +9,7 @@

New category

Name diff --git a/frontend/Exence/src/app/private/transactions-and-categories/create-category-dialog/create-category-dialog.component.ts b/frontend/Exence/src/app/private/transactions-and-categories/create-category-dialog/create-category-dialog.component.ts index 73ed26d..5604b20 100644 --- a/frontend/Exence/src/app/private/transactions-and-categories/create-category-dialog/create-category-dialog.component.ts +++ b/frontend/Exence/src/app/private/transactions-and-categories/create-category-dialog/create-category-dialog.component.ts @@ -13,23 +13,25 @@ import { ButtonComponent } from '../../../shared/button/button.component'; import { InputClearButtonComponent } from '../../../shared/input-clear-button/input-clear-button.component'; import { ValidatorComponent } from '../../../shared/validator/validator.component'; import { CategoryService } from '../../category.service'; +import { AutoTrimDirective } from "src/app/shared/auto-trim.directive"; @Component({ selector: 'ex-create-category-dialog', templateUrl: './create-category-dialog.component.html', styleUrl: './create-category-dialog.component.scss', imports: [ - ReactiveFormsModule, - MatFormFieldModule, - MatInputModule, - MatCardModule, - MatMenuModule, - MatIconModule, - PickerComponent, - InputClearButtonComponent, - ButtonComponent, - ValidatorComponent, - ], + ReactiveFormsModule, + MatFormFieldModule, + MatInputModule, + MatCardModule, + MatMenuModule, + MatIconModule, + PickerComponent, + InputClearButtonComponent, + ButtonComponent, + ValidatorComponent, + AutoTrimDirective +], }) export class CreateCategoryDialogComponent { private readonly dialogRef = inject(MatDialogRef); diff --git a/frontend/Exence/src/app/private/transactions-and-categories/create-transaction-dialog/create-transaction-dialog.component.html b/frontend/Exence/src/app/private/transactions-and-categories/create-transaction-dialog/create-transaction-dialog.component.html index c32befe..1c9875b 100644 --- a/frontend/Exence/src/app/private/transactions-and-categories/create-transaction-dialog/create-transaction-dialog.component.html +++ b/frontend/Exence/src/app/private/transactions-and-categories/create-transaction-dialog/create-transaction-dialog.component.html @@ -9,6 +9,7 @@

New transaction

Title @@ -79,7 +80,7 @@

New transaction

@let noteControl = form.controls.note; Note - + diff --git a/frontend/Exence/src/app/private/transactions-and-categories/create-transaction-dialog/create-transaction-dialog.component.ts b/frontend/Exence/src/app/private/transactions-and-categories/create-transaction-dialog/create-transaction-dialog.component.ts index 83fdfa8..67ae080 100644 --- a/frontend/Exence/src/app/private/transactions-and-categories/create-transaction-dialog/create-transaction-dialog.component.ts +++ b/frontend/Exence/src/app/private/transactions-and-categories/create-transaction-dialog/create-transaction-dialog.component.ts @@ -16,6 +16,7 @@ import { InputClearButtonComponent } from '../../../shared/input-clear-button/in import { ValidatorComponent } from '../../../shared/validator/validator.component'; import { CategoryService } from '../../category.service'; import { TransactionService } from '../transaction.service'; +import { AutoTrimDirective } from "src/app/shared/auto-trim.directive"; export interface CreateTransactionDialogData { type?: TransactionType; @@ -27,16 +28,17 @@ export interface CreateTransactionDialogData { templateUrl: './create-transaction-dialog.component.html', styleUrl: './create-transaction-dialog.component.scss', imports: [ - ReactiveFormsModule, - MatFormFieldModule, - MatInputModule, - MatCardModule, - MatSelectModule, MatDatepickerModule, - MatCheckboxModule, - InputClearButtonComponent, - ButtonComponent, - ValidatorComponent, - ], + ReactiveFormsModule, + MatFormFieldModule, + MatInputModule, + MatCardModule, + MatSelectModule, MatDatepickerModule, + MatCheckboxModule, + InputClearButtonComponent, + ButtonComponent, + ValidatorComponent, + AutoTrimDirective +], }) export class CreateTransactionDialogComponent extends BaseComponent implements OnInit { private readonly dialogRef = inject(MatDialogRef); diff --git a/frontend/Exence/src/app/public/forgot-password/forgot-password.component.html b/frontend/Exence/src/app/public/forgot-password/forgot-password.component.html index af73be0..f3c4ef0 100644 --- a/frontend/Exence/src/app/public/forgot-password/forgot-password.component.html +++ b/frontend/Exence/src/app/public/forgot-password/forgot-password.component.html @@ -16,7 +16,7 @@

Forgot password?

Email - + diff --git a/frontend/Exence/src/app/public/forgot-password/forgot-password.component.ts b/frontend/Exence/src/app/public/forgot-password/forgot-password.component.ts index fc9e948..e8a5b7b 100644 --- a/frontend/Exence/src/app/public/forgot-password/forgot-password.component.ts +++ b/frontend/Exence/src/app/public/forgot-password/forgot-password.component.ts @@ -15,22 +15,24 @@ import { NavigationService } from '../../shared/navigation/navigation.service'; import { SnackbarService } from '../../shared/snackbar/snackbar.service'; import { ExtraValidators } from '../../shared/validators'; import { ValidatorComponent } from '../../shared/validator/validator.component'; +import { AutoTrimDirective } from "src/app/shared/auto-trim.directive"; @Component({ selector: 'ex-forgot-password', templateUrl: './forgot-password.component.html', styleUrl: './forgot-password.component.scss', imports: [ - MatFormFieldModule, - MatInputModule, - ButtonComponent, - ReactiveFormsModule, - MatCardModule, - MatIconModule, - InputClearButtonComponent, - RouterLink, - ValidatorComponent, - ] + MatFormFieldModule, + MatInputModule, + ButtonComponent, + ReactiveFormsModule, + MatCardModule, + MatIconModule, + InputClearButtonComponent, + RouterLink, + ValidatorComponent, + AutoTrimDirective +] }) export class ForgotPasswordComponent extends BaseComponent { private readonly fb = inject(NonNullableFormBuilder); diff --git a/frontend/Exence/src/app/public/login/login.component.html b/frontend/Exence/src/app/public/login/login.component.html index c0c0d8a..a174eea 100644 --- a/frontend/Exence/src/app/public/login/login.component.html +++ b/frontend/Exence/src/app/public/login/login.component.html @@ -20,6 +20,7 @@

Log in to your account

matInput [formControl]="emailControl" type="email" + autoTrim /> diff --git a/frontend/Exence/src/app/public/login/login.component.ts b/frontend/Exence/src/app/public/login/login.component.ts index 43bb72e..ac7a3c0 100644 --- a/frontend/Exence/src/app/public/login/login.component.ts +++ b/frontend/Exence/src/app/public/login/login.component.ts @@ -15,23 +15,25 @@ import { NavigationService } from '../../shared/navigation/navigation.service'; import { CurrentUserService } from '../../shared/user/current-user.service'; import { ValidatorComponent } from '../../shared/validator/validator.component'; import { ExtraValidators } from '../../shared/validators'; +import { AutoTrimDirective } from "src/app/shared/auto-trim.directive"; @Component({ selector: 'ex-login', templateUrl: './login.component.html', styleUrl: './login.component.scss', imports: [ - MatFormFieldModule, - MatInputModule, - ReactiveFormsModule, - MatButtonModule, - RouterModule, - MatCardModule, - MatIconModule, - InputClearButtonComponent, - ButtonComponent, - ValidatorComponent, - ], + MatFormFieldModule, + MatInputModule, + ReactiveFormsModule, + MatButtonModule, + RouterModule, + MatCardModule, + MatIconModule, + InputClearButtonComponent, + ButtonComponent, + ValidatorComponent, + AutoTrimDirective +], }) export class LoginComponent extends BaseComponent { private readonly fb = inject(NonNullableFormBuilder); diff --git a/frontend/Exence/src/app/public/registration/registration.component.html b/frontend/Exence/src/app/public/registration/registration.component.html index ac7eaa3..e8eb5a0 100644 --- a/frontend/Exence/src/app/public/registration/registration.component.html +++ b/frontend/Exence/src/app/public/registration/registration.component.html @@ -19,7 +19,7 @@

Create an Account

@let nameControl = form.controls.username; Username - + @@ -28,7 +28,7 @@

Create an Account

@let emailControl = form.controls.email; Email - + diff --git a/frontend/Exence/src/app/public/registration/registration.component.ts b/frontend/Exence/src/app/public/registration/registration.component.ts index 11e5441..a190883 100644 --- a/frontend/Exence/src/app/public/registration/registration.component.ts +++ b/frontend/Exence/src/app/public/registration/registration.component.ts @@ -15,23 +15,25 @@ import { ExtraValidators } from '../../shared/validators'; import { AuthService } from '../../shared/auth/auth.service'; import { SnackbarService } from '../../shared/snackbar/snackbar.service'; import { ValidatorComponent } from '../../shared/validator/validator.component'; +import { AutoTrimDirective } from "src/app/shared/auto-trim.directive"; @Component({ selector: 'ex-registration', templateUrl: './registration.component.html', styleUrl: './registration.component.scss', imports: [ - MatCardModule, - MatIconModule, - ReactiveFormsModule, - MatFormFieldModule, - MatInput, - ButtonComponent, - InputClearButtonComponent, - RouterLink, - MatTooltipModule, - ValidatorComponent, - ] + MatCardModule, + MatIconModule, + ReactiveFormsModule, + MatFormFieldModule, + MatInput, + ButtonComponent, + InputClearButtonComponent, + RouterLink, + MatTooltipModule, + ValidatorComponent, + AutoTrimDirective +] }) export class RegistrationComponent extends BaseComponent { private readonly fb = inject(NonNullableFormBuilder); From ac21ddb267ea6edc8ecbd38212cd82a6ed940d26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20Nagy?= Date: Sun, 11 Jan 2026 18:20:06 +0100 Subject: [PATCH 3/4] #EX-207: Add directive to handle stop event propagation --- .../app/shared/stop-propagation.directive.ts | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 frontend/Exence/src/app/shared/stop-propagation.directive.ts diff --git a/frontend/Exence/src/app/shared/stop-propagation.directive.ts b/frontend/Exence/src/app/shared/stop-propagation.directive.ts new file mode 100644 index 0000000..5752d9b --- /dev/null +++ b/frontend/Exence/src/app/shared/stop-propagation.directive.ts @@ -0,0 +1,30 @@ +import { Directive, effect, ElementRef, inject, input, Renderer2 } from "@angular/core"; + +@Directive({ + selector: '[stopPropagation]' +}) +export class StopPropagationDirective { + private readonly element = inject(ElementRef); + private readonly renderer = inject(Renderer2); + + readonly events = input(['click']); + + constructor() { + effect(onCleanup => { + const events = this.events(); + const eventList = Array.isArray(events) ? events : [events]; + + const unlisteners = eventList.map(eventName => + this.renderer.listen( + this.element.nativeElement, + eventName, + (event: Event) => event.stopPropagation() + ) + ); + + onCleanup(() => { + unlisteners.forEach(unlisten => unlisten()); + }) + }); + } +} \ No newline at end of file From c6803c938def9d313435e165c4ac493a4f3ec078 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20Nagy?= Date: Sun, 11 Jan 2026 18:21:18 +0100 Subject: [PATCH 4/4] #EX-207: Add stopPropagation and remove unnecessary stopPropagations --- .../create-category-dialog.component.html | 1 - .../data-table/data-table.component.html | 8 ++--- .../shared/data-table/data-table.component.ts | 36 ++++++++++--------- 3 files changed, 23 insertions(+), 22 deletions(-) diff --git a/frontend/Exence/src/app/private/transactions-and-categories/create-category-dialog/create-category-dialog.component.html b/frontend/Exence/src/app/private/transactions-and-categories/create-category-dialog/create-category-dialog.component.html index ca4a66b..2d33055 100644 --- a/frontend/Exence/src/app/private/transactions-and-categories/create-category-dialog/create-category-dialog.component.html +++ b/frontend/Exence/src/app/private/transactions-and-categories/create-category-dialog/create-category-dialog.component.html @@ -77,6 +77,5 @@

New category

\ No newline at end of file diff --git a/frontend/Exence/src/app/shared/data-table/data-table.component.html b/frontend/Exence/src/app/shared/data-table/data-table.component.html index 41c9cb5..045c01e 100644 --- a/frontend/Exence/src/app/shared/data-table/data-table.component.html +++ b/frontend/Exence/src/app/shared/data-table/data-table.component.html @@ -145,13 +145,13 @@

- +