|
@@ -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>
|
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
| 2 |
+
|
| 3 |
+
import { BookCard } from './book-card';
|
| 4 |
+
import { Book } from '../../shared/book';
|
| 5 |
+
import { inputBinding, outputBinding } from '@angular/core';
|
| 6 |
+
|
| 7 |
+
describe('BookCard', () => {
|
| 8 |
+
let fixture: ComponentFixture<BookCard>;
|
| 9 |
+
let emittedBook: Book | undefined;
|
| 10 |
+
|
| 11 |
+
const testBook: Book = {
|
| 12 |
+
isbn: '1111',
|
| 13 |
+
title: 'Testbuch',
|
| 14 |
+
subtitle: 'Test',
|
| 15 |
+
authors: ['Author 1', 'Author 2'],
|
| 16 |
+
imageUrl: 'https://cdn.ng-buch.de/test.png',
|
| 17 |
+
description: 'Dies ist ein Testbuch',
|
| 18 |
+
createdAt: new Date().toISOString()
|
| 19 |
+
};
|
| 20 |
+
|
| 21 |
+
beforeEach(async () => {
|
| 22 |
+
emittedBook = undefined;
|
| 23 |
+
|
| 24 |
+
await TestBed.configureTestingModule({
|
| 25 |
+
imports: [BookCard]
|
| 26 |
+
}).compileComponents();
|
| 27 |
+
|
| 28 |
+
fixture = TestBed.createComponent(BookCard, {
|
| 29 |
+
bindings: [
|
| 30 |
+
inputBinding('book', () => testBook),
|
| 31 |
+
outputBinding('like', (book: Book) => emittedBook = book)
|
| 32 |
+
]
|
| 33 |
+
});
|
| 34 |
+
|
| 35 |
+
fixture.detectChanges();
|
| 36 |
+
});
|
| 37 |
+
|
| 38 |
+
it('should emit the like event with the correct book', () => {
|
| 39 |
+
// Event manuell auslösen
|
| 40 |
+
fixture.componentInstance.likeBook();
|
| 41 |
+
|
| 42 |
+
// Prüfen, ob das Event ausgelöst wurde
|
| 43 |
+
expect(emittedBook).toBeDefined();
|
| 44 |
+
expect(emittedBook).toEqual(testBook);
|
| 45 |
+
});
|
| 46 |
+
});
|
|
@@ -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,46 +0,0 @@
|
|
| 1 |
-
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
| 2 |
-
|
| 3 |
-
import { BookCard } from './book-card';
|
| 4 |
-
import { Book } from '../../shared/book';
|
| 5 |
-
import { signal, inputBinding } from '@angular/core';
|
| 6 |
-
|
| 7 |
-
describe('BookCard', () => {
|
| 8 |
-
let fixture: ComponentFixture<BookCard>;
|
| 9 |
-
const testBook = signal<Book>({
|
| 10 |
-
isbn: '1111',
|
| 11 |
-
title: 'Testbuch',
|
| 12 |
-
subtitle: 'Test',
|
| 13 |
-
authors: ['Author 1', 'Author 2'],
|
| 14 |
-
imageUrl: 'https://cdn.ng-buch.de/test.png',
|
| 15 |
-
description: 'Dies ist ein Testbuch',
|
| 16 |
-
createdAt: new Date().toISOString()
|
| 17 |
-
});
|
| 18 |
-
|
| 19 |
-
beforeEach(async () => {
|
| 20 |
-
await TestBed.configureTestingModule({
|
| 21 |
-
imports: [BookCard]
|
| 22 |
-
})
|
| 23 |
-
.compileComponents();
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
fixture = TestBed.createComponent(BookCard, {
|
| 27 |
-
bindings: [inputBinding('book', testBook)]
|
| 28 |
-
});
|
| 29 |
-
|
| 30 |
-
fixture.detectChanges();
|
| 31 |
-
});
|
| 32 |
-
|
| 33 |
-
it('should render book title, subtitle and isbn', () => {
|
| 34 |
-
const compiledElement: HTMLElement = fixture.nativeElement;
|
| 35 |
-
expect(compiledElement.textContent).toContain(testBook().isbn);
|
| 36 |
-
expect(compiledElement.textContent).toContain(testBook().subtitle);
|
| 37 |
-
expect(compiledElement.textContent).toContain(testBook().isbn);
|
| 38 |
-
});
|
| 39 |
-
|
| 40 |
-
it('should display the correct image', () => {
|
| 41 |
-
const compiledElement: HTMLElement = fixture.nativeElement;
|
| 42 |
-
const imageEl = compiledElement.querySelector('img')!;
|
| 43 |
-
expect(imageEl).toBeTruthy();
|
| 44 |
-
expect(imageEl.src).toBe(testBook().imageUrl);
|
| 45 |
-
});
|
| 46 |
-
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,46 +0,0 @@
|
|
| 1 |
-
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
| 2 |
-
|
| 3 |
-
import { BookCard } from './book-card';
|
| 4 |
-
import { Book } from '../../shared/book';
|
| 5 |
-
import { inputBinding } from '@angular/core';
|
| 6 |
-
|
| 7 |
-
describe('BookCard', () => {
|
| 8 |
-
let fixture: ComponentFixture<BookCard>;
|
| 9 |
-
const testBook: Book = {
|
| 10 |
-
isbn: '1111',
|
| 11 |
-
title: 'Testbuch',
|
| 12 |
-
subtitle: 'Test',
|
| 13 |
-
authors: ['Author 1', 'Author 2'],
|
| 14 |
-
imageUrl: 'https://cdn.ng-buch.de/test.png',
|
| 15 |
-
description: 'Dies ist ein Testbuch',
|
| 16 |
-
createdAt: new Date().toISOString()
|
| 17 |
-
};
|
| 18 |
-
|
| 19 |
-
beforeEach(async () => {
|
| 20 |
-
await TestBed.configureTestingModule({
|
| 21 |
-
imports: [BookCard]
|
| 22 |
-
})
|
| 23 |
-
.compileComponents();
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
fixture = TestBed.createComponent(BookCard, {
|
| 27 |
-
bindings: [inputBinding('book', () => testBook)]
|
| 28 |
-
});
|
| 29 |
-
|
| 30 |
-
fixture.detectChanges();
|
| 31 |
-
});
|
| 32 |
-
|
| 33 |
-
it('should render book title, subtitle and isbn', () => {
|
| 34 |
-
const compiledElement: HTMLElement = fixture.nativeElement;
|
| 35 |
-
expect(compiledElement.textContent).toContain(testBook.isbn);
|
| 36 |
-
expect(compiledElement.textContent).toContain(testBook.subtitle);
|
| 37 |
-
expect(compiledElement.textContent).toContain(testBook.isbn);
|
| 38 |
-
});
|
| 39 |
-
|
| 40 |
-
it('should display the correct image', () => {
|
| 41 |
-
const compiledElement: HTMLElement = fixture.nativeElement;
|
| 42 |
-
const imageEl = compiledElement.querySelector('img')!;
|
| 43 |
-
expect(imageEl).toBeTruthy();
|
| 44 |
-
expect(imageEl.src).toBe(testBook.imageUrl);
|
| 45 |
-
});
|
| 46 |
-
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -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 |
}
|