Skip to content

Commit

Permalink
refactor: more conversions to use signals (#388)
Browse files Browse the repository at this point in the history
  • Loading branch information
jrassa authored Jun 7, 2024
1 parent 877767f commit 683e822
Show file tree
Hide file tree
Showing 43 changed files with 501 additions and 572 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ proxy.conf.json
npm-debug.log
yarn-error.log

# NX
.nx/cache

# IDEs and editors
.idea/
.project
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<asy-modal
[title]="data.title"
[cancelText]="data.cancelText"
[cancelText]="data.cancelText ?? 'Cancel'"
[disableOk]="!modalForm.form.valid"
[hideCancel]="data.hideCancel ?? false"
[okText]="data.okText"
(cancel)="cancel()"
(ok)="ok()"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export class ConfigurableDialogData {
title: string;
okText: string;
cancelText?: string;
hideCancel?: boolean;
message: string;
inputs?: DialogInput[];
}
Expand Down
3 changes: 2 additions & 1 deletion src/app/common/dialog/dialog.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ export class DialogService {
{
title,
message,
okText
okText,
hideCancel: true
},
modalOptions
);
Expand Down
10 changes: 5 additions & 5 deletions src/app/common/modal/modal/modal.component.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<section class="modal-header">
<h2 class="modal-title">
{{ title }}
{{ title() }}
</h2>

<button class="btn-close" type="button" aria-label="Cancel" (click)="cancel.emit()"></button>
Expand All @@ -11,12 +11,12 @@ <h2 class="modal-title">
</section>

<section class="modal-footer">
@if (cancelText) {
@if (hideCancel()) {
<button class="btn btn-outline-primary me-2" type="button" (click)="cancel.emit()">
{{ cancelText }}
{{ cancelText() }}
</button>
}
<button class="btn btn-primary" type="button" [disabled]="disableOk" (click)="ok.emit()">
{{ okText }}
<button class="btn btn-primary" type="button" [disabled]="disableOk()" (click)="ok.emit()">
{{ okText() }}
</button>
</section>
22 changes: 8 additions & 14 deletions src/app/common/modal/modal/modal.component.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { A11yModule } from '@angular/cdk/a11y';
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { ChangeDetectionStrategy, Component, booleanAttribute, input, output } from '@angular/core';

@Component({
selector: 'asy-modal',
Expand All @@ -13,27 +13,21 @@ export class ModalComponent {
/**
* Title to display in the modal header
*/
@Input()
title = '';
readonly title = input('');

/**
* Text to display on the modal 'ok' button
*/
@Input()
okText = 'OK';
readonly okText = input('OK');

/**
* Text to display on the modal 'cancel' button
*/
@Input()
cancelText?: string = 'Cancel';
readonly cancelText = input('Cancel');

@Input()
disableOk = false;
readonly disableOk = input(false, { transform: booleanAttribute });
readonly hideCancel = input(false, { transform: booleanAttribute });

@Output()
readonly ok = new EventEmitter<void>();

@Output()
readonly cancel = new EventEmitter<void>();
readonly ok = output();
readonly cancel = output();
}
43 changes: 22 additions & 21 deletions src/app/common/multi-select.directive.ts
Original file line number Diff line number Diff line change
@@ -1,59 +1,60 @@
import {
DestroyRef,
Directive,
HostBinding,
Input,
OnInit,
SimpleChange,
inject
booleanAttribute,
inject,
input
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

import { NgSelectComponent } from '@ng-select/ng-select';

@Directive({
selector: 'ng-select[multi-select]',
standalone: true
standalone: true,
// eslint-disable-next-line @angular-eslint/no-host-metadata-property
host: {
['class.ng-hide-arrow-wrapper']: 'hideArrow()'
}
})
export class MultiSelectDirective implements OnInit {
private select = inject(NgSelectComponent);

private destroyRef = inject(DestroyRef);
readonly #select = inject(NgSelectComponent);
readonly #destroyRef = inject(DestroyRef);

@Input()
@HostBinding('class.ng-hide-arrow-wrapper')
hideArrow = true;
readonly hideArrow = input(true, { transform: booleanAttribute });

ngOnInit() {
this.select.addTag = true;
this.select.hideSelected = true;
this.select.multiple = true;
this.#select.addTag = true;
this.#select.hideSelected = true;
this.#select.multiple = true;
this.updateIsOpen(false);

// change detection doesn't work properly when setting items programmatically
// tslint:disable-next-line:no-lifecycle-call
this.select.ngOnChanges({
this.#select.ngOnChanges({
items: new SimpleChange([], [], true)
});

this.select.addEvent.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
this.#select.addEvent.pipe(takeUntilDestroyed(this.#destroyRef)).subscribe(() => {
this.updateIsOpen(false);
});
this.select.searchEvent.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((event) => {
this.#select.searchEvent.pipe(takeUntilDestroyed(this.#destroyRef)).subscribe((event) => {
const isOpen = event.term.trim().length > 0;
this.updateIsOpen(isOpen);
});
// Clear the items on clear event. Fixes bug where cleared items are suggested as options.
this.select.clearEvent.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
this.select.itemsList.setItems([]);
this.#select.clearEvent.pipe(takeUntilDestroyed(this.#destroyRef)).subscribe(() => {
this.#select.itemsList.setItems([]);
});
}

private updateIsOpen(isOpen: boolean) {
// change detection doesn't work properly when setting input programmatically
// tslint:disable-next-line:no-lifecycle-call
const change = new SimpleChange(this.select.isOpen, isOpen, false);
this.select.isOpen = isOpen;
this.select.ngOnChanges({ isOpen: change });
const change = new SimpleChange(this.#select.isOpen, isOpen, false);
this.#select.isOpen = isOpen;
this.#select.ngOnChanges({ isOpen: change });
}
}
2 changes: 1 addition & 1 deletion src/app/common/system-alert/system-alert.component.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@for (alert of alertService.alerts$ | async; track alert; let i = $index) {
@for (alert of alerts(); track alert; let i = $index) {
<div
class="alert alert-{{ alert.type }} alert-dismissible d-flex"
role="alert"
Expand Down
8 changes: 4 additions & 4 deletions src/app/common/system-alert/system-alert.component.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { animate, style, transition, trigger } from '@angular/animations';
import { AsyncPipe } from '@angular/common';
import { Component, inject } from '@angular/core';
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';

import { SystemAlertService } from './system-alert.service';

Expand All @@ -22,10 +21,11 @@ import { SystemAlertService } from './system-alert.service';
])
],
standalone: true,
imports: [AsyncPipe]
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SystemAlertComponent {
alertService = inject(SystemAlertService);
readonly alertService = inject(SystemAlertService);
readonly alerts = this.alertService.alerts;

clearAlert(index: number) {
this.alertService.clear(index);
Expand Down
24 changes: 12 additions & 12 deletions src/app/common/system-alert/system-alert.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@ describe('SystemAlertService', () => {
describe('clearAllAlerts', () => {
it('clears all alerts', () => {
service.clearAllAlerts();
expect(service.getAlerts().length).toBe(0);
expect(service.alerts().length).toBe(0);
});
});

describe('clear', () => {
it('clears single alert by index', () => {
const indexToClear = 3;
service.clear(indexToClear);
const alerts = service.getAlerts();
const alerts = service.alerts();
expect(alerts.length).toBe(4);
for (const alert of alerts) {
expect(alert.msg).not.toBe(testAlertMsgs[indexToClear]);
Expand All @@ -38,9 +38,10 @@ describe('SystemAlertService', () => {
describe('clearAlertById', () => {
it('clears single alert by Id', () => {
const idToClear = 3;
const clearedAlertMsg = service.getAlerts().find((alert) => alert.id === idToClear).msg;
const clearedAlertMsg =
service.alerts().find((alert) => alert.id === idToClear)?.msg ?? 'not found';
service.clearAlertById(idToClear);
const alerts = service.getAlerts();
const alerts = service.alerts();
expect(alerts.length).toBe(4);
for (const alert of alerts) {
expect(alert.msg).not.toBe(clearedAlertMsg);
Expand All @@ -52,7 +53,7 @@ describe('SystemAlertService', () => {
it('new alert is created added to list', () => {
const newAlertMsg = 'added alert';
service.addAlert(newAlertMsg);
const alerts = service.getAlerts();
const alerts = service.alerts();
expect(alerts.length).toBe(6);

const newAlert = alerts[alerts.length - 1];
Expand All @@ -66,7 +67,7 @@ describe('SystemAlertService', () => {
const newAlertType = 'warning';
const newAlertSubtext = 'alert subtext';
service.addAlert(newAlertMsg, newAlertType, null, newAlertSubtext);
const alerts = service.getAlerts();
const alerts = service.alerts();
expect(alerts.length).toBe(6);

const newAlert = alerts[alerts.length - 1];
Expand All @@ -82,12 +83,12 @@ describe('SystemAlertService', () => {
const newAlertMsg = 'added alert';
const spy = spyOn(service, 'clearAlertById').and.callThrough();
service.addAlert(newAlertMsg, 'danger', ttl);
expect(service.getAlerts().length).toBe(6);
expect(service.alerts().length).toBe(6);

jasmine.clock().tick(ttl);

expect(spy).toHaveBeenCalled();
expect(service.getAlerts().length).toBe(5);
expect(service.alerts().length).toBe(5);

jasmine.clock().uninstall();
});
Expand All @@ -104,7 +105,7 @@ describe('SystemAlertService', () => {
}
})
);
const alerts = service.getAlerts();
const alerts = service.alerts();
expect(alerts.length).toBe(6);
expect(alerts[alerts.length - 1].msg).toBe(newAlertMsg);
});
Expand All @@ -118,14 +119,13 @@ describe('SystemAlertService', () => {
}
})
);
const alerts = service.getAlerts();
expect(alerts.length).toBe(5);
expect(service.alerts().length).toBe(5);
});
});

describe('getAlerts', () => {
it('gets all alerts', () => {
const alerts = service.getAlerts();
const alerts = service.alerts();
expect(alerts.length).toBe(5);
for (let i = 0; i < testAlertMsgs.length; i++) {
expect(alerts[i].msg).toBe(testAlertMsgs[i]);
Expand Down
36 changes: 15 additions & 21 deletions src/app/common/system-alert/system-alert.service.ts
Original file line number Diff line number Diff line change
@@ -1,53 +1,47 @@
import { HttpErrorResponse } from '@angular/common/http';
import { Injectable } from '@angular/core';

import { BehaviorSubject } from 'rxjs';
import { Injectable, signal } from '@angular/core';

import { SystemAlert } from './system-alert.model';

@Injectable({
providedIn: 'root'
})
export class SystemAlertService {
private id = 0;
private defaultType = 'danger';
private alerts: SystemAlert[] = [];
alerts$: BehaviorSubject<SystemAlert[]> = new BehaviorSubject(this.alerts);
#id = 0;
#defaultType = 'danger';
#alerts = signal<SystemAlert[]>([]);

alerts = this.#alerts.asReadonly();

clearAllAlerts() {
this.alerts.length = 0;
this.alerts$.next(this.alerts);
this.#alerts.set([]);
}

clear(index: number) {
this.alerts.splice(index, 1);
this.alerts$.next(this.alerts);
this.#alerts.update((a) => {
a.splice(index, 1);
return [...a];
});
}

clearAlertById(id: number) {
const index = this.alerts.findIndex((value) => value.id === id);
const index = this.#alerts().findIndex((value) => value.id === id);
this.clear(index);
}

addAlert(msg: string, type?: string, ttl?: number, subtext?: string) {
const alert = new SystemAlert(this.id++, type || this.defaultType, msg, subtext);

this.alerts.push(alert);
const alert = new SystemAlert(this.#id++, type || this.#defaultType, msg, subtext);

// If they passed in a ttl parameter, age off the alert after said timeout
if (ttl && ttl > 0) {
if (ttl ?? 0 > 0) {
setTimeout(() => this.clearAlertById(alert.id), ttl);
}
this.alerts$.next(this.alerts);
this.#alerts.update((alerts) => [...alerts, alert]);
}

addClientErrorAlert(error: HttpErrorResponse) {
if (error.status >= 400 && error.status < 500) {
this.addAlert(error.error.message);
}
}

getAlerts(): SystemAlert[] {
return this.alerts;
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<ng-container cdkColumnDef="actionsMenu" [sticky]="sticky" [stickyEnd]="stickyEnd">
<ng-container cdkColumnDef="actionsMenu" [sticky]="sticky()" [stickyEnd]="stickyEnd()">
<th cdk-header-cell *cdkHeaderCellDef></th>
<td cdk-cell *cdkCellDef="let item; let index = index">
<button
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<ng-container cdkColumnDef [sticky]="sticky" [stickyEnd]="stickyEnd">
<th asy-sort-header cdk-header-cell *cdkHeaderCellDef [sortable]="sortable">
<ng-container cdkColumnDef [sticky]="sticky()" [stickyEnd]="stickyEnd()">
<th asy-sort-header cdk-header-cell *cdkHeaderCellDef [sortable]="sortable()">
<ng-template #defaultHeaderTemplate>
{{ header ?? (name | titlecase) }}
{{ header() ?? (name | titlecase) }}
</ng-template>
<ng-template
[ngTemplateOutlet]="headerTemplate || defaultHeaderTemplate"
[ngTemplateOutlet]="headerTemplate ?? defaultHeaderTemplate"
[ngTemplateOutletContext]="{
header: header
header: header()
}"
>
</ng-template>
Expand Down
Loading

0 comments on commit 683e822

Please sign in to comment.