[ADF-1312] form validation enhancements (#2180)

* validation api enhancements

- changing 'required' causes re-validation of the form
- get field by id

* allow binding field validators from html

* demo validator

* documentation updates

* fix after rebase

* markdown fixes

* markdown linter settings for workspace config (vs code)

* restore material theme
This commit is contained in:
Denys Vuika
2017-08-07 18:41:17 +01:00
committed by Mario Romano
parent 6c1a758561
commit 3d65b49af7
16 changed files with 292 additions and 44 deletions

View File

@@ -188,6 +188,117 @@ If needed, you can completely redefine the set of validators used by the form.
All changes to `fieldValidators` collection are automatically applied to all the further validation cycles.
##### Custom set of validators
You can provide your own set of field validators based on either custom validator instances, or a mixture of default and custom ones.
```html
<adf-form [fieldValidators]="fieldValidators"></adf-form>
```
The Form component exposes a special `FORM_FIELD_VALIDATORS` constant that allows you get a quick access to all system validator instances.
```ts
import { FORM_FIELD_VALIDATORS } from 'ng2-activiti-form';
@Component({...})
export class AppComponent {
fieldValidators = [
// default set of ADF validators if needed
...FORM_FIELD_VALIDATORS,
// custom validators
new MyValidator1(),
new MyValidator2()
];
}
```
##### Custom validator example
A form field validator must implement the "FormFieldValidator" interface:
```ts
export interface FormFieldValidator {
isSupported(field: FormFieldModel): boolean;
validate(field: FormFieldModel): boolean;
}
```
There might be many different validators used for various field types and purposes,
so the validation layer needs every validator instance to support "isSupported" call.
It is up to validator to declare support for a form field.
If you want to check field types the [FormFieldTypes](https://github.com/Alfresco/alfresco-ng2-components/blob/master/ng2-components/ng2-activiti-form/src/components/widgets/core/form-field-types.ts) class can help you with the predefined constants and helper methods.
In addition every validator has access to all underlying APIs of the [FormFieldModel](https://github.com/Alfresco/alfresco-ng2-components/blob/master/ng2-components/ng2-activiti-form/src/components/widgets/core/form-field.model.ts),
including the reference to the Form instance and so other form fields.
Below is a source code for a demo validator that is executed for all the "TEXT" fields, and ensures the value is not "admin", otherwise the `field.validationSummary` value is set to an error.
```ts
import { FormFieldModel, FormFieldTypes, FormFieldValidator } from 'ng2-activiti-form';
export class DemoFieldValidator implements FormFieldValidator {
isSupported(field: FormFieldModel): boolean {
return field && field.type === FormFieldTypes.TEXT;
}
validate(field: FormFieldModel): boolean {
if (this.isSupported(field)) {
if (field.value && field.value.toLowerCase() === 'admin') {
field.validationSummary = 'Sorry, the value cannot be "admin".';
return false;
}
}
return true;
}
}
```
Your component can extend the default validation set instead of replacing it entirely.
In the example below we redefine a default validation set with an additional "DemoFieldValidator":
```ts
import { DemoFieldValidator } from './demo-field-validator';
@Component({...})
export class AppComponent {
fieldValidators = [
...FORM_FIELD_VALIDATORS,
new DemoFieldValidator()
];
}
```
You can now use the 'fieldValidators' property with the Form or Task Details components to assign custom validator set for the underlying Form Model:
```html
<activiti-task-details
[fieldValidators]="fieldValidators"
taskId="123">
</<activiti-task-details>
<!-- OR -->
<adf-form
[fieldValidators]="fieldValidators"
taskI="123">
</adf-form>
```
Now if you run the application and try to enter "admin" in one of the text fields (either optional or required), you should see the following error:
![](docs/assets/demo-validator.png)
### Advanced properties
The following properties are for complex customisation purposes:

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -47,6 +47,7 @@ export * from './src/services/ecm-model.service';
export * from './src/services/node.service';
export * from './src/services/form-rendering.service';
export * from './src/events/index';
export { FORM_FIELD_VALIDATORS } from './src/components/widgets/core/form-field-validator';
// Old deprecated import
import {FormComponent as ActivitiForm } from './src/components/form.component';

View File

@@ -22,7 +22,7 @@ import { EcmModelService } from './../services/ecm-model.service';
import { FormService } from './../services/form.service';
import { NodeService } from './../services/node.service';
import { ContentLinkModel } from './widgets/core/content-link.model';
import { FormFieldModel, FormModel, FormOutcomeEvent, FormOutcomeModel, FormValues } from './widgets/core/index';
import { FormFieldModel, FormModel, FormOutcomeEvent, FormOutcomeModel, FormValues, FormFieldValidator } from './widgets/core/index';
import { WidgetVisibilityService } from './../services/widget-visibility.service';
@@ -90,6 +90,9 @@ export class FormComponent implements OnInit, OnChanges {
@Input()
showValidationIcon: boolean = true;
@Input()
fieldValidators: FormFieldValidator[] = [];
@Output()
formSaved: EventEmitter<FormModel> = new EventEmitter<FormModel>();
@@ -307,16 +310,16 @@ export class FormComponent implements OnInit, OnChanges {
this.formService
.getTaskForm(taskId)
.subscribe(
form => {
this.form = new FormModel(form, this.data, this.readOnly, this.formService);
this.onFormLoaded(this.form);
resolve(this.form);
},
error => {
this.handleError(error);
// reject(error);
resolve(null);
}
form => {
this.form = this.parseForm(form);
this.onFormLoaded(this.form);
resolve(this.form);
},
error => {
this.handleError(error);
// reject(error);
resolve(null);
}
);
});
});
@@ -396,6 +399,10 @@ export class FormComponent implements OnInit, OnChanges {
if (!json.fields) {
form.outcomes = this.getFormDefinitionOutcomes(form);
}
if (this.fieldValidators && this.fieldValidators.length > 0) {
console.log('Applying custom field validators');
form.fieldValidators = this.fieldValidators;
}
return form;
}
return null;
@@ -419,7 +426,7 @@ export class FormComponent implements OnInit, OnChanges {
}
private refreshFormData() {
this.form = new FormModel(this.form.json, this.data, this.readOnly, this.formService);
this.form = this.parseForm(this.form.json);
this.onFormLoaded(this.form);
this.onFormDataRefreshed(this.form);
}

View File

@@ -355,3 +355,16 @@ export class RegExFieldValidator implements FormFieldValidator {
}
}
export const FORM_FIELD_VALIDATORS = [
new RequiredFieldValidator(),
new NumberFieldValidator(),
new MinLengthFieldValidator(),
new MaxLengthFieldValidator(),
new MinValueFieldValidator(),
new MaxValueFieldValidator(),
new RegExFieldValidator(),
new DateFieldValidator(),
new MinDateFieldValidator(),
new MaxDateFieldValidator()
];

