#726 basic form validation support

- initial validation workflow
- validation for Required field (text, multiline text widgets)
- disable form outcomes on failed validation
This commit is contained in:
Denys Vuika
2016-09-12 16:49:55 +01:00
parent 3e5e023a06
commit a181cd2c5f
8 changed files with 99 additions and 13 deletions

View File

@@ -4,8 +4,9 @@
</div>
<div *ngIf="hasForm()">
<div class="mdl-card mdl-shadow--2dp activiti-form-container">
<div *ngIf="isTitleEnabled()" class="mdl-card__title">
<h2 class="mdl-card__title-text">{{form.taskName}}</h2>
<div class="mdl-card__title">
<i class="material-icons">{{ form.isValid ? 'event_available' : 'event_busy' }}</i>
<h2 *ngIf="isTitleEnabled()" class="mdl-card__title-text">{{form.taskName}}</h2>
</div>
<div class="mdl-card__media">
<div *ngIf="form.hasTabs()">
@@ -19,9 +20,9 @@
<div *ngIf="form.hasOutcomes()" class="mdl-card__actions mdl-card--border">
<button *ngFor="let outcome of form.outcomes"
alfresco-mdl-button
[disabled]="readOnly"
[disabled]="!isOutcomeButtonEnabled(outcome)"
[class.mdl-button--colored]="!outcome.isSystem"
[class.activiti-form-hide-button]="!isOutcomeButtonEnabled(outcome)"
[class.activiti-form-hide-button]="!isOutcomeButtonVisible(outcome)"
(click)="onOutcomeClicked(outcome, $event)">
{{outcome.name}}
</button>

View File

