Skip to content

Commit ea50976

Browse files
refactor: migrate to signal inputs
1 parent 5fd2aed commit ea50976

17 files changed

+66
-58
lines changed

src/bhs-web-angular-app/eslint.config.mjs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,12 @@ export default tseslint.config(gitignore(), {
264264
style: 'kebab-case',
265265
},
266266
],
267+
'@angular-eslint/prefer-signals': [
268+
'error',
269+
{
270+
useTypeChecking: true,
271+
},
272+
],
267273

268274
// Allow Angular modules with only a decorator.
269275
'@typescript-eslint/no-extraneous-class': [

src/bhs-web-angular-app/src/app/features/blog/components/categories-list-view/categories-list-view.component.html

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
@if (!isLoading) {
1+
@if (!isLoading()) {
22
<div class="list-group list-group-flush">
3-
@if (error) {
3+
@if (error()) {
44
<span class="list-group-item list-group-item-danger">
5-
{{error}}
5+
{{error()}}
66
</span>
77
}
88

9-
@for (category of categories; track category.slug) {
9+
@for (category of categories(); track category.slug) {
1010
<a [routerLink]="['/apps/blog/category', category.slug]"
1111
class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
1212
{{category.name}}

src/bhs-web-angular-app/src/app/features/blog/components/categories-list-view/categories-list-view.component.spec.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,31 +26,31 @@ describe('CategoriesListViewComponent', () => {
2626
});
2727

2828
it('should show loading', () => {
29-
expect(component.isLoading).toBe(false); // off at first
29+
expect(component.isLoading()).toBe(false); // off at first
3030

3131
fixture.componentRef.setInput('isLoading', true);
3232
fixture.detectChanges();
3333

3434
const element = fixture.nativeElement as HTMLElement;
3535

36-
expect(component.isLoading).toBe(true);
36+
expect(component.isLoading()).toBe(true);
3737
expect(element.querySelector('.list-group .placeholder-glow')).toBeTruthy(); // should show placeholder
3838
});
3939

4040
it('should show error', () => {
41-
expect(component.error).toBeUndefined(); // no error at first
41+
expect(component.error()).toBeUndefined(); // no error at first
4242

4343
fixture.componentRef.setInput('error', 'An error occurred.');
4444
fixture.detectChanges();
4545

4646
const element = fixture.nativeElement as HTMLElement;
4747

48-
expect(component.error).toBeDefined();
48+
expect(component.error()).toBeDefined();
4949
expect(element.querySelector('.list-group .list-group-item-danger')).toBeTruthy(); // should show danger banner
5050
});
5151

5252
it('should show each category', () => {
53-
expect(component.categories).toHaveLength(0); // empty at first
53+
expect(component.categories()).toHaveLength(0); // empty at first
5454

5555
fixture.componentRef.setInput('categories', [{
5656
slug: 'newsletters',
@@ -65,7 +65,7 @@ describe('CategoriesListViewComponent', () => {
6565

6666
const element = fixture.nativeElement as HTMLElement;
6767

68-
expect(component.categories).toHaveLength(2);
68+
expect(component.categories()).toHaveLength(2);
6969
expect(element.querySelector('.list-group')?.children).toHaveLength(2);
7070

7171
const linkDebugElements = fixture.debugElement.queryAll(By.directive(RouterLink));
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
1+
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
22
import { RouterLink } from '@angular/router';
33
import { CategorySummary } from '@data/blog';
44

@@ -10,7 +10,7 @@ import { CategorySummary } from '@data/blog';
1010
imports: [RouterLink],
1111
})
1212
export class CategoriesListViewComponent {
13-
@Input() isLoading = false;
14-
@Input() error?: string | null;
15-
@Input() categories: Array<CategorySummary> = [];
13+
readonly isLoading = input(false);
14+
readonly error = input<string | null>();
15+
readonly categories = input<Array<CategorySummary>>([]);
1616
}

src/bhs-web-angular-app/src/app/features/blog/components/edit-blog-entry-form/edit-blog-entry-form.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
<div class="mb-3">
99
<label for="inputCategories" class="form-label">Categories</label>
1010
<select multiple class="form-select" id="inputCategories" name="categories" formControlName="categories">
11-
@for (c of allCategories; track c.slug) {
11+
@for (c of allCategories(); track c.slug) {
1212
<option [ngValue]="c.slug">{{c.name}}</option>
1313
}
1414
</select>

src/bhs-web-angular-app/src/app/features/blog/components/edit-blog-entry-form/edit-blog-entry-form.component.ts

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ChangeDetectionStrategy, Component, EventEmitter, inject, Input, OnChanges, Output, signal } from '@angular/core';
1+
import { ChangeDetectionStrategy, Component, EventEmitter, inject, input, OnChanges, Output, signal } from '@angular/core';
22
import { FormBuilder, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
33
import { RouterLink } from '@angular/router';
44
import { BsDatepickerModule } from 'ngx-bootstrap/datepicker';
@@ -20,39 +20,40 @@ import { Category, categorySchema, Post, PostRequest } from '@data/blog';
2020
export class EditBlogEntryFormComponent implements OnChanges {
2121
private readonly fb = inject(FormBuilder);
2222

23-
@Input() initialPost?: Post;
24-
@Input() currentAuthor?: Author | null;
25-
@Input() allCategories: Array<Category> = [];
23+
readonly initialPost = input<Post>();
24+
readonly currentAuthor = input<Author | null>();
25+
readonly allCategories = input<Array<Category>>([]);
2626

2727
@Output() readonly publish = new EventEmitter<PostRequest>();
2828

29-
cancelRoute = signal<Array<string>>(['/apps/blog']);
29+
readonly cancelRoute = signal<Array<string>>(['/apps/blog']);
3030
editFormGroup = this.fb.nonNullable.group({
3131
title: ['', [Validators.required]],
3232
categories: [[] as Array<string>],
3333
publishDate: [new Date()],
3434
contentMarkdown: [''],
3535
});
3636

37-
authorWarning = signal<string | null>(null);
37+
readonly authorWarning = signal<string | null>(null);
3838

3939
ngOnChanges(): void {
40-
this.cancelRoute.set(this.initialPost ? ['/apps/blog/entry', this.initialPost.slug] : ['/apps/blog']);
40+
const initialPost = this.initialPost();
41+
this.cancelRoute.set(initialPost ? ['/apps/blog/entry', initialPost.slug] : ['/apps/blog']);
4142

4243
this.editFormGroup.reset({
43-
title: this.initialPost?.title ?? '',
44-
categories: this.initialPost?.categories.map(x => x.slug) ?? [],
45-
publishDate: this.initialPost?.datePublished ?? new Date(),
46-
contentMarkdown: this.initialPost?.contentMarkdown ?? '',
44+
title: initialPost?.title ?? '',
45+
categories: initialPost?.categories.map(x => x.slug) ?? [],
46+
publishDate: initialPost?.datePublished ?? new Date(),
47+
contentMarkdown: initialPost?.contentMarkdown ?? '',
4748
});
4849

4950
this.authorWarning.set(this.isChangingAuthor()
50-
? `Author is changing from '${this.initialPost?.author?.username ?? '(null)'}' to '${this.currentAuthor?.username ?? '(null)'}'.`
51+
? `Author is changing from '${initialPost?.author?.username ?? '(null)'}' to '${this.currentAuthor()?.username ?? '(null)'}'.`
5152
: null);
5253
}
5354

5455
private isChangingAuthor(): boolean {
55-
return !this.initialPost?.author && !!this.currentAuthor;
56+
return !this.initialPost()?.author && !!this.currentAuthor();
5657
}
5758

5859
onSubmit(): void {
@@ -61,11 +62,11 @@ export class EditBlogEntryFormComponent implements OnChanges {
6162
const request: PostRequest = {
6263
title: raw.title,
6364
contentMarkdown: raw.contentMarkdown,
64-
filePath: this.initialPost?.filePath ?? null,
65-
photosAlbumSlug: this.initialPost?.photosAlbumSlug ?? null,
66-
author: this.isChangingAuthor() ? this.currentAuthor ?? null : this.initialPost?.author ?? null, // TODO: support multiple authors.
65+
filePath: this.initialPost()?.filePath ?? null,
66+
photosAlbumSlug: this.initialPost()?.photosAlbumSlug ?? null,
67+
author: this.isChangingAuthor() ? this.currentAuthor() ?? null : this.initialPost()?.author ?? null, // TODO: support multiple authors.
6768
datePublished: raw.publishDate,
68-
categories: this.allCategories.filter(c => raw.categories.includes(c.slug)).map(c => categorySchema.parse(c)),
69+
categories: this.allCategories().filter(c => raw.categories.includes(c.slug)).map(c => categorySchema.parse(c)),
6970
};
7071

7172
this.publish.emit(request);

src/bhs-web-angular-app/src/app/features/blog/components/entry-album/entry-album.component.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<div>
22
<p class="text-muted">This post contains photos:</p>
3-
@for (photo of album.photos; track photo.id) {
4-
<a [routerLink]="['/apps/photos/album', album.slug, 'photo', photo.id]" class="d-inline-block m-1">
3+
@for (photo of album().photos; track photo.id) {
4+
<a [routerLink]="['/apps/photos/album', album().slug, 'photo', photo.id]" class="d-inline-block m-1">
55
<img [ngSrc]="photo.imagePath" class="img-fluid" [alt]="photo.name" width="200" height="200">
66
</a>
77
}

src/bhs-web-angular-app/src/app/features/blog/components/entry-album/entry-album.component.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,14 @@ describe.skip('EntryAlbumComponent', () => {
2222

2323
fixture = TestBed.createComponent(EntryAlbumComponent);
2424
component = fixture.componentInstance;
25-
component.album = {
25+
fixture.componentRef.setInput('album', {
2626
slug: 'album-one',
2727
photos: [{
2828
id: 'photo-one',
2929
imagePath: 'http://example.com/photo-one.jpg',
3030
datePosted: new Date(),
3131
}],
32-
};
32+
});
3333
fixture.detectChanges();
3434
});
3535

src/bhs-web-angular-app/src/app/features/blog/components/entry-album/entry-album.component.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { NgOptimizedImage } from '@angular/common';
2-
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
2+
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
33
import { RouterLink } from '@angular/router';
44
import { AlbumPhotos } from '@data/photos';
55

@@ -11,5 +11,5 @@ import { AlbumPhotos } from '@data/photos';
1111
imports: [RouterLink, NgOptimizedImage],
1212
})
1313
export class EntryAlbumComponent {
14-
@Input({ required: true }) album!: AlbumPhotos;
14+
readonly album = input.required<AlbumPhotos>();
1515
}

src/bhs-web-angular-app/src/app/features/blog/components/post-card/post-card.component.html

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,31 @@
11
<div class="card">
22
<div class="card-header">
33
<h2 class="card-title">
4-
<a [routerLink]="['/apps/blog/entry', post.slug]">
5-
{{post.title}}
4+
<a [routerLink]="['/apps/blog/entry', post().slug]">
5+
{{post().title}}
66
</a>
77
</h2>
88
<p class="card-subtitle">
99
Posted
10-
@if (post.author) {
10+
@let author = post().author;
11+
@if (author) {
1112
by
12-
<a [routerLink]="['/apps/profile', post.author.username]">{{post.author.name}}</a>
13+
<a [routerLink]="['/apps/profile', author.username]">{{author.name}}</a>
1314
}
1415
on
15-
<app-date [datetime]="post.datePublished" />
16+
<app-date [datetime]="post().datePublished" />
1617
</p>
1718
</div>
1819
<div class="card-body">
19-
<p>{{post.contentPreview}}</p>
20+
<p>{{post().contentPreview}}</p>
2021
<p>
21-
<a class="btn btn-secondary" role="button" [routerLink]="['/apps/blog/entry', post.slug]">
22+
<a class="btn btn-secondary" role="button" [routerLink]="['/apps/blog/entry', post().slug]">
2223
Read Full Post &raquo;
2324
</a>
2425
</p>
2526
</div>
2627
<div class="card-footer">
27-
@for (cat of post.categories; track cat.slug) {
28+
@for (cat of post().categories; track cat.slug) {
2829
<span class="me-2">{{cat.name}}</span>
2930
}
3031
</div>

0 commit comments

Comments
 (0)