@@ -1,6 +1,7 @@
|
|
1 |
import { NgModule } from '@angular/core';
|
2 |
import { RouterModule, Routes } from '@angular/router';
|
3 |
import { BookCreateComponent } from './book-create/book-create.component';
|
|
|
4 |
|
5 |
const routes: Routes = [
|
6 |
{
|
@@ -10,6 +11,10 @@ const routes: Routes = [
|
|
10 |
{
|
11 |
path: 'admin/create',
|
12 |
component: BookCreateComponent,
|
|
|
|
|
|
|
|
|
13 |
}
|
14 |
];
|
15 |
|
1 |
import { NgModule } from '@angular/core';
|
2 |
import { RouterModule, Routes } from '@angular/router';
|
3 |
import { BookCreateComponent } from './book-create/book-create.component';
|
4 |
+
import { BookEditComponent } from './book-edit/book-edit.component';
|
5 |
|
6 |
const routes: Routes = [
|
7 |
{
|
11 |
{
|
12 |
path: 'admin/create',
|
13 |
component: BookCreateComponent,
|
14 |
+
},
|
15 |
+
{
|
16 |
+
path: 'admin/edit/:isbn',
|
17 |
+
component: BookEditComponent,
|
18 |
}
|
19 |
];
|
20 |
|
@@ -1,20 +1,24 @@
|
|
1 |
import { NgModule } from '@angular/core';
|
2 |
import { CommonModule } from '@angular/common';
|
3 |
-
import {
|
|
|
4 |
|
5 |
import { AdminRoutingModule } from './admin-routing.module';
|
6 |
import { BookFormComponent } from './book-form/book-form.component';
|
7 |
import { BookCreateComponent } from './book-create/book-create.component';
|
|
|
8 |
|
9 |
@NgModule({
|
10 |
declarations: [
|
11 |
BookFormComponent,
|
12 |
-
BookCreateComponent
|
|
|
13 |
],
|
14 |
imports: [
|
15 |
CommonModule,
|
16 |
AdminRoutingModule,
|
17 |
-
|
|
|
18 |
],
|
19 |
})
|
20 |
export class AdminModule { }
|
1 |
import { NgModule } from '@angular/core';
|
2 |
import { CommonModule } from '@angular/common';
|
3 |
+
import { ReactiveFormsModule } from '@angular/forms';
|
4 |
+
import { LocalIsoDateValueAccessor } from 'angular-date-value-accessor';
|
5 |
|
6 |
import { AdminRoutingModule } from './admin-routing.module';
|
7 |
import { BookFormComponent } from './book-form/book-form.component';
|
8 |
import { BookCreateComponent } from './book-create/book-create.component';
|
9 |
+
import { BookEditComponent } from './book-edit/book-edit.component';
|
10 |
|
11 |
@NgModule({
|
12 |
declarations: [
|
13 |
BookFormComponent,
|
14 |
+
BookCreateComponent,
|
15 |
+
BookEditComponent
|
16 |
],
|
17 |
imports: [
|
18 |
CommonModule,
|
19 |
AdminRoutingModule,
|
20 |
+
ReactiveFormsModule,
|
21 |
+
LocalIsoDateValueAccessor
|
22 |
],
|
23 |
})
|
24 |
export class AdminModule { }
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<h1>Edit Book</h1>
|
2 |
+
|
3 |
+
<bm-book-form
|
4 |
+
*ngIf="book$ | async as book"
|
5 |
+
[book]="book"
|
6 |
+
(submitBook)="update($event)"></bm-book-form>
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
2 |
+
|
3 |
+
import { BookEditComponent } from './book-edit.component';
|
4 |
+
|
5 |
+
describe('BookEditComponent', () => {
|
6 |
+
let component: BookEditComponent;
|
7 |
+
let fixture: ComponentFixture<BookEditComponent>;
|
8 |
+
|
9 |
+
beforeEach(async () => {
|
10 |
+
await TestBed.configureTestingModule({
|
11 |
+
declarations: [ BookEditComponent ]
|
12 |
+
})
|
13 |
+
.compileComponents();
|
14 |
+
|
15 |
+
fixture = TestBed.createComponent(BookEditComponent);
|
16 |
+
component = fixture.componentInstance;
|
17 |
+
fixture.detectChanges();
|
18 |
+
});
|
19 |
+
|
20 |
+
it('should create', () => {
|
21 |
+
expect(component).toBeTruthy();
|
22 |
+
});
|
23 |
+
});
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Component } from '@angular/core';
|
2 |
+
import { ActivatedRoute, Router } from '@angular/router';
|
3 |
+
import { map, Observable, switchMap } from 'rxjs';
|
4 |
+
|
5 |
+
import { Book } from '../../shared/book';
|
6 |
+
import { BookStoreService } from '../../shared/book-store.service';
|
7 |
+
|
8 |
+
@Component({
|
9 |
+
selector: 'bm-book-edit',
|
10 |
+
templateUrl: './book-edit.component.html',
|
11 |
+
styleUrls: ['./book-edit.component.css']
|
12 |
+
})
|
13 |
+
export class BookEditComponent {
|
14 |
+
book$: Observable<Book>;
|
15 |
+
|
16 |
+
constructor(
|
17 |
+
private service: BookStoreService,
|
18 |
+
private route: ActivatedRoute,
|
19 |
+
private router: Router
|
20 |
+
) {
|
21 |
+
this.book$ = this.route.paramMap.pipe(
|
22 |
+
map(params => params.get('isbn')!),
|
23 |
+
switchMap(isbn => this.service.getSingle(isbn))
|
24 |
+
);
|
25 |
+
}
|
26 |
+
|
27 |
+
update(book: Book) {
|
28 |
+
this.service.update(book).subscribe(updatedBook => {
|
29 |
+
this.router.navigate(['/books', updatedBook.isbn]);
|
30 |
+
});
|
31 |
+
}
|
32 |
+
}
|
@@ -1,26 +1,33 @@
|
|
1 |
-
<form (ngSubmit)="submitForm()"
|
2 |
<label for="title">Title</label>
|
3 |
-
<input
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
required>
|
8 |
|
9 |
<label for="isbn">ISBN</label>
|
10 |
-
<input
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
17 |
|
18 |
-
<label for="
|
19 |
-
<input
|
20 |
-
name="author"
|
21 |
-
id="author"
|
22 |
-
[(ngModel)]="book.authors[0]"
|
23 |
-
required>
|
24 |
|
25 |
<button type="submit" [disabled]="form.invalid">
|
26 |
Save
|
1 |
+
<form [formGroup]="form" (ngSubmit)="submitForm()">
|
2 |
<label for="title">Title</label>
|
3 |
+
<input id="title" formControlName="title">
|
4 |
+
|
5 |
+
<label for="subtitle">Subtitle</label>
|
6 |
+
<input id="subtitle" formControlName="subtitle">
|
|
|
7 |
|
8 |
<label for="isbn">ISBN</label>
|
9 |
+
<input id="isbn" formControlName="isbn">
|
10 |
+
|
11 |
+
<label>Authors</label>
|
12 |
+
<button type="button" class="add"
|
13 |
+
(click)="addAuthorControl()">
|
14 |
+
+ Author
|
15 |
+
</button>
|
16 |
+
<fieldset formArrayName="authors">
|
17 |
+
<input
|
18 |
+
*ngFor="let a of authors.controls; index as i"
|
19 |
+
[attr.aria-label]="'Author ' + i"
|
20 |
+
[formControlName]="i">
|
21 |
+
</fieldset>
|
22 |
+
|
23 |
+
<label for="description">Description</label>
|
24 |
+
<textarea id="description" formControlName="description"></textarea>
|
25 |
+
|
26 |
+
<label for="published">Published</label>
|
27 |
+
<input type="date" useValueAsLocalIso id="published" formControlName="published">
|
28 |
|
29 |
+
<label for="thumbnailUrl">Thumbnail URL</label>
|
30 |
+
<input type="url" id="thumbnailUrl" formControlName="thumbnailUrl">
|
|
|
|
|
|
|
|
|
31 |
|
32 |
<button type="submit" [disabled]="form.invalid">
|
33 |
Save
|
@@ -1,4 +1,5 @@
|
|
1 |
-
import { Component, Output, EventEmitter } from '@angular/core';
|
|
|
2 |
|
3 |
import { Book } from '../../shared/book';
|
4 |
|
@@ -7,16 +8,81 @@ import { Book } from '../../shared/book';
|
|
7 |
templateUrl: './book-form.component.html',
|
8 |
styleUrls: ['./book-form.component.css']
|
9 |
})
|
10 |
-
export class BookFormComponent {
|
11 |
-
book
|
12 |
-
isbn: '',
|
13 |
-
title: '',
|
14 |
-
authors: ['']
|
15 |
-
};
|
16 |
-
|
17 |
@Output() submitBook = new EventEmitter<Book>();
|
18 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
19 |
submitForm() {
|
20 |
-
this.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
21 |
}
|
22 |
}
|
1 |
+
import { Component, Output, EventEmitter, Input, OnChanges } from '@angular/core';
|
2 |
+
import { FormArray, FormControl, FormGroup, Validators } from '@angular/forms';
|
3 |
|
4 |
import { Book } from '../../shared/book';
|
5 |
|
8 |
templateUrl: './book-form.component.html',
|
9 |
styleUrls: ['./book-form.component.css']
|
10 |
})
|
11 |
+
export class BookFormComponent implements OnChanges {
|
12 |
+
@Input() book?: Book;
|
|
|
|
|
|
|
|
|
|
|
13 |
@Output() submitBook = new EventEmitter<Book>();
|
14 |
|
15 |
+
form = new FormGroup({
|
16 |
+
title: new FormControl('', {
|
17 |
+
nonNullable: true,
|
18 |
+
validators: Validators.required,
|
19 |
+
}),
|
20 |
+
subtitle: new FormControl('', { nonNullable: true }),
|
21 |
+
isbn: new FormControl('', {
|
22 |
+
nonNullable: true,
|
23 |
+
validators: [
|
24 |
+
Validators.required,
|
25 |
+
Validators.minLength(10),
|
26 |
+
Validators.maxLength(13),
|
27 |
+
]
|
28 |
+
}),
|
29 |
+
description: new FormControl('', { nonNullable: true }),
|
30 |
+
published: new FormControl('', { nonNullable: true }),
|
31 |
+
authors: this.buildAuthorsArray(['']),
|
32 |
+
thumbnailUrl: new FormControl('', { nonNullable: true })
|
33 |
+
});
|
34 |
+
|
35 |
+
ngOnChanges(): void {
|
36 |
+
if (this.book) {
|
37 |
+
this.setFormValues(this.book);
|
38 |
+
this.setEditMode(true);
|
39 |
+
} else {
|
40 |
+
this.setEditMode(false);
|
41 |
+
}
|
42 |
+
}
|
43 |
+
|
44 |
+
private setFormValues(book: Book) {
|
45 |
+
this.form.patchValue(book);
|
46 |
+
|
47 |
+
this.form.setControl(
|
48 |
+
'authors',
|
49 |
+
this.buildAuthorsArray(book.authors)
|
50 |
+
);
|
51 |
+
}
|
52 |
+
|
53 |
+
private setEditMode(isEditing: boolean) {
|
54 |
+
const isbnControl = this.form.controls.isbn;
|
55 |
+
|
56 |
+
if (isEditing) {
|
57 |
+
isbnControl.disable();
|
58 |
+
} else {
|
59 |
+
isbnControl.enable();
|
60 |
+
}
|
61 |
+
}
|
62 |
+
|
63 |
+
private buildAuthorsArray(authors: string[]) {
|
64 |
+
return new FormArray(
|
65 |
+
authors.map(v => new FormControl(v, { nonNullable: true }))
|
66 |
+
);
|
67 |
+
}
|
68 |
+
|
69 |
+
get authors() {
|
70 |
+
return this.form.controls.authors;
|
71 |
+
}
|
72 |
+
|
73 |
+
addAuthorControl() {
|
74 |
+
this.authors.push(new FormControl('', { nonNullable: true }));
|
75 |
+
}
|
76 |
+
|
77 |
submitForm() {
|
78 |
+
const formValue = this.form.getRawValue();
|
79 |
+
const authors = formValue.authors.filter(author => !!author);
|
80 |
+
|
81 |
+
const newBook: Book = {
|
82 |
+
...formValue,
|
83 |
+
authors
|
84 |
+
};
|
85 |
+
|
86 |
+
this.submitBook.emit(newBook);
|
87 |
}
|
88 |
}
|
@@ -26,4 +26,8 @@
|
|
26 |
<button class="red" (click)="removeBook(book.isbn)">
|
27 |
Remove book
|
28 |
</button>
|
|
|
|
|
|
|
|
|
29 |
</div>
|
26 |
<button class="red" (click)="removeBook(book.isbn)">
|
27 |
Remove book
|
28 |
</button>
|
29 |
+
<a class="button"
|
30 |
+
[routerLink]="['/admin/edit', book.isbn]">
|
31 |
+
Edit book
|
32 |
+
</a>
|
33 |
</div>
|
@@ -41,4 +41,11 @@ export class BookStoreService {
|
|
41 |
create(book: Book): Observable<Book> {
|
42 |
return this.http.post<Book>(`${this.apiUrl}/books`, book);
|
43 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
44 |
}
|
41 |
create(book: Book): Observable<Book> {
|
42 |
return this.http.post<Book>(`${this.apiUrl}/books`, book);
|
43 |
}
|
44 |
+
|
45 |
+
update(book: Book): Observable<Book> {
|
46 |
+
return this.http.put<Book>(
|
47 |
+
`${this.apiUrl}/books/${book.isbn}`,
|
48 |
+
book
|
49 |
+
);
|
50 |
+
}
|
51 |
}
|