mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2025-05-12 17:04:57 +00:00
[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:
parent
6c1a758561
commit
3d65b49af7
12
.vscode/settings.json
vendored
12
.vscode/settings.json
vendored
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -68,6 +68,7 @@
|
||||
<activiti-task-details #activitidetails
|
||||
[debugMode]="true"
|
||||
[taskId]="currentTaskId"
|
||||
[fieldValidators]="fieldValidators"
|
||||
(formCompleted)="onFormCompleted($event)"
|
||||
(formContentClicked)="onFormContentClick($event)"
|
||||
(taskCreated)="onTaskCreated($event)"
|
||||
|
@ -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() {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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:
|
||||
|
||||

|
||||
|
||||
### Advanced properties
|
||||
|
||||
The following properties are for complex customisation purposes:
|
||||
|
BIN
ng2-components/ng2-activiti-form/docs/assets/demo-validator.png
Normal file
BIN
ng2-components/ng2-activiti-form/docs/assets/demo-validator.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 26 KiB |
@ -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';
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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()
|
||||
];
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -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[] = [];
|
||||
|
@ -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
|
||||
|
||||
|
@ -40,6 +40,7 @@
|
||||
[disableCompleteButton]="!isAssignedToMe()"
|
||||
[showSaveButton]="showFormSaveButton"
|
||||
[readOnly]="readOnlyForm"
|
||||
[fieldValidators]="fieldValidators"
|
||||
(formSaved)='onFormSaved($event)'
|
||||
(formCompleted)='onFormCompleted($event)'
|
||||
(formContentClicked)='onFormContentClick($event)'
|
||||
|
@ -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>();
|
||||
|
||||
|
@ -48,7 +48,6 @@ export class TaskHeaderComponent implements OnChanges {
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
console.log('change van:', changes, this.taskDetails);
|
||||
this.refreshData();
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user