</ng-container>
<ng-container class="modal-content">
<form [formGroup]="formGroup"
+ #formDir="ngForm"
novalidate>
<div class="modal-body">
<p *ngIf="message">{{ message }}</p>
<ng-container *ngFor="let field of fields">
- <div class="form-group row">
- <ng-container [ngSwitch]="field.type">
- <ng-template [ngSwitchCase]="'inputText'">
- <label *ngIf="field.label"
- class="col-form-label col-sm-3"
- [for]="field.name">
- {{ field.label }}
- </label>
- <div [ngClass]="{'col-sm-9': field.label, 'col-sm-12': !field.label}">
- <input type="text"
- class="form-control"
- [id]="field.name"
- [name]="field.name"
- [formControlName]="field.name">
- <span *ngIf="formGroup.hasError('required', field.name)"
- class="invalid-feedback"
- i18n>This field is required.</span>
- </div>
- </ng-template>
- <ng-template [ngSwitchCase]="'select'">
- <label *ngIf="field.label"
- class="col-form-label col-sm-3"
- [for]="field.name">
- {{ field.label }}
- </label>
- <div [ngClass]="{'col-sm-9': field.label, 'col-sm-12': !field.label}">
- <select class="form-control custom-select"
- [id]="field.name"
- [formControlName]="field.name">
- <option *ngIf="field.placeholder"
- [ngValue]="null">
- {{ field.placeholder }}
- </option>
- <option *ngFor="let option of field.options"
- [value]="option.value">
- {{ option.text }}
- </option>
- </select>
- <span *ngIf="formGroup.hasError('required', field.name)"
- class="invalid-feedback"
- i18n>This field is required.</span>
- </div>
- </ng-template>
- </ng-container>
+ <div class="form-group row cd-{{field.name}}-form-group">
+ <label *ngIf="field.label"
+ class="col-form-label col-sm-3"
+ [for]="field.name">
+ {{ field.label }}
+ </label>
+ <div [ngClass]="{'col-sm-9': field.label, 'col-sm-12': !field.label}">
+ <input *ngIf="['text', 'number'].includes(field.type)"
+ [type]="field.type"
+ class="form-control"
+ [id]="field.name"
+ [name]="field.name"
+ [formControlName]="field.name">
+ <input *ngIf="field.type === 'binary'"
+ type="text"
+ class="form-control"
+ [id]="field.name"
+ [name]="field.name"
+ [formControlName]="field.name"
+ cdDimlessBinary>
+ <select *ngIf="field.name === 'select'"
+ class="form-control custom-select"
+ [id]="field.name"
+ [formControlName]="field.name">
+ <option *ngIf="field.placeholder"
+ [ngValue]="null">
+ {{ field.placeholder }}
+ </option>
+ <option *ngFor="let option of field.options"
+ [value]="option.value">
+ {{ option.text }}
+ </option>
+ </select>
+ <span *ngIf="formGroup.showError(field.name,formDir)"
+ class="invalid-feedback">
+ {{ getError(field) }}
+ </span>
+ </div>
</div>
</ng-container>
</div>
import { ComponentFixture, TestBed } from '@angular/core/testing';
-import { ReactiveFormsModule } from '@angular/forms';
+import { ReactiveFormsModule, Validators } from '@angular/forms';
import { RouterTestingModule } from '@angular/router/testing';
import { NgBootstrapFormValidationModule } from 'ng-bootstrap-form-validation';
import {
configureTestBed,
FixtureHelper,
+ FormHelper,
i18nProviders
} from '../../../../testing/unit-test-helper';
+import { CdValidators } from '../../forms/cd-validators';
import { SharedModule } from '../../shared.module';
import { FormModalComponent } from './form-modal.component';
let component: FormModalComponent;
let fixture: ComponentFixture<FormModalComponent>;
let fh: FixtureHelper;
+ let formHelper: FormHelper;
let submitted;
const initialState = {
message: 'Some description',
fields: [
{
- type: 'inputText',
+ type: 'text',
name: 'requiredField',
value: 'some-value',
required: true
},
{
- type: 'inputText',
+ type: 'number',
name: 'optionalField',
- label: 'Optional'
+ label: 'Optional',
+ errors: { min: 'Value has to be above zero!' },
+ validators: [Validators.min(0), Validators.max(10)]
+ },
+ {
+ type: 'binary',
+ name: 'dimlessBinary',
+ label: 'Size',
+ value: 2048,
+ validators: [CdValidators.binaryMin(1024), CdValidators.binaryMax(3072)]
}
],
submitButtonText: 'Submit button name',
Object.assign(component, initialState);
fixture.detectChanges();
fh = new FixtureHelper(fixture);
+ formHelper = new FormHelper(component.formGroup);
});
it('should create', () => {
it('gives back all form values on submit', () => {
component.onSubmitForm(component.formGroup.value);
expect(submitted).toEqual({
+ dimlessBinary: 2048,
+ requiredField: 'some-value',
+ optionalField: null
+ });
+ });
+
+ it('tests required field validation', () => {
+ formHelper.expectErrorChange('requiredField', '', 'required');
+ });
+
+ it('tests required field message', () => {
+ formHelper.setValue('requiredField', '', true);
+ fh.expectTextToBe('.cd-requiredField-form-group .invalid-feedback', 'This field is required.');
+ });
+
+ it('tests custom validator on number field', () => {
+ formHelper.expectErrorChange('optionalField', -1, 'min');
+ formHelper.expectErrorChange('optionalField', 11, 'max');
+ });
+
+ it('tests custom validator error message', () => {
+ formHelper.setValue('optionalField', -1, true);
+ fh.expectTextToBe(
+ '.cd-optionalField-form-group .invalid-feedback',
+ 'Value has to be above zero!'
+ );
+ });
+
+ it('tests default error message', () => {
+ formHelper.setValue('optionalField', 11, true);
+ fh.expectTextToBe('.cd-optionalField-form-group .invalid-feedback', 'An error occurred.');
+ });
+
+ it('tests binary error messages', () => {
+ formHelper.setValue('dimlessBinary', '4 K', true);
+ fh.expectTextToBe(
+ '.cd-dimlessBinary-form-group .invalid-feedback',
+ 'Size has to be at most 3 KiB or less'
+ );
+ formHelper.setValue('dimlessBinary', '0.5 K', true);
+ fh.expectTextToBe(
+ '.cd-dimlessBinary-form-group .invalid-feedback',
+ 'Size has to be at least 1 KiB or more'
+ );
+ });
+
+ it('shows result of dimlessBinary pipe', () => {
+ fh.expectFormFieldToBe('#dimlessBinary', '2 KiB');
+ });
+
+ it('changes dimlessBinary value and the result will still be a number', () => {
+ formHelper.setValue('dimlessBinary', '3 K', true);
+ component.onSubmitForm(component.formGroup.value);
+ expect(submitted).toEqual({
+ dimlessBinary: 3072,
requiredField: 'some-value',
optionalField: null
});
import { Component, OnInit } from '@angular/core';
-import { FormControl, FormGroup, Validators } from '@angular/forms';
+import { FormControl, ValidatorFn, Validators } from '@angular/forms';
+import { I18n } from '@ngx-translate/i18n-polyfill';
import * as _ from 'lodash';
import { BsModalRef } from 'ngx-bootstrap/modal';
+import { DimlessBinaryPipe } from 'app/shared/pipes/dimless-binary.pipe';
import { CdFormBuilder } from '../../forms/cd-form-builder';
-
-interface CdFormFieldConfig {
- type: 'inputText' | 'select';
- name: string;
- label?: string;
- value?: any;
- required?: boolean;
- // --- select ---
- placeholder?: string;
- options?: Array<{
- text: string;
- value: any;
- }>;
-}
+import { CdFormGroup } from '../../forms/cd-form-group';
+import { CdFormModalFieldConfig } from '../../models/cd-form-modal-field-config';
+import { FormatterService } from '../../services/formatter.service';
@Component({
selector: 'cd-form-modal',
// Input
titleText: string;
message: string;
- fields: CdFormFieldConfig[];
+ fields: CdFormModalFieldConfig[];
submitButtonText: string;
onSubmit: Function;
// Internal
- formGroup: FormGroup;
+ formGroup: CdFormGroup;
+
+ constructor(
+ public bsModalRef: BsModalRef,
+ private formBuilder: CdFormBuilder,
+ private formatter: FormatterService,
+ private dimlessBinaryPipe: DimlessBinaryPipe,
+ private i18n: I18n
+ ) {}
- constructor(public bsModalRef: BsModalRef, private formBuilder: CdFormBuilder) {}
+ ngOnInit() {
+ this.createForm();
+ }
createForm() {
const controlsConfig = {};
this.fields.forEach((field) => {
- const validators = [];
- if (_.isBoolean(field.required) && field.required) {
- validators.push(Validators.required);
- }
- controlsConfig[field.name] = new FormControl(_.defaultTo(field.value, null), { validators });
+ controlsConfig[field.name] = this.createFormControl(field);
});
this.formGroup = this.formBuilder.group(controlsConfig);
}
- ngOnInit() {
- this.createForm();
+ private createFormControl(field: CdFormModalFieldConfig): FormControl {
+ let validators: ValidatorFn[] = [];
+ if (_.isBoolean(field.required) && field.required) {
+ validators.push(Validators.required);
+ }
+ if (field.validators) {
+ validators = validators.concat(field.validators);
+ }
+ return new FormControl(
+ _.defaultTo(
+ field.type === 'binary' ? this.dimlessBinaryPipe.transform(field.value) : field.value,
+ null
+ ),
+ { validators }
+ );
+ }
+
+ getError(field: CdFormModalFieldConfig): string {
+ const formErrors = this.formGroup.get(field.name).errors;
+ const errors = Object.keys(formErrors).map((key) => {
+ return this.getErrorMessage(key, formErrors[key], field.errors);
+ });
+ return errors.join('<br>');
+ }
+
+ private getErrorMessage(
+ error: string,
+ errorContext: any,
+ fieldErrors: { [error: string]: string }
+ ): string {
+ if (fieldErrors) {
+ const customError = fieldErrors[error];
+ if (customError) {
+ return customError;
+ }
+ }
+ if (['binaryMin', 'binaryMax'].includes(error)) {
+ // binaryMin and binaryMax return a function that take I18n to
+ // provide a translated error message.
+ return errorContext(this.i18n);
+ }
+ if (error === 'required') {
+ return this.i18n('This field is required.');
+ }
+ return this.i18n('An error occurred.');
}
onSubmitForm(values) {
+ const binaries = this.fields
+ .filter((field) => field.type === 'binary')
+ .map((field) => field.name);
+ binaries.forEach((key) => {
+ const value = values[key];
+ if (value) {
+ values[key] = this.formatter.toBytes(value);
+ }
+ });
this.bsModalRef.hide();
if (_.isFunction(this.onSubmit)) {
this.onSubmit(values);
--- /dev/null
+import { ValidatorFn } from '@angular/forms';
+
+export class CdFormModalFieldConfig {
+ name: string;
+ // 'binary' will use cdDimlessBinary directive on input element
+ // 'select' will use select element
+ type: 'number' | 'text' | 'binary' | 'select';
+ label?: string;
+ required?: boolean;
+ value?: any;
+ errors?: { [errorName: string]: string };
+ validators: ValidatorFn[];
+ // only for type select
+ placeholder?: string;
+ options?: Array<{
+ text: string;
+ value: any;
+ }>;
+}