@@ -7,12 +7,14 @@ 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,
|
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 |
+
import { FormErrorsComponent } from './form-errors/form-errors.component';
|
11 |
|
12 |
@NgModule({
|
13 |
declarations: [
|
14 |
BookFormComponent,
|
15 |
BookCreateComponent,
|
16 |
+
BookEditComponent,
|
17 |
+
FormErrorsComponent
|
18 |
],
|
19 |
imports: [
|
20 |
CommonModule,
|
@@ -1,12 +1,24 @@
|
|
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"
|
@@ -19,6 +31,10 @@
|
|
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>
|
1 |
<form [formGroup]="form" (ngSubmit)="submitForm()">
|
2 |
<label for="title">Title</label>
|
3 |
<input id="title" formControlName="title">
|
4 |
+
<bm-form-errors
|
5 |
+
controlName="title"
|
6 |
+
[messages]="{ required: 'Title is required' }">
|
7 |
+
</bm-form-errors>
|
8 |
|
9 |
<label for="subtitle">Subtitle</label>
|
10 |
<input id="subtitle" formControlName="subtitle">
|
11 |
|
12 |
<label for="isbn">ISBN</label>
|
13 |
<input id="isbn" formControlName="isbn">
|
14 |
+
<bm-form-errors
|
15 |
+
controlName="isbn"
|
16 |
+
[messages]="{
|
17 |
+
required: 'ISBN is required',
|
18 |
+
isbnformat: 'ISBN must have 10 or 13 chars',
|
19 |
+
isbnexists: 'ISBN already exists'
|
20 |
+
}">
|
21 |
+
</bm-form-errors>
|
22 |
|
23 |
<label>Authors</label>
|
24 |
<button type="button" class="add"
|
31 |
[attr.aria-label]="'Author ' + i"
|
32 |
[formControlName]="i">
|
33 |
</fieldset>
|
34 |
+
<bm-form-errors
|
35 |
+
controlName="authors"
|
36 |
+
[messages]="{ atleastonevalue: 'At least one author required' }">
|
37 |
+
</bm-form-errors>
|
38 |
|
39 |
<label for="description">Description</label>
|
40 |
<textarea id="description" formControlName="description"></textarea>
|
@@ -1,7 +1,9 @@
|
|
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 |
|
6 |
@Component({
|
7 |
selector: 'bm-book-form',
|
@@ -22,9 +24,9 @@ export class BookFormComponent implements OnChanges {
|
|
22 |
nonNullable: true,
|
23 |
validators: [
|
24 |
Validators.required,
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
}),
|
29 |
description: new FormControl('', { nonNullable: true }),
|
30 |
published: new FormControl('', { nonNullable: true }),
|
@@ -62,7 +64,8 @@ export class BookFormComponent implements OnChanges {
|
|
62 |
|
63 |
private buildAuthorsArray(authors: string[]) {
|
64 |
return new FormArray(
|
65 |
-
authors.map(v => new FormControl(v, { nonNullable: true }))
|
|
|
66 |
);
|
67 |
}
|
68 |
|
1 |
+
import { Component, Output, EventEmitter, Input, OnChanges, inject } from '@angular/core';
|
2 |
import { FormArray, FormControl, FormGroup, Validators } from '@angular/forms';
|
3 |
|
4 |
import { Book } from '../../shared/book';
|
5 |
+
import { AsyncValidatorsService } from '../shared/async-validators.service';
|
6 |
+
import { atLeastOneValue, isbnFormat } from '../shared/validators';
|
7 |
|
8 |
@Component({
|
9 |
selector: 'bm-book-form',
|
24 |
nonNullable: true,
|
25 |
validators: [
|
26 |
Validators.required,
|
27 |
+
isbnFormat
|
28 |
+
],
|
29 |
+
asyncValidators: inject(AsyncValidatorsService).isbnExists()
|
30 |
}),
|
31 |
description: new FormControl('', { nonNullable: true }),
|
32 |
published: new FormControl('', { nonNullable: true }),
|
64 |
|
65 |
private buildAuthorsArray(authors: string[]) {
|
66 |
return new FormArray(
|
67 |
+
authors.map(v => new FormControl(v, { nonNullable: true })),
|
68 |
+
atLeastOneValue
|
69 |
);
|
70 |
}
|
71 |
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
1 |
+
<p class="error" *ngFor="let error of errors">
|
2 |
+
{{ error }}
|
3 |
+
</p>
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
2 |
+
|
3 |
+
import { FormErrorsComponent } from './form-errors.component';
|
4 |
+
|
5 |
+
describe('FormErrorsComponent', () => {
|
6 |
+
let component: FormErrorsComponent;
|
7 |
+
let fixture: ComponentFixture<FormErrorsComponent>;
|
8 |
+
|
9 |
+
beforeEach(async () => {
|
10 |
+
await TestBed.configureTestingModule({
|
11 |
+
declarations: [ FormErrorsComponent ]
|
12 |
+
})
|
13 |
+
.compileComponents();
|
14 |
+
|
15 |
+
fixture = TestBed.createComponent(FormErrorsComponent);
|
16 |
+
component = fixture.componentInstance;
|
17 |
+
fixture.detectChanges();
|
18 |
+
});
|
19 |
+
|
20 |
+
it('should create', () => {
|
21 |
+
expect(component).toBeTruthy();
|
22 |
+
});
|
23 |
+
});
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Component, Input } from '@angular/core';
|
2 |
+
import { FormGroupDirective } from '@angular/forms';
|
3 |
+
|
4 |
+
@Component({
|
5 |
+
selector: 'bm-form-errors',
|
6 |
+
templateUrl: './form-errors.component.html',
|
7 |
+
styleUrls: ['./form-errors.component.css'],
|
8 |
+
})
|
9 |
+
export class FormErrorsComponent {
|
10 |
+
@Input() controlName?: string;
|
11 |
+
@Input() messages: { [errorCode: string]: string } = {};
|
12 |
+
|
13 |
+
constructor(private form: FormGroupDirective) {}
|
14 |
+
|
15 |
+
get errors(): string[] {
|
16 |
+
if (!this.controlName) {
|
17 |
+
return [];
|
18 |
+
}
|
19 |
+
|
20 |
+
const control = this.form.control.get(this.controlName);
|
21 |
+
|
22 |
+
if (!control || !control.errors || !control.touched) {
|
23 |
+
return [];
|
24 |
+
}
|
25 |
+
|
26 |
+
return Object.keys(control.errors).map(errorCode => {
|
27 |
+
return this.messages[errorCode];
|
28 |
+
});
|
29 |
+
}
|
30 |
+
}
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { TestBed } from '@angular/core/testing';
|
2 |
+
|
3 |
+
import { AsyncValidatorsService } from './async-validators.service';
|
4 |
+
|
5 |
+
describe('AsyncValidatorsService', () => {
|
6 |
+
let service: AsyncValidatorsService;
|
7 |
+
|
8 |
+
beforeEach(() => {
|
9 |
+
TestBed.configureTestingModule({});
|
10 |
+
service = TestBed.inject(AsyncValidatorsService);
|
11 |
+
});
|
12 |
+
|
13 |
+
it('should be created', () => {
|
14 |
+
expect(service).toBeTruthy();
|
15 |
+
});
|
16 |
+
});
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Injectable } from '@angular/core';
|
2 |
+
import { AsyncValidatorFn } from '@angular/forms';
|
3 |
+
import { map } from 'rxjs';
|
4 |
+
|
5 |
+
import { BookStoreService } from '../../shared/book-store.service';
|
6 |
+
|
7 |
+
@Injectable({
|
8 |
+
providedIn: 'root'
|
9 |
+
})
|
10 |
+
export class AsyncValidatorsService {
|
11 |
+
|
12 |
+
constructor(private service: BookStoreService) { }
|
13 |
+
|
14 |
+
isbnExists(): AsyncValidatorFn {
|
15 |
+
return (control) => {
|
16 |
+
return this.service.check(control.value).pipe(
|
17 |
+
map(exists => exists ? { isbnexists: true } : null)
|
18 |
+
);
|
19 |
+
}
|
20 |
+
}
|
21 |
+
}
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { isFormArray, ValidatorFn } from '@angular/forms';
|
2 |
+
|
3 |
+
export const isbnFormat: ValidatorFn = function(control) {
|
4 |
+
if (!control.value || typeof control.value !== 'string') {
|
5 |
+
return null;
|
6 |
+
}
|
7 |
+
|
8 |
+
const isbnWithoutDashes = control.value.replace(/-/g, '');
|
9 |
+
const length = isbnWithoutDashes.length;
|
10 |
+
|
11 |
+
if (length === 10 || length === 13) {
|
12 |
+
return null;
|
13 |
+
} else {
|
14 |
+
return { isbnformat: true };
|
15 |
+
}
|
16 |
+
}
|
17 |
+
|
18 |
+
export const atLeastOneValue: ValidatorFn = function(control) {
|
19 |
+
if (!isFormArray(control)) {
|
20 |
+
return null;
|
21 |
+
}
|
22 |
+
|
23 |
+
if (control.controls.some(el => !!el.value)) {
|
24 |
+
return null;
|
25 |
+
} else {
|
26 |
+
return { atleastonevalue: true };
|
27 |
+
}
|
28 |
+
}
|
@@ -48,4 +48,10 @@ export class BookStoreService {
|
|
48 |
book
|
49 |
);
|
50 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
51 |
}
|
48 |
book
|
49 |
);
|
50 |
}
|
51 |
+
|
52 |
+
check(isbn: string): Observable<boolean> {
|
53 |
+
return this.http.get<boolean>(
|
54 |
+
`${this.apiUrl}/books/${isbn}/check`
|
55 |
+
);
|
56 |
+
}
|
57 |
}
|