diff --git a/.vscode/settings.json b/.vscode/settings.json index 3f5f316821..f32d94f698 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -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 + } } diff --git a/demo-shell-ng2/app/components/activiti/activiti-demo.component.html b/demo-shell-ng2/app/components/activiti/activiti-demo.component.html index c0a5afab2b..022f38e635 100644 --- a/demo-shell-ng2/app/components/activiti/activiti-demo.component.html +++ b/demo-shell-ng2/app/components/activiti/activiti-demo.component.html @@ -68,6 +68,7 @@ { console.log('Event fired:' + event.type); console.log('Event Target:' + event.target); }); - + */ } ngOnInit() { diff --git a/demo-shell-ng2/app/components/activiti/demo-field-validator.ts b/demo-shell-ng2/app/components/activiti/demo-field-validator.ts new file mode 100644 index 0000000000..3d0c391afa --- /dev/null +++ b/demo-shell-ng2/app/components/activiti/demo-field-validator.ts @@ -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; + } + +} diff --git a/ng2-components/ng2-activiti-form/README.md b/ng2-components/ng2-activiti-form/README.md index 2c6d263446..8e00b5f94b 100644 --- a/ng2-components/ng2-activiti-form/README.md +++ b/ng2-components/ng2-activiti-form/README.md @@ -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 + +``` + +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 + + + + + + + +``` + +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: diff --git a/ng2-components/ng2-activiti-form/docs/assets/demo-validator.png b/ng2-components/ng2-activiti-form/docs/assets/demo-validator.png new file mode 100644 index 0000000000..e06d00b0c6 Binary files /dev/null and b/ng2-components/ng2-activiti-form/docs/assets/demo-validator.png differ diff --git a/ng2-components/ng2-activiti-form/index.ts b/ng2-components/ng2-activiti-form/index.ts index 293ea0ab3f..2bd2ebe95f 100644 --- a/ng2-components/ng2-activiti-form/index.ts +++ b/ng2-components/ng2-activiti-form/index.ts @@ -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'; diff --git a/ng2-components/ng2-activiti-form/src/components/form.component.ts b/ng2-components/ng2-activiti-form/src/components/form.component.ts index 15ac9521eb..8ab15de872 100644 --- a/ng2-components/ng2-activiti-form/src/components/form.component.ts +++ b/ng2-components/ng2-activiti-form/src/components/form.component.ts @@ -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 = new EventEmitter(); @@ -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); } diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/core/form-field-validator.ts b/ng2-components/ng2-activiti-form/src/components/widgets/core/form-field-validator.ts index a1888cad44..178f760e60 100644 --- a/ng2-components/ng2-activiti-form/src/components/widgets/core/form-field-validator.ts +++ b/ng2-components/ng2-activiti-form/src/components/widgets/core/form-field-validator.ts @@ -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() +]; diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/core/form-field.model.ts b/ng2-components/ng2-activiti-form/src/components/widgets/core/form-field.model.ts index 1068bab70f..2037aef007 100644 --- a/ng2-components/ng2-activiti-form/src/components/widgets/core/form-field.model.ts +++ b/ng2-components/ng2-activiti-form/src/components/widgets/core/form-field.model.ts @@ -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 = json.required; + this._required = json.required; this._readOnly = json.readOnly || json.type === 'readonly'; this.overrideId = json.overrideId; this.tab = json.tab; diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/core/form.model.spec.ts b/ng2-components/ng2-activiti-form/src/components/widgets/core/form.model.spec.ts index 68b7e10f0f..3db8de714a 100644 --- a/ng2-components/ng2-activiti-form/src/components/widgets/core/form.model.spec.ts +++ b/ng2-components/ng2-activiti-form/src/components/widgets/core/form.model.spec.ts @@ -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 = { + 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( {}); + + expect(form.fieldValidators.length).toBe(defaultLength + 1); + expect(FORM_FIELD_VALIDATORS.length).toBe(defaultLength); + }); }); diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/core/form.model.ts b/ng2-components/ng2-activiti-form/src/components/widgets/core/form.model.ts index 2d4d90a7b7..f55fb0363c 100644 --- a/ng2-components/ng2-activiti-form/src/components/widgets/core/form.model.ts +++ b/ng2-components/ng2-activiti-form/src/components/widgets/core/form.model.ts @@ -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[] = []; diff --git a/ng2-components/ng2-activiti-tasklist/README.md b/ng2-components/ng2-activiti-tasklist/README.md index 01f1b61d0e..6a006e6329 100644 --- a/ng2-components/ng2-activiti-tasklist/README.md +++ b/ng2-components/ng2-activiti-tasklist/README.md @@ -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 diff --git a/ng2-components/ng2-activiti-tasklist/src/components/task-details.component.html b/ng2-components/ng2-activiti-tasklist/src/components/task-details.component.html index 69afe9b615..e373501842 100644 --- a/ng2-components/ng2-activiti-tasklist/src/components/task-details.component.html +++ b/ng2-components/ng2-activiti-tasklist/src/components/task-details.component.html @@ -40,6 +40,7 @@ [disableCompleteButton]="!isAssignedToMe()" [showSaveButton]="showFormSaveButton" [readOnly]="readOnlyForm" + [fieldValidators]="fieldValidators" (formSaved)='onFormSaved($event)' (formCompleted)='onFormCompleted($event)' (formContentClicked)='onFormContentClick($event)' diff --git a/ng2-components/ng2-activiti-tasklist/src/components/task-details.component.ts b/ng2-components/ng2-activiti-tasklist/src/components/task-details.component.ts index 2a2f5d840b..7919465b34 100644 --- a/ng2-components/ng2-activiti-tasklist/src/components/task-details.component.ts +++ b/ng2-components/ng2-activiti-tasklist/src/components/task-details.component.ts @@ -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 = new EventEmitter(); diff --git a/ng2-components/ng2-activiti-tasklist/src/components/task-header.component.ts b/ng2-components/ng2-activiti-tasklist/src/components/task-header.component.ts index 4966184699..006a3bbf9b 100644 --- a/ng2-components/ng2-activiti-tasklist/src/components/task-header.component.ts +++ b/ng2-components/ng2-activiti-tasklist/src/components/task-header.component.ts @@ -48,7 +48,6 @@ export class TaskHeaderComponent implements OnChanges { } ngOnChanges(changes: SimpleChanges) { - console.log('change van:', changes, this.taskDetails); this.refreshData(); }