BookMonkey 3 Diff

Files changed (16) hide show
  1. tmp/src/app/book-monkey/{iteration-3/interceptors → iteration-4/custom-validation}/app-routing.module.ts +15 -0
  2. tmp/src/app/book-monkey/{iteration-3/interceptors → iteration-4/custom-validation}/app.component.html +1 -0
  3. tmp/src/app/book-monkey/{iteration-3/interceptors → iteration-4/custom-validation}/app.module.ts +14 -2
  4. tmp/src/app/book-monkey/{iteration-3/interceptors → iteration-4/custom-validation}/book-details/book-details.component.html +4 -0
  5. tmp/src/app/book-monkey/{iteration-3/interceptors → iteration-4/custom-validation}/book-form/book-form.component.html +75 -0
  6. tmp/src/app/book-monkey/{iteration-3/interceptors → iteration-4/custom-validation}/book-form/book-form.component.ts +119 -0
  7. tmp/src/app/book-monkey/{iteration-3/interceptors → iteration-4/custom-validation}/create-book/create-book.component.html +3 -0
  8. tmp/src/app/book-monkey/{iteration-3/interceptors → iteration-4/custom-validation}/create-book/create-book.component.ts +29 -0
  9. tmp/src/app/book-monkey/{iteration-3/interceptors → iteration-4/custom-validation}/edit-book/edit-book.component.html +8 -0
  10. tmp/src/app/book-monkey/{iteration-3/interceptors → iteration-4/custom-validation}/edit-book/edit-book.component.ts +37 -0
  11. tmp/src/app/book-monkey/{iteration-3/interceptors → iteration-4/custom-validation}/form-messages/form-messages.component.html +4 -0
  12. tmp/src/app/book-monkey/{iteration-3/interceptors → iteration-4/custom-validation}/form-messages/form-messages.component.ts +50 -0
  13. tmp/src/app/book-monkey/{iteration-3/interceptors → iteration-4/custom-validation}/shared/book-exists-validator.service.ts +25 -0
  14. tmp/src/app/book-monkey/{iteration-3/interceptors → iteration-4/custom-validation}/shared/book-factory.ts +1 -0
  15. tmp/src/app/book-monkey/{iteration-3/interceptors → iteration-4/custom-validation}/shared/book-store.service.ts +28 -0
  16. tmp/src/app/book-monkey/{iteration-3/interceptors → iteration-4/custom-validation}/shared/book.validators.ts +30 -0