@@ -98,13 +98,13 @@ describe('ActivitiForm', () => {
});
it('should not enable outcome button when model missing', () => {
expect(formComponent.isOutcomeButtonEnabled(null)).toBeFalsy();
expect(formComponent.isOutcomeButtonVisible(null)).toBeFalsy();
});
it('should enable custom outcome buttons', () => {
let formModel = new FormModel();
let outcome = new FormOutcomeModel(formModel, { id: 'action1', name: 'Action 1' });
expect(formComponent.isOutcomeButtonEnabled(outcome)).toBeTruthy();
expect(formComponent.isOutcomeButtonVisible(outcome)).toBeTruthy();
});
@@ -113,10 +113,10 @@ describe('ActivitiForm', () => {
let outcome = new FormOutcomeModel(formModel, { id: '$save', name: FormOutcomeModel.SAVE_ACTION });
formComponent.showSaveButton = true;
expect(formComponent.isOutcomeButtonEnabled(outcome)).toBeTruthy();
expect(formComponent.isOutcomeButtonVisible(outcome)).toBeTruthy();
formComponent.showSaveButton = false;
expect(formComponent.isOutcomeButtonEnabled(outcome)).toBeFalsy();
expect(formComponent.isOutcomeButtonVisible(outcome)).toBeFalsy();
});
it('should allow controlling [save] button visibility', () => {
@@ -124,10 +124,10 @@ describe('ActivitiForm', () => {
let outcome = new FormOutcomeModel(formModel, { id: '$save', name: FormOutcomeModel.COMPLETE_ACTION });
formComponent.showCompleteButton = true;
expect(formComponent.isOutcomeButtonEnabled(outcome)).toBeTruthy();
expect(formComponent.isOutcomeButtonVisible(outcome)).toBeTruthy();
formComponent.showCompleteButton = false;
expect(formComponent.isOutcomeButtonEnabled(outcome)).toBeFalsy();
expect(formComponent.isOutcomeButtonVisible(outcome)).toBeFalsy();
});
it('should load form on refresh', () => {

View File

@@ -128,7 +128,7 @@ export class ActivitiForm implements OnInit, AfterViewChecked, OnChanges {
showSaveButton: boolean = true;
@Input()
showDebugButton: boolean = false;
showDebugButton: boolean = true;
@Input()
readOnly: boolean = false;
@@ -175,6 +175,21 @@ export class ActivitiForm implements OnInit, AfterViewChecked, OnChanges {
}
isOutcomeButtonEnabled(outcome: FormOutcomeModel): boolean {
if (this.form.readOnly) {
return false;
}
if (outcome) {
// Make 'Save' button always available
if (outcome.name === FormOutcomeModel.SAVE_ACTION) {
return true;
}
return this.form.isValid;
}
return false;
}
isOutcomeButtonVisible(outcome: FormOutcomeModel): boolean {
if (outcome && outcome.name) {
if (outcome.name === FormOutcomeModel.COMPLETE_ACTION) {
return this.showCompleteButton;

View File

@@ -65,6 +65,17 @@ export class FormFieldModel extends FormWidgetModel {
return this._readOnly;
}
isValid(): boolean {
if (this.required) {
if (this.type === FormFieldTypes.TEXT || this.type === FormFieldTypes.MULTILINE_TEXT) {
return this._value ? true : false;
}
}
return true;
}
constructor(form: FormModel, json?: any) {
super(form, json);
@@ -129,7 +140,6 @@ export class FormFieldModel extends FormWidgetModel {
}
updateForm() {
switch (this.type) {
case FormFieldTypes.DROPDOWN:
/*
@@ -177,5 +187,7 @@ export class FormFieldModel extends FormWidgetModel {
this.form.values[this.id] = this.value;
}
}
this.form.onFormFieldChanged(this);
}
}

View File

@@ -20,6 +20,7 @@ import { FormValues } from './form-values';
import { ContainerModel } from './container.model';
import { TabModel } from './tab.model';
import { FormOutcomeModel } from './form-outcome.model';
import { FormFieldModel } from './form-field.model';
export class FormModel {
@@ -31,6 +32,7 @@ export class FormModel {
private _name: string;
private _taskId: string;
private _taskName: string = FormModel.UNSET_TASK_NAME;
private _isValid: boolean = true;
get id(): string {
return this._id;
@@ -48,6 +50,10 @@ export class FormModel {
return this._taskName;
}
get isValid(): boolean {
return this._isValid;
}
readOnly: boolean = false;
tabs: TabModel[] = [];
fields: ContainerModel[] = [];
@@ -102,7 +108,8 @@ export class FormModel {
if (field.tab) {
let tab = tabCache[field.tab];
if (tab) {
tab.fields.push(new ContainerModel(this, field.json));
// tab.fields.push(new ContainerModel(this, field.json));
tab.fields.push(field);
}
}
}
@@ -119,6 +126,48 @@ export class FormModel {
}
}
onFormFieldChanged(field: FormFieldModel) {
this.validateField(field);
}
// TODO: evaluate and cache once the form is loaded
private getFormFields(): FormFieldModel[] {
let result: FormFieldModel[] = [];
for (let i = 0; i < this.fields.length; i++) {
let container = this.fields[i];
for (let j = 0; j < container.columns.length; j++) {
let column = container.columns[j];
for (let k = 0; k < column.fields.length; k++) {
let field = column.fields[k];
result.push(field);
}
}
}
return result;
}
private validateForm() {
this._isValid = true;
let fields = this.getFormFields();
for (let i = 0; i < fields.length; i++) {
if (!fields[i].isValid()) {
this._isValid = false;
return;
}
}
}
private validateField(field: FormFieldModel) {
if (!field) return;
if (!field.isValid()) {
this._isValid = false;
return;
}
this.validateForm();
}
private parseContainerFields(json: any): ContainerModel[] {
let fields = [];

View File

@@ -3,6 +3,7 @@
type="text"
rows= "3"
[attr.id]="field.id"
[attr.required]="isRequired()"
[(ngModel)]="field.value"
(ngModelChange)="checkVisibility(field)"
[disabled]="field.readOnly">

View File

@@ -2,6 +2,7 @@
<input class="mdl-textfield__input"
type="text"
[attr.id]="field.id"
[attr.required]="isRequired()"
[(ngModel)]="field.value"
(ngModelChange)="checkVisibility(field)"
[disabled]="field.readOnly">

View File

@@ -36,6 +36,13 @@ export class WidgetComponent implements AfterViewInit {
return this.field ? true : false;
}
isRequired(): any {
if (this.field && this.field.required) {
return true;
}
return null;
}
ngAfterViewInit() {
this.setupMaterialComponents();
this.fieldChanged.emit(this.field);