Skip to content

Commit e7fb07c

Browse files
authored
Feature/data type form validation (#54)
* ADD: optional exclusion of constraints The prototype handles only base scenarios with simple tree structure Signed-off-by: Pavel Shalamkov <[email protected]> * ADD: basic mechanism for adding validators based on constraints Signed-off-by: Pavel Shalamkov <[email protected]> * FIX: template issue with multiple validators Signed-off-by: Pavel Shalamkov <[email protected]> * ADD: "en" as default language for translations If no language specified within a model, translation files were not generated Signed-off-by: Pavel Shalamkov <[email protected]> * UPDATE: mechanism for adding validators for scalar types Signed-off-by: Pavel Shalamkov <[email protected]> * ADD: validators for major constraints (scalar types) Signed-off-by: Pavel Shalamkov <[email protected]> * ADD: i18n support for form validators Signed-off-by: Pavel Shalamkov <[email protected]> * ADD: ignore "RangeConstraint" validation for date types Signed-off-by: Pavel Shalamkov <[email protected]> * ADD: extract constraints from complex elements in Prompter Signed-off-by: Pavel Shalamkov <[email protected]> * FIX: empty type definition for "Either" Characteristic with Trait parent Signed-off-by: Pavel Shalamkov <[email protected]> * FIX: incorrect names generated from Characteristic with Trait parent Signed-off-by: Pavel Shalamkov <[email protected]> * ADD: handle constraints for complex field types Signed-off-by: Pavel Shalamkov <[email protected]> * UPDATE: "README.md" files with validation-related info Signed-off-by: Pavel Shalamkov <[email protected]> * UPDATE: make "isDirectGroupValidator" field mandatory in "ValidatorConfig" interface Signed-off-by: Pavel Shalamkov <[email protected]> * ADD: validators for "number" form field Signed-off-by: Pavel Shalamkov <[email protected]> * ADD: validators "data" category data types Exceptions: xsd:date, xsd:dateTime, xsd:dateTimeStamp Signed-off-by: Pavel Shalamkov <[email protected]> * ADD: validators for string-like data types Signed-off-by: Pavel Shalamkov <[email protected]> * FIX: incorrect detection of "isDirectGroupValidator" property Signed-off-by: Pavel Shalamkov <[email protected]> * UPDATE: "form" feature documentation "Validation" section Signed-off-by: Pavel Shalamkov <[email protected]> * ADD: types for form validators Signed-off-by: Pavel Shalamkov <[email protected]> * UPDATE: make "anyUriValidator" and "curieValidator" more strict Signed-off-by: Pavel Shalamkov <[email protected]> * FIX: prompter error with empty "excludedProperties" Signed-off-by: Pavel Shalamkov <[email protected]> * FIX: error from child controls is not shown for "list" type Signed-off-by: Pavel Shalamkov <[email protected]> --------- Signed-off-by: Pavel Shalamkov <[email protected]>
1 parent 40b335e commit e7fb07c

File tree

71 files changed

+2758
-305
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

71 files changed

+2758
-305
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,7 @@ The form schematics can be used for form generation.
312312
4. Multiple aspect models selection
313313
5. Wizard output to regenerate the same form without going through the wizard again
314314
6. Possibility to set the form as read only
315+
7. Validation rules for form fields and groups (partial support)
315316

316317
### How to generate a form component with the schematics command
317318

src/ng-generate/components/form/README.md

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
- [Generate a component with a custom name](#generate-a-component-with-a-custom-name)
66
- [Select the element for which the form will be generated](#select-the-element-for-which-the-form-will-be-generated)
77
- [Exclude one or more properties from the generation](#exclude-one-or-more-properties-from-the-generation)
8+
- [Exclude one or more constraints from the generation](#exclude-one-or-more-constraints-from-the-generation)
89
- [Multi-version support for Aspect Models](#multi-version-support-for-aspect-models)
910
- [Manual adaptions in _app.module.ts_](#manual-adaptions-in-appmodulets)
1011
- [Show form as read only](#show-form-as-read-only)
@@ -17,6 +18,16 @@
1718
- [Generate the environments files](#generate-the-environments-files)
1819
- [Output](#output)
1920
- [Form structure](#form-structure)
21+
- [Validation](#validation)
22+
- [Base validators](#base-validators)
23+
- [Constraint validators](#constraint-validators)
24+
- [Supported Constraint types](#supported-constraint-types)
25+
- [Unsupported Constraint types](#unsupported-constraint-types)
26+
- [Type-specific validators](#type-specific-validators)
27+
- [Supported complex data types](#supported-complex-data-types)
28+
- [Unsupported complex data types](#unsupported-complex-data-types)
29+
- [Supported scalar data types](#supported-scalar-data-types)
30+
- [Unsupported scalar data types](#unsupported-scalar-data-types)
2031
- [Usage](#usage)
2132
- [Working with list-like controls](#working-with-list-like-controls)
2233

@@ -117,6 +128,24 @@ should be removed from the form.
117128

118129
---
119130

131+
## Exclude one or more constraints from the generation
132+
133+
One or more constraints can be excluded during the initial setup when
134+
the following question appears:
135+
136+
```bash
137+
Choose the constraints to ignore in the form: (Press <space> to select, <a> to toggle all, <i> to invert selection, and <enter> to proceed)
138+
>( ) urn:samm:org.eclipse.digitaltwin:1.0.0#LengthConstraintEitherRight
139+
( ) urn:samm:org.eclipse.digitaltwin:1.0.0#RangeConstraintCollection
140+
```
141+
142+
The constraints will be automatically read from the corresponding subtree of the selected element (Aspect Model Element or Entity),
143+
and can be select/deselect in order to ignore/keep them in the generated form.
144+
145+
If a constraint relates to a subtree of previously excluded property, it will not be shown in the list during this step.
146+
147+
---
148+
120149
## Multi-version support for Aspect Models
121150

122151
By default, the support for different versions of the Aspect Models is
@@ -350,6 +379,94 @@ Since the generated form is an Angular Reactive Form, all its methods are availa
350379

351380
---
352381

382+
## Validation
383+
384+
Each form field or form group can have multiple validators depending on the subtree structure, taking into consideration element's data type, associated constraints, etc.
385+
386+
### Base validators
387+
388+
Each field/group can be marked as "required" depending on whether the corresponding element in the parsed model is considered as optional or not (has `"isOptional": true | false`).
389+
390+
### Constraint validators
391+
392+
During the generation process, validation rules from constraints will be extracted and applied to the corresponding form elements (individual fields or groups) if they were not explicitly excluded in one of the prompter questions.
393+
394+
When dealing with complex data types, the validation rules from constraints are applied to its children in most cases, however, there are exceptions.
395+
For instance, [Length Constraint](https://eclipse-esmf.github.io/samm-specification/snapshot/characteristics.html#length-constraint) is applied directly to a group when working with Collection Characteristics (Collection, Set, Sorted Set, List).
396+
397+
Consult [ESMF documentation](https://eclipse-esmf.github.io/samm-specification/snapshot/characteristics.html#constraints) for more details on how different types of constraints work with different element types.
398+
399+
#### Supported Constraint types
400+
401+
* EncodingConstraint
402+
* FixedPointConstraint
403+
* LengthConstraint
404+
* RangeConstraint (except dates)
405+
* RegularExpressionConstraint
406+
407+
#### Unsupported Constraint types
408+
409+
* LanguageConstraint
410+
* LocaleConstraint
411+
* RangeConstraint (for dates)
412+
413+
### Type-specific validators
414+
415+
Since some fields can imply additional restrictions depending on the corresponding element type or its configuration, additional validators will be applied to the respective form control.
416+
A typical example of such case could be "Either" characteristic, which expects values of its child controls to be specified and unique.
417+
418+
#### Supported complex data types
419+
420+
* Either
421+
* StructuredValue
422+
423+
#### Unsupported complex data types
424+
425+
_None/Unknown_
426+
427+
#### Supported scalar data types
428+
429+
* xsd:string
430+
* xsd:boolean
431+
* xsd:decimal
432+
* xsd:integer
433+
* xsd:double
434+
* xsd:float
435+
* xsd:byte
436+
* xsd:short
437+
* xsd:int
438+
* xsd:long
439+
* xsd:unsignedByte
440+
* xsd:unsignedShort
441+
* xsd:unsignedInt
442+
* xsd:unsignedLong
443+
* xsd:positiveInteger
444+
* xsd:nonNegativeInteger
445+
* xsd:negativeInteger
446+
* xsd:nonPositiveInteger
447+
* xsd:gYear
448+
* xsd:gMonth
449+
* xsd:gDay
450+
* xsd:gYearMonth
451+
* xsd:gMonthDay
452+
* xsd:duration
453+
* xsd:yearMonthDuration
454+
* xsd:dayTimeDuration
455+
* xsd:time
456+
* xsd:hexBinary
457+
* xsd:base64Binary
458+
* xsd:anyURI
459+
* samm:curie
460+
* rdf:langString
461+
462+
#### Unsupported scalar data types
463+
464+
* xsd:date
465+
* xsd:dateTime
466+
* xsd:dateTimeStamp
467+
468+
---
469+
353470
## Usage
354471

355472
For simple scenarios, the root component of the generated form provides the following outputs:

src/ng-generate/components/form/generators/components/fields/FormFieldStrategy.ts

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,15 @@
1111
* SPDX-License-Identifier: MPL-2.0
1212
*/
1313

14-
import {Characteristic, Property} from '@esmf/aspect-model-loader';
14+
import {Characteristic, Constraint, DefaultConstraint, DefaultTrait, Property} from '@esmf/aspect-model-loader';
1515
import {apply, applyTemplates, chain, MergeStrategy, mergeWith, move, Rule, SchematicContext, url} from '@angular-devkit/schematics';
1616
import {strings} from '@angular-devkit/core';
1717
import {templateInclude} from '../../../../shared/include';
1818
import {addToComponentModule} from '../../../../../../utils/angular';
1919
import {getFormFieldStrategy} from './index';
20-
21-
export interface ValidatorConfig {
22-
definition: string;
23-
errorCode: string;
24-
errorMessage: string;
25-
}
20+
import {getConstraintValidatorStrategy} from '../validators/constraint/index';
21+
import {ConstraintValidatorStrategyClass} from '../validators/constraint/constraint-validator-strategies';
22+
import {DataType, GenericValidator, ValidatorConfig} from '../validators/validatorsTypes';
2623

2724
export interface BaseFormFieldConfig {
2825
name: string;
@@ -43,43 +40,78 @@ export interface FormFieldConfig extends BaseFormFieldConfig {
4340
isScalarChild?: boolean;
4441
}
4542

46-
export class FormFieldStrategy {
43+
export abstract class FormFieldStrategy {
4744
pathToFiles: string;
4845
hasChildren: boolean;
4946
options: any;
47+
isList = false;
5048

5149
static isTargetStrategy(child: Characteristic): boolean {
5250
throw new Error('An implementation of the method has to be provided by a derived class');
5351
}
5452

55-
static getShortUrn(child: Characteristic): string | undefined {
56-
return child.dataType?.shortUrn;
53+
static getShortUrn(child: Characteristic): DataType {
54+
return child.dataType?.shortUrn as DataType;
5755
}
5856

5957
constructor(
6058
options: any,
6159
public context: SchematicContext,
6260
public parent: Property,
6361
public child: Characteristic,
64-
public fieldName: string
62+
public fieldName: string,
63+
public constraints: Constraint[]
6564
) {
6665
this.options = {...options};
6766
}
6867

68+
getValidatorsConfigs(ignoreConstraintValidatorStrategies: ConstraintValidatorStrategyClass = []): ValidatorConfig[] {
69+
return [
70+
...this.getBaseValidatorsConfigs(),
71+
...this.getDataTypeValidatorsConfigs(),
72+
...this.getConstraintValidatorsConfigs(ignoreConstraintValidatorStrategies),
73+
];
74+
}
75+
6976
getBaseValidatorsConfigs(): ValidatorConfig[] {
7077
const validatorsConfigs: ValidatorConfig[] = [];
7178

7279
if (!this.parent.isOptional) {
7380
validatorsConfigs.push({
81+
name: GenericValidator.Required,
7482
definition: 'Validators.required',
75-
errorCode: 'required',
76-
errorMessage: `${this.fieldName} is required.`,
83+
isDirectGroupValidator: false,
7784
});
7885
}
7986

8087
return validatorsConfigs;
8188
}
8289

90+
getDataTypeValidatorsConfigs(): ValidatorConfig[] {
91+
return [];
92+
}
93+
94+
getConstraintValidatorsConfigs(ignoreStrategies: ConstraintValidatorStrategyClass): ValidatorConfig[] {
95+
const applicableConstraints: Constraint[] = this.constraints.filter(
96+
constraint =>
97+
// Check that it's not excluded explicitly
98+
!this.options.excludedConstraints.includes(constraint.aspectModelUrn) &&
99+
// It's not a direct instance of "DefaultConstraint" (it contains no validation rules)
100+
constraint.constructor !== DefaultConstraint
101+
);
102+
103+
return applicableConstraints.reduce((acc, constraint) => {
104+
const validatorStrategy = getConstraintValidatorStrategy(constraint, this.child);
105+
const isIgnoredStrategy = !!ignoreStrategies.find(ignoredStrategy => validatorStrategy instanceof ignoredStrategy);
106+
107+
if (isIgnoredStrategy) {
108+
return acc;
109+
}
110+
111+
return [...acc, ...validatorStrategy.getValidatorsConfigs()];
112+
}, []);
113+
}
114+
83115
getBaseFormFieldConfig(): BaseFormFieldConfig {
84116
return {
85117
name: this.fieldName,
@@ -149,6 +181,12 @@ export class FormFieldStrategy {
149181
}
150182

151183
getChildStrategy(parent: Property, child: Characteristic): FormFieldStrategy {
152-
return getFormFieldStrategy(this.options, this.context, this.parent, child, child.name);
184+
return getFormFieldStrategy(
185+
this.options,
186+
this.context,
187+
this.parent,
188+
child,
189+
child instanceof DefaultTrait ? child.baseCharacteristic.name : child.name
190+
);
153191
}
154192
}

src/ng-generate/components/form/generators/components/fields/boolean/BooleanFormFieldStrategy.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,21 @@
1313

1414
import {Characteristic} from '@esmf/aspect-model-loader';
1515
import {FormFieldConfig, FormFieldStrategy} from '../FormFieldStrategy';
16+
import {DataType} from '../../validators/validatorsTypes';
1617

1718
export class BooleanFormFieldStrategy extends FormFieldStrategy {
1819
pathToFiles = './generators/components/fields/boolean/files';
1920
hasChildren = false;
2021

2122
static isTargetStrategy(child: Characteristic): boolean {
22-
const urn = this.getShortUrn(child);
23-
return urn === 'boolean';
23+
const type = this.getShortUrn(child);
24+
return type === DataType.Boolean;
2425
}
2526

2627
buildConfig(): FormFieldConfig {
2728
return {
2829
...this.getBaseFormFieldConfig(),
29-
validators: [...this.getBaseValidatorsConfigs()],
30+
validators: this.getValidatorsConfigs(),
3031
};
3132
}
3233
}

src/ng-generate/components/form/generators/components/fields/boolean/files/__name@dasherize__.component.ts.template

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,20 @@
22

33
import {Attribute, Component, forwardRef} from '@angular/core';
44
import {CommonModule} from '@angular/common';
5-
import {FormControl, FormsModule, NG_VALUE_ACCESSOR, ReactiveFormsModule, Validators} from '@angular/forms';
5+
import {FormControl, FormsModule, NG_VALUE_ACCESSOR, ReactiveFormsModule, ValidatorFn, Validators} from '@angular/forms';
66
import {MatFormFieldModule} from '@angular/material/form-field';
77
import {MatInputModule} from '@angular/material/input';
88
import {MatCheckboxModule} from "@angular/material/checkbox";
99
import {FormControlReusable} from "<% if (options.enableVersionSupport) { %>../<% } %>../../../utils/form-control-reusable";
10+
import {FormValidators} from "<% if (options.enableVersionSupport) { %>../<% } %>../../../utils/form-validators";
1011

11-
export const <%= options.fieldConfig.name %>FormControl = new FormControl(
12-
false,
13-
[
14-
<% for(let validator of options.fieldConfig.validators) { %>
15-
<%= validator.definition %>
16-
<% } %>
17-
]
18-
);
12+
export const validators: {[key: string]: ValidatorFn} = {
13+
<% for(let validator of options.fieldConfig.validators) { %>
14+
<%= validator.name %>: <%= validator.definition %>,
15+
<% } %>
16+
}
17+
18+
export const <%= options.fieldConfig.name %>FormControl = new FormControl(false, Object.values(validators));
1919

2020
@Component({
2121
selector: '<%= options.fieldConfig.selector %>',
@@ -37,10 +37,4 @@ export class <%= classify(options.fieldConfig.name) %>Component extends FormCont
3737
constructor(@Attribute('formControlName') public formControlName: string) {
3838
super();
3939
}
40-
41-
get errorMessage() {
42-
return this.formControl.hasError('required')
43-
? 'The field is required'
44-
: 'The value is invalid or empty';
45-
}
4640
}

src/ng-generate/components/form/generators/components/fields/complex/ComplexFormFieldStrategy.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export class ComplexFormFieldStrategy extends FormFieldStrategy {
2626
buildConfig(): FormFieldConfig {
2727
return {
2828
...this.getBaseFormFieldConfig(),
29-
validators: [...this.getBaseValidatorsConfigs()],
29+
validators: this.getValidatorsConfigs(),
3030
children: this.getChildConfigs(),
3131
};
3232
}

src/ng-generate/components/form/generators/components/fields/date/DateFormFieldStrategy.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,37 +13,39 @@
1313

1414
import {Characteristic} from '@esmf/aspect-model-loader';
1515
import {FormFieldConfig, FormFieldStrategy} from '../FormFieldStrategy';
16+
import {ConstraintValidatorRangeStrategy} from '../../validators/constraint/ConstraintValidatorRangeStrategy';
17+
import {DataType} from '../../validators/validatorsTypes';
1618

1719
const DEFAULT_FORMAT = 'yyyy-MM-DD';
1820
const typesConfigs = [
1921
{
20-
type: 'date',
22+
type: DataType.Date,
2123
format: 'yyyy-MM-DD',
2224
},
2325
];
24-
const supportedTypes = typesConfigs.map(dt => dt.type);
26+
const supportedTypes: DataType[] = typesConfigs.map(dt => dt.type);
2527

2628
export class DateFormFieldStrategy extends FormFieldStrategy {
2729
pathToFiles = './generators/components/fields/date/files';
2830
hasChildren = false;
2931

3032
static isTargetStrategy(child: Characteristic): boolean {
31-
const urn = this.getShortUrn(child);
32-
return urn ? supportedTypes.includes(urn) : false;
33+
const type = this.getShortUrn(child);
34+
return type ? supportedTypes.includes(type) : false;
3335
}
3436

3537
buildConfig(): FormFieldConfig {
3638
return {
3739
...this.getBaseFormFieldConfig(),
3840
exampleValue: this.parent.exampleValue || '',
39-
validators: [...this.getBaseValidatorsConfigs()],
41+
validators: this.getValidatorsConfigs([ConstraintValidatorRangeStrategy]),
4042
dataFormat: this.getDataFormat(),
4143
};
4244
}
4345

4446
getDataFormat(): string {
45-
const urn = DateFormFieldStrategy.getShortUrn(this.child);
46-
const format = typesConfigs.find(dt => dt.type === urn)?.format;
47+
const type = DateFormFieldStrategy.getShortUrn(this.child);
48+
const format = typesConfigs.find(dt => dt.type === type)?.format;
4749
return format || DEFAULT_FORMAT;
4850
}
4951
}

0 commit comments

Comments
 (0)