Relocate an already existing async validator into a separate validator that can be reused by every other form. This validator is useful to check immediately after typing if an entered value, e.g. username, already exists.
The API request will be triggered after a delay of 500ms (can be modified). During this delay, every keystroke will reset the timer, so the REST API is not flooded with request.
Signed-off-by: Volker Theile <vtheile@suse.com>
This field is required.
</span>
<span class="help-block"
- *ngIf="userForm.showError('user_id', frm, 'userIdExists')"
+ *ngIf="userForm.showError('user_id', frm, 'notUnique')"
i18n>
The chosen user ID is already in use.
</span>
import { RouterTestingModule } from '@angular/router/testing';
import { BsModalService } from 'ngx-bootstrap/modal';
-import { Observable, of as observableOf } from 'rxjs';
+import { of as observableOf } from 'rxjs';
import { configureTestBed } from '../../../../testing/unit-test-helper';
import { RgwUserService } from '../../../shared/api/rgw-user.service';
describe('RgwUserFormComponent', () => {
let component: RgwUserFormComponent;
let fixture: ComponentFixture<RgwUserFormComponent>;
- let queryResult: Array<string> = [];
-
- class MockRgwUserService extends RgwUserService {
- enumerate() {
- return observableOf(queryResult);
- }
- }
configureTestBed({
declarations: [RgwUserFormComponent],
imports: [HttpClientTestingModule, ReactiveFormsModule, RouterTestingModule, SharedModule],
- providers: [BsModalService, { provide: RgwUserService, useClass: MockRgwUserService }]
+ providers: [BsModalService, RgwUserService]
});
beforeEach(() => {
});
});
- describe('userIdValidator', () => {
- it('should validate user id (1/3)', () => {
- const validatorFn = component.userIdValidator();
- const ctrl = new FormControl('');
- const validator$ = validatorFn(ctrl);
- expect(validator$ instanceof Observable).toBeTruthy();
- if (validator$ instanceof Observable) {
- validator$.subscribe((resp) => {
- expect(resp).toBe(null);
- });
- }
+ describe('username validation', () => {
+ let rgwUserService: RgwUserService;
+
+ beforeEach(() => {
+ rgwUserService = TestBed.get(RgwUserService);
+ spyOn(rgwUserService, 'enumerate').and.returnValue(observableOf(['abc', 'xyz']));
+ });
+
+ it('should validate that username is required', () => {
+ const user_id = component.userForm.get('user_id');
+ user_id.markAsDirty();
+ user_id.setValue('');
+ expect(user_id.hasError('required')).toBeTruthy();
+ expect(user_id.valid).toBeFalsy();
});
it(
- 'should validate user id (2/3)',
+ 'should validate that username is valid',
fakeAsync(() => {
- const validatorFn = component.userIdValidator(0);
- const ctrl = new FormControl('ab');
- ctrl.markAsDirty();
- const validator$ = validatorFn(ctrl);
- expect(validator$ instanceof Observable).toBeTruthy();
- if (validator$ instanceof Observable) {
- validator$.subscribe((resp) => {
- expect(resp).toBe(null);
- });
- tick();
- }
+ const user_id = component.userForm.get('user_id');
+ user_id.markAsDirty();
+ user_id.setValue('ab');
+ tick(500);
+ expect(user_id.hasError('notUnique')).toBeFalsy();
+ expect(user_id.valid).toBeTruthy();
})
);
it(
- 'should validate user id (3/3)',
+ 'should validate that username is invalid',
fakeAsync(() => {
- queryResult = ['abc'];
- const validatorFn = component.userIdValidator(0);
- const ctrl = new FormControl('abc');
- ctrl.markAsDirty();
- const validator$ = validatorFn(ctrl);
- expect(validator$ instanceof Observable).toBeTruthy();
- if (validator$ instanceof Observable) {
- validator$.subscribe((resp) => {
- expect(resp instanceof Object).toBeTruthy();
- expect(resp.userIdExists).toBeTruthy();
- });
- tick();
- }
+ const user_id = component.userForm.get('user_id');
+ user_id.markAsDirty();
+ user_id.setValue('abc');
+ tick(500);
+ expect(user_id.hasError('notUnique')).toBeTruthy();
+ expect(user_id.valid).toBeFalsy();
})
);
});
import * as _ from 'lodash';
import { BsModalService } from 'ngx-bootstrap';
-import {
- forkJoin as observableForkJoin,
- Observable,
- of as observableOf,
- timer as observableTimer
-} from 'rxjs';
-import { map, switchMapTo, take } from 'rxjs/operators';
+import { forkJoin as observableForkJoin, Observable } from 'rxjs';
import { RgwUserService } from '../../../shared/api/rgw-user.service';
import { CdFormBuilder } from '../../../shared/forms/cd-form-builder';
createForm() {
this.userForm = this.formBuilder.group({
// General
- user_id: [null, [Validators.required], [this.userIdValidator()]],
+ user_id: [
+ null,
+ [Validators.required],
+ [CdValidators.unique(this.rgwUserService.exists, this.rgwUserService)]
+ ],
display_name: [null, [Validators.required]],
email: [null, [CdValidators.email]],
max_buckets: [1000, [Validators.required, Validators.min(0)]],
return bytes < 1024 ? { quotaMaxSize: true } : null;
}
- /**
- * Validate the username.
- * @param {number|Date} dueTime The delay time to wait before the
- * API call is executed. This is useful to prevent API calls on
- * every keystroke. Defaults to 500.
- */
- userIdValidator(dueTime = 500): AsyncValidatorFn {
- const rgwUserService = this.rgwUserService;
- return (control: AbstractControl): Observable<ValidationErrors | null> => {
- // Exit immediately if user has not interacted with the control yet
- // or the control value is empty.
- if (control.pristine || control.value === '') {
- return observableOf(null);
- }
- // Forgot previous requests if a new one arrives within the specified
- // delay time.
- return observableTimer(dueTime).pipe(
- switchMapTo(rgwUserService.exists(control.value)),
- map((resp: boolean) => {
- if (!resp) {
- return null;
- } else {
- return { userIdExists: true };
- }
- }),
- take(1)
- );
- };
- }
-
/**
* Add/Update a subuser.
*/
import { Injectable } from '@angular/core';
import * as _ from 'lodash';
-import { forkJoin as observableForkJoin, of as observableOf } from 'rxjs';
+import { forkJoin as observableForkJoin, Observable, of as observableOf } from 'rxjs';
import { mergeMap } from 'rxjs/operators';
import { cdEncode } from '../decorators/cd-encode';
* @param {string} uid The user ID to check.
* @return {Observable<boolean>}
*/
- exists(uid: string) {
+ exists(uid: string): Observable<boolean> {
return this.enumerate().pipe(
mergeMap((resp: string[]) => {
const index = _.indexOf(resp, uid);
-import { FormControl, FormGroup } from '@angular/forms';
+import { fakeAsync, tick } from '@angular/core/testing';
+import { AbstractControl, FormControl, FormGroup } from '@angular/forms';
+
+import { of as observableOf } from 'rxjs';
import { CdValidators } from './cd-validators';
expect(y.hasError('match')).toBeFalsy();
});
});
+
+ describe('unique', () => {
+ let form: FormGroup;
+ let x: AbstractControl;
+
+ beforeEach(() => {
+ form = new FormGroup({
+ x: new FormControl(
+ '',
+ null,
+ CdValidators.unique((value) => {
+ return observableOf('xyz' === value);
+ })
+ )
+ });
+ x = form.get('x');
+ x.markAsDirty();
+ });
+
+ it('should not error because of empty input', () => {
+ x.setValue('');
+ expect(x.hasError('notUnique')).toBeFalsy();
+ expect(x.valid).toBeTruthy();
+ });
+
+ it(
+ 'should not error because of not existing input',
+ fakeAsync(() => {
+ x.setValue('abc');
+ tick(500);
+ expect(x.hasError('notUnique')).toBeFalsy();
+ expect(x.valid).toBeTruthy();
+ })
+ );
+
+ it(
+ 'should error because of already existing input',
+ fakeAsync(() => {
+ x.setValue('xyz');
+ tick(500);
+ expect(x.hasError('notUnique')).toBeTruthy();
+ expect(x.valid).toBeFalsy();
+ })
+ );
+ });
});
-import { AbstractControl, ValidationErrors, ValidatorFn, Validators } from '@angular/forms';
+import {
+ AbstractControl,
+ AsyncValidatorFn,
+ ValidationErrors,
+ ValidatorFn,
+ Validators
+} from '@angular/forms';
import * as _ from 'lodash';
+import { Observable, of as observableOf, timer as observableTimer } from 'rxjs';
+import { map, switchMapTo, take } from 'rxjs/operators';
export function isEmptyInputValue(value: any): boolean {
return value == null || value.length === 0;
}
+export type existsServiceFn = (value: any) => Observable<boolean>;
+
export class CdValidators {
/**
* Validator that performs email validation. In contrast to the Angular
return null;
};
}
+
+ /**
+ * Asynchronous validator that requires the control's value to be unique.
+ * The validation is only executed after the specified delay. Every
+ * keystroke during this delay will restart the timer.
+ * @param serviceFn {existsServiceFn} The service function that is
+ * called to check whether the given value exists. It must return
+ * boolean 'true' if the given value exists, otherwise 'false'.
+ * @param serviceFnThis {any} The object to be used as the 'this' object
+ * when calling the serviceFn function. Defaults to null.
+ * @param {number|Date} dueTime The delay time to wait before the
+ * serviceFn call is executed. This is useful to prevent calls on
+ * every keystroke. Defaults to 500.
+ * @return {AsyncValidatorFn} Returns an asynchronous validator function
+ * that returns an error map with the `notUnique` property if the
+ * validation check succeeds, otherwise `null`.
+ */
+ static unique(
+ serviceFn: existsServiceFn,
+ serviceFnThis: any = null,
+ dueTime = 500
+ ): AsyncValidatorFn {
+ return (control: AbstractControl): Observable<ValidationErrors | null> => {
+ // Exit immediately if user has not interacted with the control yet
+ // or the control value is empty.
+ if (control.pristine || isEmptyInputValue(control.value)) {
+ return observableOf(null);
+ }
+ // Forgot previous requests if a new one arrives within the specified
+ // delay time.
+ return observableTimer(dueTime).pipe(
+ switchMapTo(serviceFn.call(serviceFnThis, control.value)),
+ map((resp: boolean) => {
+ if (!resp) {
+ return null;
+ } else {
+ return { notUnique: true };
+ }
+ }),
+ take(1)
+ );
+ };
+ }
}