tmp/src/app/book-monkey/{iteration-3/interceptors → iteration-4/custom-validation}/app-routing.module.ts RENAMED
@@ -4,6 +4,8 @@
4
import { HomeComponent } from './home/home.component';
5
import { BookListComponent } from './book-list/book-list.component';
6
import { BookDetailsComponent } from './book-details/book-details.component';
7
8
export const routes: Routes = [
9
{
@@ -22,6 +24,19 @@
22
{
23
path: 'books/:isbn',
24
component: BookDetailsComponent
25
}
26
];
27
4
import { HomeComponent } from './home/home.component';
5
import { BookListComponent } from './book-list/book-list.component';
6
import { BookDetailsComponent } from './book-details/book-details.component';
7
+ import { CreateBookComponent } from './create-book/create-book.component';
8
+ import { EditBookComponent } from './edit-book/edit-book.component';
9
10
export const routes: Routes = [
11
{
24
{
25
path: 'books/:isbn',
26
component: BookDetailsComponent
27
+ },
28
+ {
29
+ path: 'admin',
30
+ redirectTo: 'admin/create',
31
+ pathMatch: 'full'
32
+ },
33
+ {
34
+ path: 'admin/create',
35
+ component: CreateBookComponent
36
+ },
37
+ {
38
+ path: 'admin/edit/:isbn',
39
+ component: EditBookComponent
40
}
41
];
42
tmp/src/app/book-monkey/{iteration-3/interceptors → iteration-4/custom-validation}/app.component.html RENAMED
@@ -1,5 +1,6 @@
1
<div class="ui menu">
2
<a routerLink="home" routerLinkActive="active" class="item">Home</a>
3
<a routerLink="books" routerLinkActive="active" class="item">Bücher</a>
4
</div>
5
<router-outlet></router-outlet>
1
<div class="ui menu">
2
<a routerLink="home" routerLinkActive="active" class="item">Home</a>
3
<a routerLink="books" routerLinkActive="active" class="item">Bücher</a>
4
+ <a routerLink="admin" routerLinkActive="active" class="item">Administration</a>
5
</div>
6
<router-outlet></router-outlet>
tmp/src/app/book-monkey/{iteration-3/interceptors → iteration-4/custom-validation}/app.module.ts RENAMED
@@ -1,6 +1,8 @@
1
import { CommonModule } from '@angular/common';
2
import { NgModule } from '@angular/core';
3
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
4
5
import { AppRoutingModule } from './app-routing.module.one-app';
6
import { AppComponent } from './app.component';
@@ -10,6 +12,10 @@
10
import { BookDetailsComponent } from './book-details/book-details.component';
11
import { SearchComponent } from './search/search.component';
12
import { TokenInterceptor } from './shared/token-interceptor';
13
14
@NgModule({
15
declarations: [
@@ -18,12 +24,18 @@
18
BookListComponent,
19
BookListItemComponent,
20
BookDetailsComponent,
21
- SearchComponent
22
],
23
imports: [
24
CommonModule,
25
HttpClientModule,
26
- AppRoutingModule
27
],
28
providers: [
29
{ provide: HTTP_INTERCEPTORS, useClass: TokenInterceptor, multi: true }
1
import { CommonModule } from '@angular/common';
2
import { NgModule } from '@angular/core';
3
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
4
+ import { ReactiveFormsModule } from '@angular/forms';
5
+ import { DateValueAccessorModule } from 'angular-date-value-accessor';
6
7
import { AppRoutingModule } from './app-routing.module.one-app';
8
import { AppComponent } from './app.component';
12
import { BookDetailsComponent } from './book-details/book-details.component';
13
import { SearchComponent } from './search/search.component';
14
import { TokenInterceptor } from './shared/token-interceptor';
15
+ import { BookFormComponent } from './book-form/book-form.component';
16
+ import { CreateBookComponent } from './create-book/create-book.component';
17
+ import { FormMessagesComponent } from './form-messages/form-messages.component';
18
+ import { EditBookComponent } from './edit-book/edit-book.component';
19
20
@NgModule({
21
declarations: [
24
BookListComponent,
25
BookListItemComponent,
26
BookDetailsComponent,
27
+ SearchComponent,
28
+ BookFormComponent,
29
+ CreateBookComponent,
30
+ FormMessagesComponent,
31
+ EditBookComponent,
32
],
33
imports: [
34
CommonModule,
35
HttpClientModule,
36
+ AppRoutingModule,
37
+ ReactiveFormsModule,
38
+ DateValueAccessorModule
39
],
40
providers: [
41
{ provide: HTTP_INTERCEPTORS, useClass: TokenInterceptor, multi: true }
tmp/src/app/book-monkey/{iteration-3/interceptors → iteration-4/custom-validation}/book-details/book-details.component.html RENAMED
@@ -33,6 +33,10 @@
33
(click)="removeBook()">
34
<i class="remove icon"></i> Buch löschen
35
</button>
36
</div>
37
38
<ng-template #loading>
33
(click)="removeBook()">
34
<i class="remove icon"></i> Buch löschen
35
</button>
36
+ <a class="ui tiny yellow labeled icon button"
37
+ [routerLink]="['../../admin/edit', book.isbn]">
38
+ <i class="write icon"></i> Buch bearbeiten
39
+ </a>
40
</div>
41
42
<ng-template #loading>
tmp/src/app/book-monkey/{iteration-3/interceptors → iteration-4/custom-validation}/book-form/book-form.component.html RENAMED
@@ -0,0 +1,75 @@
1
+ <form class="ui form"
2
+ [formGroup]="bookForm"
3
+ (ngSubmit)="submitForm()">
4
+
5
+ <label>Buchtitel</label>
6
+ <input formControlName="title">
7
+ <bm-form-messages
8
+ [control]="bookForm.get('title')"
9
+ controlName="title">
10
+ </bm-form-messages>
11
+
12
+ <label>Untertitel</label>
13
+ <input formControlName="subtitle">
14
+
15
+ <label>ISBN</label>
16
+ <input formControlName="isbn">
17
+ <bm-form-messages
18
+ [control]="bookForm.get('isbn')"
19
+ controlName="isbn">
20
+ </bm-form-messages>
21
+
22
+ <label>Erscheinungsdatum</label>
23
+ <input type="date"
24
+ useValueAsDate
25
+ formControlName="published">
26
+ <bm-form-messages
27
+ [control]="bookForm.get('published')"
28
+ controlName="published">
29
+ </bm-form-messages>
30
+
31
+ <label>Autoren</label>
32
+ <button type="button" class="ui mini button"
33
+ (click)="addAuthorControl()">
34
+ + Autor
35
+ </button>
36
+ <div class="fields" formArrayName="authors">
37
+ <div class="sixteen wide field"
38
+ *ngFor="let c of authors.controls; index as i">
39
+ <input placeholder="Autor"
40
+ [formControlName]="i">
41
+ </div>
42
+ </div>
43
+ <bm-form-messages
44
+ [control]="bookForm.get('authors')"
45
+ controlName="authors">
46
+ </bm-form-messages>
47
+
48
+ <label>Beschreibung</label>
49
+ <textarea formControlName="description"></textarea>
50
+
51
+ <label>Bilder</label>
52
+ <button type="button" class="ui mini button"
53
+ (click)="addThumbnailControl()">
54
+ + Bild
55
+ </button>
56
+ <div formArrayName="thumbnails">
57
+ <div class="fields"
58
+ *ngFor="let c of thumbnails.controls; index as i"
59
+ [formGroupName]="i">
60
+ <div class="nine wide field">
61
+ <input placeholder="URL"
62
+ formControlName="url">
63
+ </div>
64
+ <div class="seven wide field">
65
+ <input placeholder="Titel"
66
+ formControlName="title">
67
+ </div>
68
+ </div>
69
+ </div>
70
+
71
+ <button class="ui button" type="submit"
72
+ [disabled]="bookForm.invalid">
73
+ Speichern
74
+ </button>
75
+ </form>
tmp/src/app/book-monkey/{iteration-3/interceptors → iteration-4/custom-validation}/book-form/book-form.component.ts RENAMED
@@ -0,0 +1,119 @@
1
+ import { Component, OnInit, Input, Output, EventEmitter, OnChanges } from '@angular/core';
2
+ import { FormBuilder, FormGroup, FormArray, Validators } from '@angular/forms';
3
+
4
+ import { Book, Thumbnail } from '../shared/book';
5
+ import { BookValidators } from '../shared/book.validators';
6
+ import { BookExistsValidatorService } from '../shared/book-exists-validator.service';
7
+
8
+ @Component({
9
+ selector: 'bm-book-form',
10
+ templateUrl: './book-form.component.html',
11
+ styleUrls: ['./book-form.component.css']
12
+ })
13
+ export class BookFormComponent implements OnInit, OnChanges {
14
+
15
+ bookForm: FormGroup;
16
+
17
+ @Input() book: Book;
18
+ @Input() editing = false;
19
+ @Output() submitBook = new EventEmitter<Book>();
20
+
21
+ constructor(
22
+ private fb: FormBuilder,
23
+ private bookExistsValidator: BookExistsValidatorService
24
+ ) { }
25
+
26
+ ngOnInit() {
27
+ this.initForm();
28
+ }
29
+
30
+ ngOnChanges() {
31
+ this.initForm();
32
+ this.setFormValues(this.book);
33
+ }
34
+
35
+ private setFormValues(book: Book) {
36
+ this.bookForm.patchValue(book);
37
+
38
+ this.bookForm.setControl(
39
+ 'authors',
40
+ this.buildAuthorsArray(book.authors)
41
+ );
42
+
43
+ this.bookForm.setControl(
44
+ 'thumbnails',
45
+ this.buildThumbnailsArray(book.thumbnails)
46
+ );
47
+ }
48
+
49
+ private initForm() {
50
+ if (this.bookForm) { return; }
51
+
52
+ this.bookForm = this.fb.group({
53
+ title: ['', Validators.required],
54
+ subtitle: [''],
55
+ isbn: [
56
+ { value: '', disabled: this.editing },
57
+ [
58
+ Validators.required,
59
+ BookValidators.isbnFormat
60
+ ],
61
+ this.editing ? null : [this.bookExistsValidator]
62
+ ],
63
+ description: [''],
64
+ authors: this.buildAuthorsArray(['']),
65
+ thumbnails: this.buildThumbnailsArray([
66
+ { title: '', url: '' }
67
+ ]),
68
+ published: []
69
+ });
70
+ }
71
+
72
+ private buildAuthorsArray(values: string[]): FormArray {
73
+ return this.fb.array(values, BookValidators.atLeastOneAuthor);
74
+ }
75
+
76
+ private buildThumbnailsArray(values: Thumbnail[]): FormArray {
77
+ return this.fb.array(
78
+ values.map(t => this.fb.group(t))
79
+ );
80
+ }
81
+
82
+ get authors(): FormArray {
83
+ return this.bookForm.get('authors') as FormArray;
84
+ }
85
+
86
+ get thumbnails(): FormArray {
87
+ return this.bookForm.get('thumbnails') as FormArray;
88
+ }
89
+
90
+ addAuthorControl() {
91
+ this.authors.push(this.fb.control(''));
92
+ }
93
+
94
+ addThumbnailControl() {
95
+ this.thumbnails.push(
96
+ this.fb.group({ url: '', title: '' })
97
+ );
98
+ }
99
+
100
+ submitForm() {
101
+ const formValue = this.bookForm.value;
102
+ const authors = formValue.authors
103
+ .filter(author => author);
104
+ const thumbnails = formValue.thumbnails
105
+ .filter(thumbnail => thumbnail.url);
106
+
107
+ const isbn = this.editing ? this.book.isbn : formValue.isbn;
108
+
109
+ const newBook: Book = {
110
+ ...formValue,
111
+ isbn,
112
+ authors,
113
+ thumbnails
114
+ };
115
+
116
+ this.submitBook.emit(newBook);
117
+ this.bookForm.reset();
118
+ }
119
+ }
tmp/src/app/book-monkey/{iteration-3/interceptors → iteration-4/custom-validation}/create-book/create-book.component.html RENAMED
@@ -0,0 +1,3 @@
1
+ <h1>Buch hinzufügen</h1>
2
+
3
+ <bm-book-form (submitBook)="createBook($event)"></bm-book-form>
tmp/src/app/book-monkey/{iteration-3/interceptors → iteration-4/custom-validation}/create-book/create-book.component.ts RENAMED
@@ -0,0 +1,29 @@
1
+ import { Component, OnInit } from '@angular/core';
2
+ import { ActivatedRoute, Router } from '@angular/router';
3
+
4
+ import { Book } from '../shared/book';
5
+ import { BookStoreService } from '../shared/book-store.service';
6
+
7
+ @Component({
8
+ selector: 'bm-create-book',
9
+ templateUrl: './create-book.component.html',
10
+ styleUrls: ['./create-book.component.css']
11
+ })
12
+ export class CreateBookComponent implements OnInit {
13
+
14
+ constructor(
15
+ private bs: BookStoreService,
16
+ private route: ActivatedRoute,
17
+ private router: Router
18
+ ) { }
19
+
20
+ ngOnInit() {
21
+ }
22
+
23
+ createBook(book: Book) {
24
+ this.bs.create(book).subscribe(() => {
25
+ this.router.navigate(['../..', 'books'], { relativeTo: this.route });
26
+ });
27
+ }
28
+
29
+ }
tmp/src/app/book-monkey/{iteration-3/interceptors → iteration-4/custom-validation}/edit-book/edit-book.component.html RENAMED
@@ -0,0 +1,8 @@
1
+ <h1>Buch bearbeiten</h1>
2
+
3
+ <bm-book-form
4
+ *ngIf="book"
5
+ (submitBook)="updateBook($event)"
6
+ [book]="book"
7
+ [editing]="true"
8
+ ></bm-book-form>
tmp/src/app/book-monkey/{iteration-3/interceptors → iteration-4/custom-validation}/edit-book/edit-book.component.ts RENAMED
@@ -0,0 +1,37 @@
1
+ import { Component, OnInit } from '@angular/core';
2
+ import { ActivatedRoute, Router } from '@angular/router';
3
+ import { map, switchMap } from 'rxjs/operators';
4
+
5
+ import { Book } from '../shared/book';
6
+ import { BookStoreService } from '../shared/book-store.service';
7
+
8
+ @Component({
9
+ selector: 'bm-edit-book',
10
+ templateUrl: './edit-book.component.html',
11
+ styleUrls: ['./edit-book.component.css']
12
+ })
13
+ export class EditBookComponent implements OnInit {
14
+
15
+ book: Book;
16
+
17
+ constructor(
18
+ private bs: BookStoreService,
19
+ private route: ActivatedRoute,
20
+ private router: Router
21
+ ) { }
22
+
23
+ ngOnInit() {
24
+ this.route.paramMap.pipe(
25
+ map(params => params.get('isbn')),
26
+ switchMap((isbn: string) => this.bs.getSingle(isbn))
27
+ )
28
+ .subscribe(book => this.book = book);
29
+ }
30
+
31
+ updateBook(book: Book) {
32
+ this.bs.update(book).subscribe(() => {
33
+ this.router.navigate(['../../..', 'books', book.isbn], { relativeTo: this.route });
34
+ });
35
+ }
36
+
37
+ }
tmp/src/app/book-monkey/{iteration-3/interceptors → iteration-4/custom-validation}/form-messages/form-messages.component.html RENAMED
@@ -0,0 +1,4 @@
1
+ <div class="ui negative message"
2
+ *ngFor="let msg of errorsForControl()">
3
+ {{ msg }}
4
+ </div>
tmp/src/app/book-monkey/{iteration-3/interceptors → iteration-4/custom-validation}/form-messages/form-messages.component.ts RENAMED
@@ -0,0 +1,50 @@
1
+ import { Component, OnInit, Input } from '@angular/core';
2
+ import { AbstractControl } from '@angular/forms';
3
+
4
+ @Component({
5
+ selector: 'bm-form-messages',
6
+ templateUrl: './form-messages.component.html',
7
+ styleUrls: ['./form-messages.component.css']
8
+ })
9
+ export class FormMessagesComponent implements OnInit {
10
+
11
+ @Input() control: AbstractControl;
12
+ @Input() controlName: string;
13
+
14
+ private allMessages = {
15
+ title: {
16
+ required: 'Ein Buchtitel muss angegeben werden.'
17
+ },
18
+ isbn: {
19
+ required: 'Es muss eine ISBN angegeben werden.',
20
+ isbnFormat: 'Die ISBN muss aus 10 oder 13 Zeichen bestehen.',
21
+ isbnExists: 'Die ISBN existiert bereits.'
22
+ },
23
+ published: {
24
+ required: 'Es muss ein Erscheinungsdatum angegeben werden.'
25
+ },
26
+ authors: {
27
+ atLeastOneAuthor: 'Es muss ein Autor angegeben werden.'
28
+ }
29
+ };
30
+
31
+ constructor() { }
32
+
33
+ ngOnInit() {
34
+ }
35
+
36
+ errorsForControl(): string[] {
37
+ const messages = this.allMessages[this.controlName];
38
+
39
+ if (
40
+ !this.control ||
41
+ !this.control.errors ||
42
+ !messages ||
43
+ !this.control.dirty
44
+ ) { return null; }
45
+
46
+ return Object.keys(this.control.errors)
47
+ .map(err => messages[err]);
48
+ }
49
+
50
+ }
tmp/src/app/book-monkey/{iteration-3/interceptors → iteration-4/custom-validation}/shared/book-exists-validator.service.ts RENAMED
@@ -0,0 +1,25 @@
1
+ import { Injectable } from '@angular/core';
2
+ import { FormControl, AsyncValidator, ValidationErrors } from '@angular/forms';
3
+ import { Observable, of } from 'rxjs';
4
+ import { map, catchError } from 'rxjs/operators';
5
+
6
+ import { BookStoreService } from './book-store.service';
7
+
8
+ @Injectable({
9
+ providedIn: 'root'
10
+ })
11
+ export class BookExistsValidatorService implements AsyncValidator {
12
+
13
+ constructor(private bs: BookStoreService) { }
14
+
15
+ validate(
16
+ control: FormControl
17
+ ): Observable<ValidationErrors | null> {
18
+ return this.bs.check(control.value).pipe(
19
+ map(exists => (exists === false) ? null : {
20
+ isbnExists: { valid: false }
21
+ }),
22
+ catchError(() => of(null))
23
+ );
24
+ }
25
+ }
tmp/src/app/book-monkey/{iteration-3/interceptors → iteration-4/custom-validation}/shared/book-factory.ts RENAMED
@@ -2,6 +2,7 @@
2
import { BookRaw } from './book-raw';
3
4
export class BookFactory {
5
static fromRaw(b: BookRaw): Book {
6
return {
7
...b,
2
import { BookRaw } from './book-raw';
3
4
export class BookFactory {
5
+
6
static fromRaw(b: BookRaw): Book {
7
return {
8
...b,
tmp/src/app/book-monkey/{iteration-3/interceptors → iteration-4/custom-validation}/shared/book-store.service.ts RENAMED
@@ -36,6 +36,26 @@
36
);
37
}
38
39
remove(isbn: string): Observable<any> {
40
return this.http.delete(
41
`${this.api}/book/${isbn}`,
@@ -56,6 +76,14 @@
56
catchError(this.errorHandler)
57
);
58
}
59
60
private errorHandler(error: HttpErrorResponse): Observable<any> {
61
console.error('Fehler aufgetreten!');
36
);
37
}
38
39
+ create(book: Book): Observable<any> {
40
+ return this.http.post(
41
+ `${this.api}/book`,
42
+ book,
43
+ { responseType: 'text' }
44
+ ).pipe(
45
+ catchError(this.errorHandler)
46
+ );
47
+ }
48
+
49
+ update(book: Book): Observable<any> {
50
+ return this.http.put(
51
+ `${this.api}/book/${book.isbn}`,
52
+ book,
53
+ { responseType: 'text' }
54
+ ).pipe(
55
+ catchError(this.errorHandler)
56
+ );
57
+ }
58
+
59
remove(isbn: string): Observable<any> {
60
return this.http.delete(
61
`${this.api}/book/${isbn}`,
76
catchError(this.errorHandler)
77
);
78
}
79
+
80
+ check(isbn: string): Observable<boolean> {
81
+ return this.http.get(
82
+ `${this.api}/book/${isbn}/check`
83
+ ).pipe(
84
+ catchError(this.errorHandler)
85
+ );
86
+ }
87
88
private errorHandler(error: HttpErrorResponse): Observable<any> {
89
console.error('Fehler aufgetreten!');
tmp/src/app/book-monkey/{iteration-3/interceptors → iteration-4/custom-validation}/shared/book.validators.ts RENAMED
@@ -0,0 +1,30 @@
1
+ import { FormControl, FormArray, ValidationErrors } from '@angular/forms';
2
+
3
+ export class BookValidators {
4
+
5
+ static isbnFormat(control: FormControl): ValidationErrors | null {
6
+ if (!control.value) { return null; }
7
+
8
+ const numbers = control.value.replace(/-/g, '');
9
+ const isbnPattern = /(^\d{10}$)|(^\d{13}$)/;
10
+
11
+ if (isbnPattern.test(numbers)) {
12
+ return null;
13
+ } else {
14
+ return {
15
+ isbnFormat: { valid: false }
16
+ };
17
+ }
18
+ }
19
+
20
+ static atLeastOneAuthor(controlArray: FormArray): ValidationErrors | null {
21
+ if (controlArray.controls.some(el => el.value)) {
22
+ return null;
23
+ } else {
24
+ return {
25
+ atLeastOneAuthor: { valid: false }
26
+ };
27
+ }
28
+ }
29
+
30
+ }