[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

12
.vscode/settings.json vendored
View File

@ -9,5 +9,15 @@
"**/.happypack": true
},
"editor.renderIndentGuides": true,
"tslint.configFile": "ng2-components/tslint.json"
"tslint.configFile": "ng2-components/tslint.json",
"markdownlint.config": {
"MD032": false,
"MD004": false,
"MD024": false,
"MD009": false,
"MD013": false,
"MD036": false,
"MD033" : false,
"MD031" : false
}
}

View File

@ -68,6 +68,7 @@
<activiti-task-details #activitidetails
[debugMode]="true"
[taskId]="currentTaskId"
[fieldValidators]="fieldValidators"
(formCompleted)="onFormCompleted($event)"
(formContentClicked)="onFormContentClick($event)"
(taskCreated)="onTaskCreated($event)"

View File

@ -15,10 +15,11 @@
* limitations under the License.
*/
// tslint:disable-next-line:adf-file-name
import { AfterViewInit, Component, ElementRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { AnalyticsReportListComponent } from 'ng2-activiti-analytics';
import { FormEvent, FormFieldEvent, FormRenderingService, FormService } from 'ng2-activiti-form';
import { FORM_FIELD_VALIDATORS, FormEvent, FormFieldEvent, FormRenderingService, FormService } from 'ng2-activiti-form';
import {
FilterProcessRepresentationModel,
ProcessFiltersComponent,
@ -43,6 +44,7 @@ import {
} from 'ng2-alfresco-datatable';
import { Subscription } from 'rxjs/Rx';
import { /*CustomEditorComponent*/ CustomStencil01 } from './custom-editor/custom-editor.component';
import { DemoFieldValidator } from './demo-field-validator';
declare var componentHandler;
@ -109,6 +111,11 @@ export class ActivitiDemoComponent implements AfterViewInit, OnDestroy, OnInit {
dataTasks: ObjectDataTableAdapter;
dataProcesses: ObjectDataTableAdapter;
fieldValidators = [
...FORM_FIELD_VALIDATORS,
new DemoFieldValidator()
];
constructor(private elementRef: ElementRef,
private route: ActivatedRoute,
private router: Router,
@ -141,11 +148,13 @@ export class ActivitiDemoComponent implements AfterViewInit, OnDestroy, OnInit {
console.log(`Field value changed. Form: ${e.form.id}, Field: ${e.field.id}, Value: ${e.field.value}`);
});
// Uncomment this block to see form event handling in action
/*
formService.formEvents.subscribe((event: Event) => {
console.log('Event fired:' + event.type);
console.log('Event Target:' + event.target);
});
*/
}
ngOnInit() {

View File

@ -0,0 +1,36 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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;
}
}

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[] = [];

View File

@ -169,6 +169,7 @@ The component shows the details of the task id passed in input
| showInvolvePeople | boolean | true | Toggle `Involve People` feature for Header component |
| showComments | boolean | true | Toggle `Comments` feature for Header component |
| showChecklist | boolean | true | Toggle `Checklist` feature for Header component |
| fieldValidators | FormFieldValidator[] | [] | Field validators for use with the form. |
### Events

View File

@ -40,6 +40,7 @@
[disableCompleteButton]="!isAssignedToMe()"
[showSaveButton]="showFormSaveButton"
[readOnly]="readOnlyForm"
[fieldValidators]="fieldValidators"
(formSaved)='onFormSaved($event)'
(formCompleted)='onFormCompleted($event)'
(formContentClicked)='onFormContentClick($event)'

View File

@ -26,7 +26,7 @@ import { Component,
TemplateRef,
ViewChild
} from '@angular/core';
import { ContentLinkModel, FormModel, FormOutcomeEvent } from 'ng2-activiti-form';
import { ContentLinkModel, FormFieldValidator, FormModel, FormOutcomeEvent } from 'ng2-activiti-form';
import { AlfrescoAuthenticationService, AlfrescoTranslationService, CardViewUpdateService, ClickNotification, UpdateNotification } from 'ng2-alfresco-core';
import { TaskQueryRequestRepresentationModel } from '../models/filter.model';
import { TaskDetailsModel } from '../models/task-details.model';
@ -96,6 +96,9 @@ export class TaskDetailsComponent implements OnInit, OnChanges {
@Input()
peopleIconImageUrl: string = require('../assets/images/user.jpg');
@Input()
fieldValidators: FormFieldValidator[] = [];
@Output()
formSaved: EventEmitter<FormModel> = new EventEmitter<FormModel>();

View File

@ -48,7 +48,6 @@ export class TaskHeaderComponent implements OnChanges {
}
ngOnChanges(changes: SimpleChanges) {
console.log('change van:', changes, this.taskDetails);
this.refreshData();
}