|
@@ -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',
|
|
@@ -23,9 +25,9 @@ export class BookFormComponent implements OnChanges {
|
|
| 23 |
nonNullable: true,
|
| 24 |
validators: [
|
| 25 |
Validators.required,
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
}),
|
| 30 |
description: new FormControl('', { nonNullable: true }),
|
| 31 |
published: new FormControl('', { nonNullable: true }),
|
|
@@ -63,7 +65,8 @@ export class BookFormComponent implements OnChanges {
|
|
| 63 |
|
| 64 |
private buildAuthorsArray(authors: string[]) {
|
| 65 |
return new FormArray(
|
| 66 |
-
authors.map(v => new FormControl(v, { nonNullable: true }))
|
|
|
|
| 67 |
);
|
| 68 |
}
|
| 69 |
|
| 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',
|
| 25 |
nonNullable: true,
|
| 26 |
validators: [
|
| 27 |
Validators.required,
|
| 28 |
+
isbnFormat
|
| 29 |
+
],
|
| 30 |
+
asyncValidators: inject(AsyncValidatorsService).isbnExists()
|
| 31 |
}),
|
| 32 |
description: new FormControl('', { nonNullable: true }),
|
| 33 |
published: new FormControl('', { nonNullable: true }),
|
| 65 |
|
| 66 |
private buildAuthorsArray(authors: string[]) {
|
| 67 |
return new FormArray(
|
| 68 |
+
authors.map(v => new FormControl(v, { nonNullable: true })),
|
| 69 |
+
atLeastOneValue
|
| 70 |
);
|
| 71 |
}
|
| 72 |
|
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<p class="error" *ngFor="let error of errors">
|
| 2 |
+
{{ error }}
|
| 3 |
+
</p>
|
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
standalone: false,
|
| 8 |
+
styleUrl: './form-errors.component.css',
|
| 9 |
+
})
|
| 10 |
+
export class FormErrorsComponent {
|
| 11 |
+
@Input() controlName?: string;
|
| 12 |
+
@Input() messages: { [errorCode: string]: string } = {};
|
| 13 |
+
|
| 14 |
+
constructor(private form: FormGroupDirective) {}
|
| 15 |
+
|
| 16 |
+
get errors(): string[] {
|
| 17 |
+
if (!this.controlName) {
|
| 18 |
+
return [];
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
const control = this.form.control.get(this.controlName);
|
| 22 |
+
|
| 23 |
+
if (!control || !control.errors || !control.touched) {
|
| 24 |
+
return [];
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
return Object.keys(control.errors).map(errorCode => {
|
| 28 |
+
return this.messages[errorCode];
|
| 29 |
+
});
|
| 30 |
+
}
|
| 31 |
+
}
|
|
@@ -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 |
}
|