Differenzansicht 12-validation
im Vergleich zu 11-reactive-forms

← Zurück zur Ãœbersicht | Demo | Quelltext auf GitHub
src/app/admin/admin.module.ts CHANGED
@@ -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,
src/app/admin/book-form/book-form.component.html CHANGED
@@ -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>
src/app/admin/book-form/book-form.component.ts CHANGED
@@ -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
- Validators.minLength(10),
26
- Validators.maxLength(13),
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
 
src/app/admin/form-errors/form-errors.component.html ADDED
@@ -0,0 +1,3 @@
 
 
 
1
+ <p class="error" *ngFor="let error of errors">
2
+ {{ error }}
3
+ </p>
src/app/admin/form-errors/form-errors.component.spec.ts ADDED
@@ -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
+ });
src/app/admin/form-errors/form-errors.component.ts ADDED
@@ -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
+ }
src/app/admin/shared/async-validators.service.spec.ts ADDED
@@ -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
+ });
src/app/admin/shared/async-validators.service.ts ADDED
@@ -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
+ }
src/app/admin/shared/validators.ts ADDED
@@ -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
+ }
src/app/shared/book-store.service.ts CHANGED
@@ -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
  }