|
@@ -11,4 +11,7 @@
|
|
| 11 |
}
|
| 12 |
ISBN: {{ b.isbn }}
|
| 13 |
</div>
|
|
|
|
|
|
|
|
|
|
| 14 |
</article>
|
|
|
|
| 11 |
}
|
| 12 |
ISBN: {{ b.isbn }}
|
| 13 |
</div>
|
| 14 |
+
<footer>
|
| 15 |
+
<button type="button" class="secondary" (click)="likeBook()">Like</button>
|
| 16 |
+
</footer>
|
| 17 |
</article>
|
|
@@ -1,5 +1,6 @@
|
|
| 1 |
-
import { inputBinding, signal } from '@angular/core';
|
| 2 |
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
|
|
| 3 |
|
| 4 |
import { Book } from '../../shared/book';
|
| 5 |
import { BookCard } from './book-card';
|
|
@@ -15,15 +16,21 @@ describe('BookCard', () => {
|
|
| 15 |
imageUrl: 'https://example.com/test.png',
|
| 16 |
createdAt: '2026-01-01'
|
| 17 |
});
|
|
|
|
| 18 |
|
| 19 |
beforeEach(async () => {
|
|
|
|
|
|
|
| 20 |
await TestBed.configureTestingModule({
|
| 21 |
imports: [BookCard]
|
| 22 |
})
|
| 23 |
.compileComponents();
|
| 24 |
|
| 25 |
fixture = TestBed.createComponent(BookCard, {
|
| 26 |
-
bindings: [
|
|
|
|
|
|
|
|
|
|
| 27 |
});
|
| 28 |
component = fixture.componentInstance;
|
| 29 |
await fixture.whenStable();
|
|
@@ -45,4 +52,9 @@ describe('BookCard', () => {
|
|
| 45 |
expect(imageEl).toBeTruthy();
|
| 46 |
expect(imageEl?.src).toBe(testBook().imageUrl);
|
| 47 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
});
|
|
|
|
| 1 |
+
import { inputBinding, outputBinding, signal } from '@angular/core';
|
| 2 |
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
| 3 |
+
import { Mock } from 'vitest';
|
| 4 |
|
| 5 |
import { Book } from '../../shared/book';
|
| 6 |
import { BookCard } from './book-card';
|
|
|
|
| 16 |
imageUrl: 'https://example.com/test.png',
|
| 17 |
createdAt: '2026-01-01'
|
| 18 |
});
|
| 19 |
+
let likeMock: Mock;
|
| 20 |
|
| 21 |
beforeEach(async () => {
|
| 22 |
+
likeMock = vi.fn();
|
| 23 |
+
|
| 24 |
await TestBed.configureTestingModule({
|
| 25 |
imports: [BookCard]
|
| 26 |
})
|
| 27 |
.compileComponents();
|
| 28 |
|
| 29 |
fixture = TestBed.createComponent(BookCard, {
|
| 30 |
+
bindings: [
|
| 31 |
+
inputBinding('book', testBook),
|
| 32 |
+
outputBinding('like', likeMock)
|
| 33 |
+
]
|
| 34 |
});
|
| 35 |
component = fixture.componentInstance;
|
| 36 |
await fixture.whenStable();
|
|
|
|
| 52 |
expect(imageEl).toBeTruthy();
|
| 53 |
expect(imageEl?.src).toBe(testBook().imageUrl);
|
| 54 |
});
|
| 55 |
+
|
| 56 |
+
it('should emit the like event with the correct book', () => {
|
| 57 |
+
component.likeBook();
|
| 58 |
+
expect(likeMock).toHaveBeenCalledExactlyOnceWith(testBook());
|
| 59 |
+
});
|
| 60 |
});
|
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
import { Component, input } from '@angular/core';
|
| 2 |
|
| 3 |
import { Book } from '../../shared/book';
|
| 4 |
|
|
@@ -10,4 +10,9 @@ import { Book } from '../../shared/book';
|
|
| 10 |
})
|
| 11 |
export class BookCard {
|
| 12 |
readonly book = input.required<Book>();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
}
|
|
|
|
| 1 |
+
import { Component, input, output } from '@angular/core';
|
| 2 |
|
| 3 |
import { Book } from '../../shared/book';
|
| 4 |
|
|
|
|
| 10 |
})
|
| 11 |
export class BookCard {
|
| 12 |
readonly book = input.required<Book>();
|
| 13 |
+
readonly like = output<Book>();
|
| 14 |
+
|
| 15 |
+
likeBook() {
|
| 16 |
+
this.like.emit(this.book());
|
| 17 |
+
}
|
| 18 |
}
|
|
@@ -1,8 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
<section>
|
| 2 |
<h1>Books</h1>
|
| 3 |
<div>
|
| 4 |
@for (b of books(); track b.isbn) {
|
| 5 |
-
<app-book-card [book]="b" />
|
| 6 |
}
|
| 7 |
</div>
|
| 8 |
</section>
|
|
|
|
| 1 |
+
<section>
|
| 2 |
+
<h1>Favorite Books</h1>
|
| 3 |
+
<button type="button" (click)="clearLikedBooks()">Clear</button>
|
| 4 |
+
<ul>
|
| 5 |
+
@for (b of likedBooks(); track b.isbn) {
|
| 6 |
+
<li>{{ b.title }} ({{ b.isbn }})</li>
|
| 7 |
+
} @empty {
|
| 8 |
+
<li>No books liked.</li>
|
| 9 |
+
}
|
| 10 |
+
</ul>
|
| 11 |
+
</section>
|
| 12 |
+
|
| 13 |
<section>
|
| 14 |
<h1>Books</h1>
|
| 15 |
<div>
|
| 16 |
@for (b of books(); track b.isbn) {
|
| 17 |
+
<app-book-card [book]="b" (like)="addLikedBook($event)" />
|
| 18 |
}
|
| 19 |
</div>
|
| 20 |
</section>
|
|
@@ -11,6 +11,7 @@ import { BookCard } from '../book-card/book-card';
|
|
| 11 |
})
|
| 12 |
export class BooksOverviewPage {
|
| 13 |
protected books = signal<Book[]>([]);
|
|
|
|
| 14 |
|
| 15 |
constructor() {
|
| 16 |
this.books.set([
|
|
@@ -34,4 +35,18 @@ export class BooksOverviewPage {
|
|
| 34 |
},
|
| 35 |
]);
|
| 36 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
}
|
|
|
|
| 11 |
})
|
| 12 |
export class BooksOverviewPage {
|
| 13 |
protected books = signal<Book[]>([]);
|
| 14 |
+
protected likedBooks = signal<Book[]>([]);
|
| 15 |
|
| 16 |
constructor() {
|
| 17 |
this.books.set([
|
|
|
|
| 35 |
},
|
| 36 |
]);
|
| 37 |
}
|
| 38 |
+
|
| 39 |
+
addLikedBook(newLikedBook: Book) {
|
| 40 |
+
const foundBook = this.likedBooks().find(
|
| 41 |
+
(b) => b.isbn === newLikedBook.isbn
|
| 42 |
+
);
|
| 43 |
+
|
| 44 |
+
if (!foundBook) {
|
| 45 |
+
this.likedBooks.update((likedBooks) => [...likedBooks, newLikedBook]);
|
| 46 |
+
}
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
clearLikedBooks() {
|
| 50 |
+
this.likedBooks.set([]);
|
| 51 |
+
}
|
| 52 |
}
|