View File

@@ -32,6 +32,7 @@ export class FormFieldModel extends FormWidgetModel {
private _value: string;
private _readOnly: boolean = false;
private _isValid: boolean = true;
private _required: boolean = false;
readonly defaultDateFormat: string = 'D-M-YYYY';
@@ -40,7 +41,6 @@ export class FormFieldModel extends FormWidgetModel {
id: string;
name: string;
type: string;
required: boolean;
overrideId: boolean;
tab: string;
rowspan: number = 1;
@@ -99,6 +99,15 @@ export class FormFieldModel extends FormWidgetModel {
this.updateForm();
}
get required(): boolean {
return this._required;
}
set required(value: boolean) {
this._required = value;
this.updateForm();
}
get isValid(): boolean {
return this._isValid;
}
@@ -126,7 +135,7 @@ export class FormFieldModel extends FormWidgetModel {
this.id = json.id;
this.name = json.name;
this.type = json.type;
this.required = <boolean> json.required;
this._required = <boolean> json.required;
this._readOnly = <boolean> json.readOnly || json.type === 'readonly';
this.overrideId = <boolean> json.overrideId;
this.tab = json.tab;

View File

@@ -20,6 +20,8 @@ import { ValidateFormEvent } from './../../../events/validate-form.event';
import { FormService } from './../../../services/form.service';
import { ContainerModel } from './container.model';
import { FormFieldTypes } from './form-field-types';
import { FORM_FIELD_VALIDATORS, FormFieldValidator } from './form-field-validator';
import { FormFieldModel } from './form-field.model';
import { FormOutcomeModel } from './form-outcome.model';
import { FormModel } from './form.model';
import { TabModel } from './tab.model';
@@ -381,4 +383,67 @@ describe('FormModel', () => {
expect(field.validate).not.toHaveBeenCalled();
expect(form.validateForm).not.toHaveBeenCalled();
});
it('should get field by id', () => {
const form = new FormModel({}, null, false, formService);
const field = { id: 'field1' };
spyOn(form, 'getFormFields').and.returnValue([field]);
const result = form.getFieldById('field1');
expect(result).toBe(field);
});
it('should use custom field validator', () => {
const form = new FormModel({}, null, false, formService);
const testField = new FormFieldModel(form, {
id: 'test-field-1'
});
spyOn(form, 'getFormFields').and.returnValue([testField]);
let validator = <FormFieldValidator> {
isSupported(field: FormFieldModel): boolean {
return true;
},
validate(field: FormFieldModel): boolean {
return true;
}
};
spyOn(validator, 'validate').and.callThrough();
form.fieldValidators = [validator];
form.validateForm();
expect(validator.validate).toHaveBeenCalledWith(testField);
});
it('should re-validate the field when required attribute changes', () => {
const form = new FormModel({}, null, false, formService);
const testField = new FormFieldModel(form, {
id: 'test-field-1',
required: false
});
spyOn(form, 'getFormFields').and.returnValue([testField]);
spyOn(form, 'onFormFieldChanged').and.callThrough();
spyOn(form, 'validateField').and.callThrough();
testField.required = true;
expect(testField.required).toBeTruthy();
expect(form.onFormFieldChanged).toHaveBeenCalledWith(testField);
expect(form.validateField).toHaveBeenCalledWith(testField);
});
it('should not change default validators export', () => {
const form = new FormModel({}, null, false, formService);
const defaultLength = FORM_FIELD_VALIDATORS.length;
expect(form.fieldValidators.length).toBe(defaultLength);
form.fieldValidators.push(<any> {});
expect(form.fieldValidators.length).toBe(defaultLength + 1);
expect(FORM_FIELD_VALIDATORS.length).toBe(defaultLength);
});
});

View File

@@ -29,17 +29,8 @@ import { FormWidgetModel, FormWidgetModelCache } from './form-widget.model';
import { TabModel } from './tab.model';
import {
DateFieldValidator,
FormFieldValidator,
MaxDateFieldValidator,
MaxLengthFieldValidator,
MaxValueFieldValidator,
MinDateFieldValidator,
MinLengthFieldValidator,
MinValueFieldValidator,
NumberFieldValidator,
RegExFieldValidator,
RequiredFieldValidator
FORM_FIELD_VALIDATORS,
FormFieldValidator
} from './form-field-validator';
export class FormModel {
@@ -67,7 +58,7 @@ export class FormModel {
fields: FormWidgetModel[] = [];
outcomes: FormOutcomeModel[] = [];
customFieldTemplates: FormFieldTemplates = {};
fieldValidators: FormFieldValidator[] = [];
fieldValidators: FormFieldValidator[] = [...FORM_FIELD_VALIDATORS];
readonly selectedOutcome: string;
values: FormValues = {};
@@ -138,19 +129,6 @@ export class FormModel {
}
}
this.fieldValidators = [
new RequiredFieldValidator(),
new NumberFieldValidator(),
new MinLengthFieldValidator(),
new MaxLengthFieldValidator(),
new MinValueFieldValidator(),
new MaxValueFieldValidator(),
new RegExFieldValidator(),
new DateFieldValidator(),
new MinDateFieldValidator(),
new MaxDateFieldValidator()
];
this.validateForm();
}
@@ -161,6 +139,10 @@ export class FormModel {
}
}
getFieldById(fieldId: string): FormFieldModel {
return this.getFormFields().find(field => field.id === fieldId);
}
// TODO: consider evaluating and caching once the form is loaded
getFormFields(): FormFieldModel[] {
let result: FormFieldModel[] = [];