[ADF-584] [ADF-3561] Validation error summary for error component (#3795)

* emit Forms error

* add demo

* add documentation
remove old form tag deprecated in 1.x
fix translation outcome

* remove prevent default validation

* fix lint
This commit is contained in:
Eugenio Romano
2018-09-18 17:40:00 +01:00
committed by GitHub
parent ec633f27d6
commit 219e093d66
20 changed files with 237 additions and 238 deletions

View File

@@ -38,7 +38,7 @@ import { WidgetComponent } from './../widgets/widget.component';
declare var adf: any;
@Component({
selector: 'adf-form-field, form-field',
selector: 'adf-form-field',
template: `
<div [id]="'field-'+field?.id+'-container'"
[hidden]="!field?.isVisible"

View File

@@ -31,7 +31,7 @@
<div *ngIf="!form.hasTabs() && form.hasFields()">
<div *ngFor="let field of form.fields">
<form-field [field]="field.field"></form-field>
<adf-form-field [field]="field.field"></adf-form-field>
</div>
</div>
</mat-card-content>
@@ -43,7 +43,7 @@
[disabled]="!isOutcomeButtonEnabled(outcome)"
[class.adf-form-hide-button]="!isOutcomeButtonVisible(outcome, form.readOnly)"
(click)="onOutcomeClicked(outcome)">
{{outcome.name | uppercase}}
{{outcome.name | uppercase | translate}}
</button>
</mat-card-actions>
</mat-card>

View File

@@ -16,16 +16,24 @@
*/
/* tslint:disable */
import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewEncapsulation } from '@angular/core';
import {
Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit,
Output, SimpleChanges, ViewEncapsulation
} from '@angular/core';
import { FormErrorEvent, FormEvent } from './../events/index';
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, FormFieldValidator } from './widgets/core/index';
import {
FormFieldModel, FormModel, FormOutcomeEvent, FormOutcomeModel,
FormValues, FormFieldValidator
} from './widgets/core/index';
import { Observable, of } from 'rxjs';
import { WidgetVisibilityService } from './../services/widget-visibility.service';
import { switchMap } from 'rxjs/operators';
import { ValidateFormEvent } from './../events/validate-form.event';
import { Subscription } from 'rxjs';
@Component({
selector: 'adf-form',
@@ -33,14 +41,14 @@ import { switchMap } from 'rxjs/operators';
styleUrls: ['./form.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class FormComponent implements OnInit, OnChanges {
export class FormComponent implements OnInit, OnChanges, OnDestroy {
static SAVE_OUTCOME_ID: string = '$save';
static COMPLETE_OUTCOME_ID: string = '$complete';
static START_PROCESS_OUTCOME_ID: string = '$startProcess';
static CUSTOM_OUTCOME_ID: string = '$custom';
static COMPLETE_BUTTON_COLOR: string = 'primary';
static COMPLETE_OUTCOME_NAME: string ='Complete'
static COMPLETE_OUTCOME_NAME: string = 'Complete'
/** Underlying form model instance. */
@Input()
@@ -138,18 +146,25 @@ export class FormComponent implements OnInit, OnChanges {
@Output()
formDataRefreshed: EventEmitter<FormModel> = new EventEmitter<FormModel>();
/** Emitted when form validations has validation error.*/
@Output()
formError: EventEmitter<FormFieldModel[]> = new EventEmitter<FormFieldModel[]>();
/** Emitted when any outcome is executed. Default behaviour can be prevented
* via `event.preventDefault()`.
*/
@Output()
executeOutcome: EventEmitter<FormOutcomeEvent> = new EventEmitter<FormOutcomeEvent>();
/** @deprecated in 2.4.0, will be renamed in error in 3.x.x */
/** Emitted when any error occurs. */
@Output()
onError: EventEmitter<any> = new EventEmitter<any>();
debugMode: boolean = false;
protected subscriptions: Subscription[] = [];
constructor(protected formService: FormService,
protected visibilityService: WidgetVisibilityService,
private ecmModelService: EcmModelService,
@@ -214,9 +229,21 @@ export class FormComponent implements OnInit, OnChanges {
}
ngOnInit() {
this.formService.formContentClicked.subscribe((content: ContentLinkModel) => {
this.formContentClicked.emit(content);
});
this.subscriptions.push(
this.formService.formContentClicked.subscribe((content: ContentLinkModel) => {
this.formContentClicked.emit(content);
}),
this.formService.validateForm.subscribe((validateFormEvent: ValidateFormEvent) => {
if (validateFormEvent.errorsField.length > 0) {
this.formError.next(validateFormEvent.errorsField);
}
})
);
}
ngOnDestroy() {
this.subscriptions.forEach(subscription => subscription.unsubscribe());
this.subscriptions = [];
}
ngOnChanges(changes: SimpleChanges) {
@@ -338,12 +365,12 @@ export class FormComponent implements OnInit, OnChanges {
getFormByTaskId(taskId: string): Promise<FormModel> {
return new Promise<FormModel>((resolve, reject) => {
this.findProcessVariablesByTaskId(taskId).subscribe( (processVariables) => {
this.findProcessVariablesByTaskId(taskId).subscribe((processVariables) => {
this.formService
.getTaskForm(taskId)
.subscribe(
form => {
const parsedForm = this.parseForm(form);
const parsedForm = this.parseForm(form);
this.visibilityService.refreshVisibility(parsedForm);
parsedForm.validateForm();
this.form = parsedForm;
@@ -364,16 +391,16 @@ export class FormComponent implements OnInit, OnChanges {
this.formService
.getFormDefinitionById(formId)
.subscribe(
form => {
this.formName = form.name;
this.form = this.parseForm(form);
this.visibilityService.refreshVisibility(this.form);
this.form.validateForm();
this.onFormLoaded(this.form);
},
(error) => {
this.handleError(error);
}
form => {
this.formName = form.name;
this.form = this.parseForm(form);
this.visibilityService.refreshVisibility(this.form);
this.form.validateForm();
this.onFormLoaded(this.form);
},
(error) => {
this.handleError(error);
}
);
}
@@ -381,22 +408,22 @@ export class FormComponent implements OnInit, OnChanges {
this.formService
.getFormDefinitionByName(formName)
.subscribe(
id => {
this.formService.getFormDefinitionById(id).subscribe(
form => {
this.form = this.parseForm(form);
this.visibilityService.refreshVisibility(this.form);
this.form.validateForm();
this.onFormLoaded(this.form);
},
(error) => {
this.handleError(error);
}
);
},
(error) => {
this.handleError(error);
}
id => {
this.formService.getFormDefinitionById(id).subscribe(
form => {
this.form = this.parseForm(form);
this.visibilityService.refreshVisibility(this.form);
this.form.validateForm();
this.onFormLoaded(this.form);
},
(error) => {
this.handleError(error);
}
);
},
(error) => {
this.handleError(error);
}
);
}
@@ -405,11 +432,11 @@ export class FormComponent implements OnInit, OnChanges {
this.formService
.saveTaskForm(this.form.taskId, this.form.values)
.subscribe(
() => {
this.onTaskSaved(this.form);
this.storeFormAsMetadata();
},
error => this.onTaskSavedError(this.form, error)
() => {
this.onTaskSaved(this.form);
this.storeFormAsMetadata();
},
error => this.onTaskSavedError(this.form, error)
);
}
}
@@ -419,11 +446,11 @@ export class FormComponent implements OnInit, OnChanges {
this.formService
.completeTaskForm(this.form.taskId, this.form.values, outcome)
.subscribe(
() => {
this.onTaskCompleted(this.form);
this.storeFormAsMetadata();
},
error => this.onTaskCompletedError(this.form, error)
() => {
this.onTaskCompleted(this.form);
this.storeFormAsMetadata();
},
error => this.onTaskCompletedError(this.form, error)
);
}
}
@@ -470,9 +497,9 @@ export class FormComponent implements OnInit, OnChanges {
private loadFormForEcmNode(nodeId: string): void {
this.nodeService.getNodeMetadata(nodeId).subscribe(data => {
this.data = data.metadata;
this.loadFormFromActiviti(data.nodeType);
},
this.data = data.metadata;
this.loadFormFromActiviti(data.nodeType);
},
this.handleError);
}
@@ -501,8 +528,8 @@ export class FormComponent implements OnInit, OnChanges {
private storeFormAsMetadata() {
if (this.saveMetadata) {
this.ecmModelService.createEcmTypeForActivitiForm(this.formName, this.form).subscribe(type => {
this.nodeService.createNodeMetadata(type.nodeType || type.entry.prefixedName, EcmModelService.MODEL_NAMESPACE, this.form.values, this.path, this.nameNode);
},
this.nodeService.createNodeMetadata(type.nodeType || type.entry.prefixedName, EcmModelService.MODEL_NAMESPACE, this.form.values, this.path, this.nameNode);
},
(error) => {
this.handleError(error);
}

View File

@@ -12,12 +12,13 @@
<div *ngIf="!form.hasTabs() && form.hasFields()">
<div *ngFor="let field of form.fields">
<form-field [field]="field.field"></form-field>
<adf-form-field [field]="field.field"></adf-form-field>
</div>
</div>
</mat-card-content>
<mat-card-content class="adf-start-form-actions" *ngIf="showOutcomeButtons && form.hasOutcomes()" #outcomesContainer>
<ng-content select="[form-custom-button]"></ng-content>
<mat-card-content class="adf-start-form-actions" *ngIf="showOutcomeButtons && form.hasOutcomes()"
#outcomesContainer>
<ng-content select="[form-custom-button]"></ng-content>
<button *ngFor="let outcome of form.outcomes"
mat-button
[attr.data-automation-id]="'adf-form-' + outcome.name | lowercase"
@@ -25,7 +26,7 @@
[class.mdl-button--colored]="!outcome.isSystem"
[class.adf-form-hide-button]="!isOutcomeButtonVisible(outcome, form.readOnly)"
(click)="onOutcomeClicked(outcome)">
{{outcome.name}}
{{outcome.name| uppercase | translate}}
</button>
</mat-card-content>
<mat-card-actions *ngIf="showRefreshButton">

View File

@@ -15,30 +15,26 @@
* limitations under the License.
*/
import { Component, ElementRef, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild, ViewEncapsulation, OnDestroy } from '@angular/core';
import {
Component,
ElementRef,
EventEmitter,
Input,
OnChanges,
OnInit,
Output,
SimpleChanges,
ViewChild,
ViewEncapsulation,
OnDestroy
} from '@angular/core';
import { FormService } from './../services/form.service';
import { WidgetVisibilityService } from './../services/widget-visibility.service';
import { FormComponent } from './form.component';
import { ContentLinkModel } from './widgets/core/content-link.model';
import { FormOutcomeModel } from './widgets/core/index';
import { Subscription } from 'rxjs';
import { ValidateFormEvent } from './../events/validate-form.event';
/**
* Displays the start form for a named process definition, which can be used to retrieve values to start a new process.
*
* After the form has been completed the form values are available from the attribute component.form.values and
* component.form.isValid (boolean) can be used to check the if the form is valid or not. Both of these properties are
* updated as the user types into the form.
*
* @Input
* {processDefinitionId} string: The process definition ID
* {showOutcomeButtons} boolean: Whether form outcome buttons should be shown, this is now always active to show form outcomes
* @Output
* {formLoaded} EventEmitter - This event is fired when the form is loaded, it pass all the value in the form.
* {formSaved} EventEmitter - This event is fired when the form is saved, it pass all the value in the form.
* {formCompleted} EventEmitter - This event is fired when the form is completed, it pass all the value in the form.
*
*/
@Component({
selector: 'adf-start-form',
templateUrl: './start-form.component.html',
@@ -47,8 +43,6 @@ import { Subscription } from 'rxjs';
})
export class StartFormComponent extends FormComponent implements OnChanges, OnInit, OnDestroy {
private subscriptions: Subscription[] = [];
/** Definition ID of the process to start. */
@Input()
processDefinitionId: string;
@@ -90,6 +84,11 @@ export class StartFormComponent extends FormComponent implements OnChanges, OnIn
this.subscriptions.push(
this.formService.formContentClicked.subscribe(content => {
this.formContentClicked.emit(content);
}),
this.formService.validateForm.subscribe((validateFormEvent: ValidateFormEvent) => {
if (validateFormEvent.errorsField.length > 0) {
this.formError.next(validateFormEvent.errorsField);
}
})
);
}
@@ -155,8 +154,8 @@ export class StartFormComponent extends FormComponent implements OnChanges, OnIn
/** @override */
isOutcomeButtonVisible(outcome: FormOutcomeModel, isFormReadOnly: boolean): boolean {
if (outcome && outcome.isSystem && ( outcome.name === FormOutcomeModel.SAVE_ACTION ||
outcome.name === FormOutcomeModel.COMPLETE_ACTION )) {
if (outcome && outcome.isSystem && (outcome.name === FormOutcomeModel.SAVE_ACTION ||
outcome.name === FormOutcomeModel.COMPLETE_ACTION)) {
return false;
} else if (outcome && outcome.name === FormOutcomeModel.START_PROCESS_ACTION) {
return true;

View File

@@ -13,7 +13,7 @@
<section class="grid-list" *ngIf="content?.isExpanded">
<div class="grid-list-item" *ngFor="let field of fields" [style.width]="getColumnWith(field)">
<form-field *ngIf="field" [field]="field"></form-field>
<adf-form-field *ngIf="field" [field]="field"></adf-form-field>
</div>
</section>

View File

@@ -271,27 +271,6 @@ describe('FormModel', () => {
form.validateField(field);
});
it('should skip form validation when default behaviour prevented', () => {
const form = new FormModel({}, null, false, formService);
let prevented = false;
formService.validateForm.subscribe((event: ValidateFormEvent) => {
event.isValid = false;
event.preventDefault();
prevented = true;
});
const field = jasmine.createSpyObj('FormFieldModel', ['validate']);
spyOn(form, 'getFormFields').and.returnValue([field]);
form.validateForm();
expect(prevented).toBeTruthy();
expect(form.isValid).toBeFalsy();
expect(field.validate).not.toHaveBeenCalled();
});
it('should skip field validation when default behaviour prevented', () => {
const form = new FormModel({}, null, false, formService);

View File

@@ -15,9 +15,11 @@
* limitations under the License.
*/
/* tslint:disable:component-selector */
/* tslint:disable:component-selector */
import { FormFieldEvent, ValidateFormEvent, ValidateFormFieldEvent } from './../../../events/index';
import { FormFieldEvent } from './../../../events/form-field.event';
import { ValidateFormFieldEvent } from './../../../events/validate-form-field.event';
import { ValidateFormEvent } from './../../../events/validate-form.event';
import { FormService } from './../../../services/form.service';
import { ContainerModel } from './container.model';
import { FormFieldTemplates } from './form-field-templates';
@@ -120,9 +122,21 @@ export class FormModel {
}
if (json.fields) {
let saveOutcome = new FormOutcomeModel(this, { id: FormModel.SAVE_OUTCOME, name: 'Save', isSystem: true });
let completeOutcome = new FormOutcomeModel(this, { id: FormModel.COMPLETE_OUTCOME, name: 'Complete', isSystem: true });
let startProcessOutcome = new FormOutcomeModel(this, { id: FormModel.START_PROCESS_OUTCOME, name: 'Start Process', isSystem: true });
let saveOutcome = new FormOutcomeModel(this, {
id: FormModel.SAVE_OUTCOME,
name: 'Save',
isSystem: true
});
let completeOutcome = new FormOutcomeModel(this, {
id: FormModel.COMPLETE_OUTCOME,
name: 'Complete',
isSystem: true
});
let startProcessOutcome = new FormOutcomeModel(this, {
id: FormModel.START_PROCESS_OUTCOME,
name: 'Start Process',
isSystem: true
});
let customOutcomes = (json.outcomes || []).map(obj => new FormOutcomeModel(this, obj));
@@ -176,27 +190,27 @@ export class FormModel {
* @memberof FormModel
*/
validateForm(): void {
const validateFormEvent = new ValidateFormEvent(this);
const validateFormEvent: any = new ValidateFormEvent(this);
let errorsField: FormFieldModel[] = [];
let fields = this.getFormFields();
for (let i = 0; i < fields.length; i++) {
if (!fields[i].validate()) {
errorsField.push(fields[i]);
}
}
if (errorsField.length > 0) {
this._isValid = false;
}
if (this.formService) {
validateFormEvent.isValid = this._isValid;
validateFormEvent.errorsField = errorsField;
this.formService.validateForm.next(validateFormEvent);
}
this._isValid = validateFormEvent.isValid;
if (validateFormEvent.defaultPrevented) {
return;
}
if (validateFormEvent.isValid) {
let fields = this.getFormFields();
for (let i = 0; i < fields.length; i++) {
if (!fields[i].validate()) {
this._isValid = false;
return;
}
}
}
}
/**
@@ -227,8 +241,8 @@ export class FormModel {
if (!field.validate()) {
this._isValid = false;
return;
}
this.validateForm();
}

View File

@@ -2,7 +2,7 @@
<mat-tab-group>
<mat-tab *ngFor="let tab of visibleTabs" [label]="tab.title">
<div *ngFor="let field of tab.fields">
<form-field [field]="field.field"></form-field>
<adf-form-field [field]="field.field"></adf-form-field>
</div>
</mat-tab>
</mat-tab-group>

View File

@@ -17,10 +17,12 @@
import { FormModel } from './../components/widgets/core/index';
import { FormEvent } from './form.event';
import { FormFieldModel } from '../components/widgets/core/form-field.model';
export class ValidateFormEvent extends FormEvent {
isValid = true;
errorsField: FormFieldModel[] = [];
constructor(form: FormModel) {
super(form);

View File

@@ -1,4 +1,7 @@
{
"SAVE": "SAVE",
"START": "COMPLETE",
"START PROCESS": "START PROCESS",
"FORM": {
"START_FORM": {
"TITLE": "Start Form"

View File

@@ -1,4 +1,7 @@
{
"SAVE": "SALVA",
"COMPLETE": "COMPLETA",
"START PROCESS": "INIZIA PROCESSO",
"FORM": {
"START_FORM": {
"TITLE": "Modulo di inizio"