From 6769a5178ab81096d62153005f28d81600a01279 Mon Sep 17 00:00:00 2001 From: Salome DO Date: Mon, 22 Apr 2024 12:38:32 +0200 Subject: [PATCH] feat: update forms doc --- docs/forms/FORM_ERRORS.md | 172 ++++---- docs/forms/FORM_STRUCTURE.md | 385 +++++----------- .../FORM_SUBMIT_AND_INTERCOMMUNICATION.md | 412 +++++++++--------- docs/forms/FORM_VALIDATION.md | 251 +++++------ docs/forms/README.md | 103 +++-- packages/@o3r/forms/README.md | 83 +--- 6 files changed, 558 insertions(+), 848 deletions(-) diff --git a/docs/forms/FORM_ERRORS.md b/docs/forms/FORM_ERRORS.md index a48623e2b9..78699a2c4b 100644 --- a/docs/forms/FORM_ERRORS.md +++ b/docs/forms/FORM_ERRORS.md @@ -1,38 +1,19 @@ -[Form errors](#introduction) - -- [Form errors](#form-errors) - - [Form error store](#form-error-store) - - [Error object model](#error-object-model) - - [Creating error object](#creating-error-object) - - [Custom errors](#custom-errors) - - [Basic/primitive errors](#basicprimitive-errors) - - [Build error messages](#build-error-messages) - - [Display inline error messages](#display-inline-error-messages) - - [Basic errors](#basic-errors) - - [Custom errors](#custom-errors-1) - - [Add errors to the store](#add-errors-to-the-store) - - [Errors translation definition](#errors-translation-definition) - - [Custom errors](#custom-errors-2) - - [Primitive errors](#primitive-errors) - - - # Form errors -Handling the form errors in Otter context (container/presenter, localization ...), it's a bit different from creating a form in a component and do all the logic there. - +Handling the form errors in the Otter context (container/presenter, localization, etc.) is a bit different from creating a form in a component and doing all the logic there. ### Form error store -To have the possibility to display inline error messages in the form and also in error panels (on the top of the page, above submit button ...) the best match is to have a dedicated store for the form errors. In this way we can listen to the store state and display the errors anywhere in the page. -The store is provided in __@o3r/forms__ package. See [Form Error Store](https://github.com/AmadeusITGroup/otter/blob/main/packages/@o3r/forms/src/stores/form-error-messages/form-error-messages.state.ts) for more details and state object model. - +To have the possibility of displaying inline error messages in the form and also in error panels (on the top of the page, above submit button, etc.), +the best option is to have a dedicated NgRX store for the form errors. This way we can listen to the store state and display the errors anywhere in the page. +The store is provided in the __@o3r/forms__ package. See [Form Error Store](https://github.com/AmadeusITGroup/otter/blob/main/packages/@o3r/forms/src/stores/form-error-messages/form-error-messages.state.ts) +for more details and to view the state object model. #### Error object model -The store model object is __FormError__. See below the form errors object models. +The store model object extends the `FormError` interface (see its object model below). -- The __FormError__ contains an identifier for each component which has a form inside, plus the errors associated to that form. +- The `FormError` interface contains an identifier for each component that has a form inside, plus the errors associated to that form. ```typescript /** Form's error messages identified by form id */ @@ -45,9 +26,8 @@ export interface FormError { } ``` -- __ElementError__ -This object contains all the errors associated to the html element. -The identifier __htmlElementId__ can be used as an anchor link to focus on the html element on which the validation has failed +- The `ElementError` object contains all the errors associated to the HTML element. +The identifier `htmlElementId` can be used as an anchor link to focus on the HTML element where the validation failed. ```typescript /** Error messages of the html element identified by its id */ @@ -60,15 +40,13 @@ export interface ElementError { } ``` -- __ErrorMessageObject__ - - associated to an error message on a field. - - It will contain: - - __translationKey__ for the error message - - __longTranslationKey__ used for a more detailed message on the same error - - __translationParams__ translations parameters - - __validationError__ original error object +- `ErrorMessageObject` is associated to an error message on a field. It contains: + - `translationKey`: translation key for the short error message + - `longTranslationKey`: translation key for a more detailed message on the same error + - `translationParams`: translations parameters of the error message + - `validationError`: original error object -```typescript +`````typescript /** The error object saved in the store for a specific element/formControl */ export interface ErrorMessageObject { /** @@ -100,7 +78,7 @@ export interface ErrorMessageObject { * @example * ```typescript * {required: true} - * ``` + * \``` * @example * ```typescript * {max: {max 12, actual: 31}} @@ -108,30 +86,37 @@ export interface ErrorMessageObject { */ validationError?: {[key: string]: any}; } -``` +````` - +You can also find these interfaces [here](https://github.com/AmadeusITGroup/otter/blob/main/packages/%40o3r/forms/src/core/errors.ts). -### Creating error object +### Creating an error object -The presenter has to implement the [Validator](https://angular.io/api/forms/NG_VALIDATORS) or [AsyncValidator](https://angular.io/api/forms/NG_ASYNC_VALIDATORS) in order to give us the possibility to define the error object which will be returned by the form. -The error message structure will be defined in the implementation of __validate__ method. -As __validate__ function should return a [ValidationErrors](https://angular.io/api/forms/ValidationErrors) object, which is a map of custom objects (with type _any_), we can prepare the returned object for the store of error messages. This will ease the process of adding the errors in the store. -We have to make sure that we are providing the __htmlElementId__ for the errors in the store which is matching the __html field__. -For this, the presenter is receiving an __id__ as input and for each field we are concatenating the __id__ with the __formControlName__. As the container is setting a __unique id__ we are sure that we have uniques html ids for the form fields. -The object returned by the __validate__ is the error object which is propagated to the container. +The presenter has to implement the [Validator](https://angular.io/api/forms/NG_VALIDATORS) or the [AsyncValidator](https://angular.io/api/forms/NG_ASYNC_VALIDATORS) interface +in order to give us the possibility of defining the error object that will be returned by the form. -There are 2 types of validators (see [Form Validation](./FORM_VALIDATION.md)), 2 categories of error messages: +The error message structure will be defined in the implementation of the `validate` function. -- one for __custom errors__ - set on the container -- one for __primitive errors__ - computed in the presenter. +As the `validate` function should return a [ValidationErrors](https://angular.io/api/forms/ValidationErrors) object, which is a map of custom objects (of type `any`), +we can prepare the returned object for the store of error messages. This will ease the process of adding the errors in the store. - +We have to make sure that we provide the `htmlElementId` of the errors in the store that match the __HTML fields__. +For this, the presenter receives an `id` as input and for each field we are concatenating, the `id` with the `formControlName`. +Since the container sets a __unique id__, we are sure to have unique HTML identifiers for the form fields. -#### Custom errors +The object returned by the `validate` function is the error object that is propagated to the container. + +#### Categories of error messages -They are returned by __custom validators__ and have the type [CustomErrors](https://github.com/AmadeusITGroup/otter/blob/main/packages/@o3r/forms/src/core/custom-validation.ts) defined in __@o3r/forms__. -This one is using _customErrors_ key with an array of __ErrorMessageObject__ which has to contain all the custom errors for a form control or group. +There are two types of validators (see [Form Validation](./FORM_VALIDATION.md)) and therefore two categories of error messages: + +- __Custom error__ - set in the container +- __Primitive error__ - computed in the presenter + +###### Custom errors + +They are returned by __custom validators__ and have the type `CustomErrors` (defined in [__@o3r/forms__](https://github.com/AmadeusITGroup/otter/blob/main/packages/@o3r/forms/src/core/custom-validation.ts)). +This type uses the `customErrors` key with an `ErrorMessageObject` array, which has to contain all the custom errors of a form control or a group. ```typescript /** @@ -143,25 +128,22 @@ export interface CustomErrors { } ``` -Error object model returned by the validator has to be compliant with the store model. +The error object model returned by the validator has to be compliant with the store model. ```typescript // Example of returned object by the custom validator {customErrors: [{translationKey, longTranslationKey, translationParams}]}; ``` - - -#### Basic/primitive errors - -The error object structure has to be created in the presenter because the __basic validators__ are defined at presenter level (see [FORM_VALIDATION](./FORM_VALIDATION.md)). +###### Basic/primitive errors - +The error object structure has to be created in the presenter because the __basic validators__ are defined at presenter level (see [Form Validation](./FORM_VALIDATION.md)). #### Build error messages -We put in place a generic helper [__getFlatControlErrors__](https://github.com/AmadeusITGroup/otter/blob/main/packages/%40o3r/forms/src/core/helpers.ts) in __@o3r/forms__. -This gets a flattened list of all the errors in the form and it's descendants, concatenating the __custom errors__; The object returned by the helper has [ControlFlatErrors](https://github.com/AmadeusITGroup/otter/blob/main/packages/%40o3r/forms/src/core/flat-errors.ts) type. +We put in place a generic helper `getFlatControlErrors` in [__@o3r/forms__](https://github.com/AmadeusITGroup/otter/blob/main/packages/%40o3r/forms/src/core/helpers.ts). +This function gets a flattened list of all the errors from the form and its descendants and concatenates the __custom errors__. +The object returned by the helper is of type `ControlFlatErrors` (defined in [__@o3r/forms__](https://github.com/AmadeusITGroup/otter/blob/main/packages/%40o3r/forms/src/core/flat-errors.ts)). ```typescript /** @@ -181,7 +163,7 @@ export interface ControlFlatErrors { } ``` -Example of __validate__ method implementation +Below is an example of the implementation of a `validate` function: ```typescript /// ----> in the presenter class @@ -225,14 +207,14 @@ export class FormsPocPresComponent implements OnInit, Validator, FormsPocPresCon return null; } - const formErrors = getFlatControlErrors(this.travelerForm); // ---> use the helper to get the flat list of errors for the form + const formErrors = getFlatControlErrors(this.travelerForm); // ---> use the helper to get the flat list of errors from the form const errors = formErrors.reduce((errorsMap: ValidationErrors, controlFlatErrors: ControlFlatErrors) => { return { // ...errorsMap, [controlFlatErrors.controlName || 'global']: { // ---> use the 'global' key for the errors applied on the root form htmlElementId: `${this.id}${controlFlatErrors.controlName || ''}`, // ---> The html id of the element - errorMessages: (controlFlatErrors.customErrors || []).concat( // ---> errors associated to the html element ( custom errors plus basic ones ) + errorMessages: (controlFlatErrors.customErrors || []).concat( // ---> errors associated to the html element (custom errors plus basic ones) controlFlatErrors.errors.map((error) => { // Translation key creation // As the primitive errors are linked to the presenter we use the component selector, the control name and the error key, to compute the translationKey @@ -297,42 +279,38 @@ export class FormsPocPresComponent implements OnInit, Validator, FormsPocPresCon } ``` -This is only an example of implementation. The _translationKey_ and _translationParams_ can be different implemented depending on the use cases. - +This is only an example of an implementation. The `translationKey` and `translationParams` can be implemented differently depending on the use cases. ### Display inline error messages - +Below, you will find examples of HTML implementations to display basic and custom errors as inline error messages. #### Basic errors ```html -///----> presenter template + - // use the translation object for the translationKey and get the translationParams from the error object returned by 'date-inline-input'. + {{translations.maxMonthInDate | o3rTranslate: {max: travelerForm.controls.dateOfBirth.errors?.max.max} }} ``` - - #### Custom errors ```html -///----> presenter template + - // translation key and params are already accessible in the error object returned by the custom validator + {{customError.translationKey | o3rTranslate: customError.translationParams }} ``` - - ### Add errors to the store -As we already defined the error message object, as the return of __validate__ method in the presenter, we can get the error messages and add them to the store, in the container. Check the example below. +As we have already defined the error message object as the return of the `validate` function in the presenter, we can get the error messages in the container and add them to the store. +Check the example below: ```typescript /// ---> in the container @@ -376,34 +354,33 @@ submitAction() { } ``` -In the example above we save the errors in the store when we execute the submit action. It can be done at valueChanges or statusChanges. - +In the example above, we save the errors in the store when we execute the submit action. This action can be called at `valueChanges` or `statusChanges` in the presenter. ### Errors translation definition -For the localization of the error messages we keep the same way we have today ([LOCALIZATION](../localization/LOCALIZATION.md)), but we have specific places where to define the default translations of error messages. - +For the localization of the error messages, we keep the same way of working as we have today (check out [LOCALIZATION](../localization/LOCALIZATION.md)), +but we have specific places where to define the default translations of error messages. #### Custom errors -Because the form validation depends on business logic and the custom validators are created in the container (see: [Form Validation](./FORM_VALIDATION.md)) we have to provide an error message for each validator and to ensure that the message is translatable. -We have to add the default translation keys, corresponding to the custom validators __in the container__ (_container.localization.json_ file). +Because the form validation depends on business logic and the custom validators are created in the container (see [Form Validation](./FORM_VALIDATION.md)), +we have to provide an error message for each validator and ensure that the message is translatable. +We have to add the default translation keys, corresponding to the custom validators, to the localization file __in the container__. ```typescript - // ---> in container class - /** - * Localization of the component - */ - @Input() - @Localization('./forms-poc-cont.localization.json') // Here we will define the error messages translation keys - public translations: FormsPocContTranslation; +// ---> in container class +/** + * Localization of the component + */ +@Input() +@Localization('./forms-poc-cont.localization.json') // Here we will define the translation keys of the error messages +public translations: FormsPocContTranslation; ``` -Default values for the custom errors +Default values have to be defined for the custom errors, for example: ```json // ----> forms-poc-cont.localization.json -... "travelerForm.dateOfBirth.max": { // ---> travelerForm is the name we have chosen for the form "description": "Validator for date of birth month", "defaultValue": "Max value for the month should be {{ max }}" @@ -415,12 +392,10 @@ Default values for the custom errors ... ``` - - #### Primitive errors These validators are defined and applied at presenter level, so we have to define the translation of the error messages here. -Each possible validator should have a corresponding error message in __presenter.localization.json__ file. +Each possible validator should have a corresponding error message in the presenter's localization file. ```typescript // ---> in presenter class @@ -428,13 +403,14 @@ Each possible validator should have a corresponding error message in __presenter * Localization of the component */ @Input() -@Localization('./forms-pres-cont.localization.json') // Here we will define the error messages translation keys +@Localization('./forms-pres-pres.localization.json') // Here we will define the translation keys of the error messages public translations: FormsPocPresTranslation; ``` -Default values for the custom errors +Default values have to be defined for the primitive errors, for example: ```json +// ----> forms-poc-pres.localization.json // The first key is not related to forms "o3r-forms-poc-pres.key.not.related.to.forms": { "description": "Test Value with a translation", diff --git a/docs/forms/FORM_STRUCTURE.md b/docs/forms/FORM_STRUCTURE.md index 8ca948f510..d8f57bf051 100644 --- a/docs/forms/FORM_STRUCTURE.md +++ b/docs/forms/FORM_STRUCTURE.md @@ -1,104 +1,84 @@ -[Forms structure](#form-structure) - 1. [Container/presenter and reactive forms](#container-presenter) - 1. [Form creation in container or in presenter?](#form-creation) - 2. [Data exchange between container and presenter](#data-exchange) - 1. [Basic case](#data-exchange-basic) - 2. [Complex case](#data-exchange-complex) - 3. [Component Creation](#component-creation) - 1. [Basic case](#basic-case) - 2. [Adding complexity ](#adding-complexity) - 1. [Basic structure](#basic-structure) - 2. [Include Basic validation](#basic-validation) - 1. [Validators definition](#validators-definition) - 2. [Apply validators ](#apply-validators) - 3. [Validators translations](#validators-translation) - 3. [Include Custom Validations](#custom-validators) - 1. [Validators definition](#custom-validators-definition) - 2. [Apply validators ](#custom-apply-validators) - 3. [Validators translations](#custom-validators-translations) - - # Forms structure -Angular provides two approaches for writing the forms, [template-driven forms](https://angular.io/guide/forms) and [model-driven or reactive forms](https://angular.io/guide/reactive-forms). -This documentation will help you with some best practices to be used at the build of Angular reactive forms components in Otter context. +Angular provides two approaches for writing the forms: [template-driven forms](https://angular.io/guide/forms) and [model-driven or reactive forms](https://angular.io/guide/reactive-forms). +This documentation will help you with some best practices to use when building Angular reactive forms components in the Otter context. - -## [Container/presenter](../components/COMPONENT_STRUCTURE.md) and reactive forms -Container/presenter architecture was put in place to ensure the best re-usability/sharing - -### Form creation in container or in presenter? +## Container/presenter and reactive forms +A [container/presenter](../components/COMPONENT_STRUCTURE.md) architecture was put in place to ensure the best reusability and sharing of components. -* The __form creation__ (it can be a [__FormGroup__](https://angular.io/api/forms/FormGroup) or [__FormArray__](https://angular.io/api/forms/FormArray) or [__FormControl__](https://angular.io/api/forms/FormControl)) should be done __in the presenter__ because: - * it's up to the presenter to decide how the data will be displayed/computed. An example is a date which can be displayed in one input field - ([FormControl](https://angular.io/api/forms/FormControl)) in one presenter, or in one [FormGroup](https://angular.io/api/forms/FormGroup) containing 3 FormControls, corresponding to 3 input fields, in other presenter (the container needs only a date). - * we will not use the formGroup / formArray / formControl object as a two-way data binding object between the container and the presenter. -* The __container__ needs only the value and in some specific cases the errors propagated from the presenter. If needed it can set the default value +### Form creation in the container or presenter? -From now on we will refer as __form presenter object__ the __formGroup__ or __formArray__ or __formControl__ created in the presenter. +* The __form creation__ (it can be a [__FormGroup__](https://angular.io/api/forms/FormGroup) or [__FormArray__](https://angular.io/api/forms/FormArray) + or [__FormControl__](https://angular.io/api/forms/FormControl)) should be done __in the presenter__ because: + * It is up to the presenter to decide how the data will be displayed/computed. For example, a date can be displayed in an input field + ([FormControl](https://angular.io/api/forms/FormControl)) in one presenter, or in a [FormGroup](https://angular.io/api/forms/FormGroup) containing + 3 input fields in another presenter (the container only needs a date value). + * We will not use the `formGroup` / `formArray` / `formControl` object as a two-way data binding object between the container and the presenter. +* The __container__ only needs the value and, in some specific cases, the errors propagated from the presenter. If needed, it can set the default value. + +From now on we will refer to __form presenter object__ as the `formGroup` or `formArray` or `formControl` created in the presenter. + +### Data exchange between container and presenter - -### Data exchange between container and presenter in forms context - #### Simple cases -The need in this case is to display the inline errors, check the form validity and emit the form value. +In a simple case, the purpose of the data exchange is to display the inline errors, check the form validity, and emit the form value. * The presenter containing the form should: - * handle the display of form errors - * trigger the form submit - * check the form validity - * use an event emitter to propagate the form value to the container -* The container should intercept the propagated value and execute the submit logic - + * Handle the display of form errors. + * Trigger the form submit. + * Check the form validity. + * Use an event emitter to propagate the form value to the container. +* The container should intercept the propagated value and execute the submit logic. + #### Complex cases -This case includes the simple case plus the display of a messages panel containing the form errors and the flexibility to submit from the presenter or from the page. +Data exchange in a complex case has the same purpose as the simple case, plus the display of a message panel containing the form errors and the ability to submit the form from the presenter or page. * The presenter containing the form should: - * implement [ControlValueAccessor](https://angular.io/api/forms/ControlValueAccessor). It will propagate all the __value/status changes__ done inside the __presenter form object__ to the parent, in our case the container. - In this way it will behave as an __HTML input element__ on which we can __apply__ the [ngModel](https://angular.io/api/forms/NgModel) directive, or we can bind a [FormControl](https://angular.io/api/forms/FormControl#description). - * implement [Validator](https://angular.io/api/forms/Validator) interface, if your form validators are only synchronous or [AsyncValidator](https://angular.io/api/forms/AsyncValidator) interface if the form needs asynchronous validators. See [FORM_VALIDATION](./FORM_VALIDATION.md) for more details about validation in Otter. - * Implementing this interface gives us the possibility to define, in the __validate__ method, the error object model which will be propagated to the parent/container. See [FORM_ERRORS](./FORM_ERRORS.md) for details. -* The container will apply a [Form Control Directive](https://angular.io/api/forms/FormControlDirective) to the presenter form to have the possibility to: - * set the default value for the presenter form object if needed. - * listen to the valueChanges if needed - * listen status changes if needed - * easily get the errors propagated by the presenter - -We prefer to use the __formControl__ rather than __ngModel__ because we can easily listen to the valueChanges or status changes of the presenter form. -Another constraint is that it's easier to identify the container context for the CMS, with one implementation (See [Component Structure](../components/COMPONENT_STRUCTURE.md) for details about the component context). - - -### Component creation -__Component__ here, refers a container and a presenter components. - - -#### Basic case -In this case the only need we have is to implement a form, display the inline errors, check the form validity and do something with the form value. -In this case for the presenter: -* __form__ is __created__ here -* __validators__ applied here (see [FORM_VALIDATION](./FORM_VALIDATION.md) for details and validator types, where they are created) -* __inline errors__ are handled here (see [FORM_ERRORS](./FORM_ERRORS.md) for details about the error messages translations) -* form validity will be checked here -* it will trigger the submission and emit the form value + * Implement [ControlValueAccessor](https://angular.io/api/forms/ControlValueAccessor). It will propagate all the __value/status changes__ done inside the __presenter form object__ to the parent (the container in our case). + In this way, it will behave as an __input HTML element__ on which we can __apply__ the [ngModel](https://angular.io/api/forms/NgModel) directive, or we can bind a [FormControl](https://angular.io/api/forms/FormControl#description). + * Implement the [Validator](https://angular.io/api/forms/Validator) interface if your form validators are only synchronous or the [AsyncValidator](https://angular.io/api/forms/AsyncValidator) interface if the form needs asynchronous validators. + See [Form Validation](./FORM_VALIDATION.md) for more details about validation in Otter. + * Implementing this interface gives us the possibility to define, in the `validate` function, the error object model which will be propagated to the parent/container. See the [form error documentation](./FORM_ERRORS.md) for details. +* The container will apply a [Form Control Directive](https://angular.io/api/forms/FormControlDirective) to the presenter form to give the possibility to: + * Set the default value for the presenter form object if needed. + * Listen to the value changes if needed. + * Listen to the status changes if needed. + * Easily get the errors propagated by the presenter. + +We prefer to use the `formControl` rather than `ngModel` because we can easily listen to the `valueChanges` or `statusChanges` of the presenter form. +Another constraint is that it's easier to identify the container context for the CMS (See [Component Structure](../components/COMPONENT_STRUCTURE.md) for details about the component context). + +## Component creation +Here, a __component__ refers to a container or a presenter component. + +### Basic case +In this case, all we need to do is to implement a form, display the inline errors, check the form validity, and do something with the form value. + +In the presenter: +* The __form__ is __created__. +* The __validators__ are applied (see [Form Validation](./FORM_VALIDATION.md) for details about validators and where they are created). +* The __inline errors__ are handled (see [Form Errors](./FORM_ERRORS.md) for details about the error messages translations). +* The form validity will be checked. +* The submission is triggered and the form value is emitted. The container: -* capture the form value emitted -* execute the submit logic +* Captures the form value emitted. +* Executes the submit logic. -The difference from the default implementation of the [forms in angular](https://angular.io/guide/reactive-forms) is that we have to emit the form value from the container to the presenter, +The difference from the default implementation of the [forms in Angular](https://angular.io/guide/reactive-forms) is that we have to emit the form value from the container to the presenter, using an [@Output](https://angular.io/api/core/Output) event. -Another difference might be related to the custom validators, which we are suggesting to be created in the container because they can be related to the business logic -(Please have a look at the dedicated section for the forms validators: [FORM_VALIDATION](./FORM_VALIDATION.md)). +Another difference might be related to the custom validators, which we suggest to be created in the container because they can be related to the business logic. +(Please have a look at the [dedicated section](./FORM_VALIDATION.md) on the forms validators.). - -#### Adding complexity +### Adding complexity In addition to the simple case, if we need an __error message__ panel, which can be displayed anywhere in the page, -or we need __form submission__, done from the page, we came up with the following implementation. +or to __submit the form outside the component__, we must follow the more complex implementation described below. - -##### 1.Basic structure -The form created in the presenter and the default value should have the same contract. The contract of a form is an interface which defines the form controls names and the type of the value which should be handled by each control. See the example of a component creation. +#### 1. Basic structure +The form created in the presenter and the default value should have the same contract. The contract of a form is an interface that defines +the names of the form controls and the type of value that should be handled by each control. -The example is based on a form used to introduce data for a Traveler object -* Define the contract object +Below is an example of a component creation based on a form used to introduce data for a Traveler object. + +__Define the contract object:__ ```typescript // form object contract export interface Traveler { @@ -107,53 +87,53 @@ export interface Traveler { dateOfBirth: Date; } ``` - * __Container__ class - * Create a form control to set the binding and the default data. +__Container class:__ +* Create a form control to set the binding and the default data. ```typescript // in container class - mainFormControl: FormControl; +mainFormControl: FormControl; - constructor(config: FormsPocContConfig, private store: Store) { - ... - // Default value - this.traveler: Traveler = {firstName: '', lastName: 'TestUser', dateOfBirth: new Date()}; - // define the form control which will be bound to presenter with default value - this.mainFormControl = new FormControl(this.traveler); - ... - } +constructor(config: FormsPocContConfig, private store: Store) { + ... + // Default value + this.traveler: Traveler = {firstName: '', lastName: 'TestUser', dateOfBirth: new Date()}; + // define the form control which will be bound to presenter with default value + this.mainFormControl = new FormControl(this.traveler); +} ngOnInit() { - this.subscriptions.push( - // Subscribe to any change done to the value of the form control applied to the presenter - this.mainFormControl.valueChanges.subscribe((value) => console.log(value)), - // Subscribe to the status change of the form control applied to the presenter - this.mainFormControl.statusChanges.subscribe((value) => console.log(value)) - ); - } + this.subscriptions.push( + // Subscribe to any change done to the value of the form control applied to the presenter + this.mainFormControl.valueChanges.subscribe((value) => console.log(value)), + // Subscribe to the status change of the form control applied to the presenter + this.mainFormControl.statusChanges.subscribe((value) => console.log(value)) + ); +} ``` - * Register the form control in the template context to be recognized if we change the presenter. See [COMPONENT_STRUCTURE](../components/COMPONENT_STRUCTURE.md) for details about the template context. +* Register the form control in the template context to be recognized if we change the presenter. See [COMPONENT_STRUCTURE](../components/COMPONENT_STRUCTURE.md) for details about the template context. ```typescript // in container class - getFormsPocPresContext(overrideContext: Partial): TemplateContext { - return { - config: this.config.presFormsPocConfig || new FormsPocPresConfig(), - inputs: { - validators: this.validators, // ---> the validators applied to the form; we'll see this later - ...overrideContext - }, - outputs: { - onSubmit: this.onSubmit.bind(this), - registerInteraction: this.registerInteraction.bind(this) - }, - parentId: this.id, // ---> this id will be used by the presenter to create html element id's for the form controls inside (it has to be unique) - formControl: this.mainFormControl // ---> this filed is keeping the 'mainFormControl' object in the context. It is not used by the presenter - }; - } +getFormsPocPresContext(overrideContext: Partial): TemplateContext { + return { + config: this.config.presFormsPocConfig || new FormsPocPresConfig(), + inputs: { + validators: this.validators, // ---> the validators applied to the form; we'll see this later + ...overrideContext + }, + outputs: { + onSubmit: this.onSubmit.bind(this), + registerInteraction: this.registerInteraction.bind(this) + }, + parentId: this.id, // ---> this id will be used by the presenter to create html element id's for the form controls inside (it has to be unique) + formControl: this.mainFormControl // ---> this filed is keeping the 'mainFormControl' object in the context. It is not used by the presenter + }; +} ``` + * Container template ```html @@ -176,10 +156,11 @@ ngOnInit() { [ngTemplateOutletContext]="formsPocPresContext$ | async"> ``` -* __Presenter__ class - * Here we have to create the formGroup/formArray/formControl object - * Provide [NG_VALUE_ACCESSOR](https://angular.io/api/forms/NG_VALUE_ACCESSOR) - used to provide a [ControlValueAccessor](https://angular.io/api/forms/DefaultValueAccessor) for form controls, to write a value and listening to changes on input elements. - * Provide [NG_VALIDATORS](https://angular.io/api/forms/NG_VALIDATORS) This is an [InjectionToken](https://angular.io/api/core/InjectionToken) for registering additional synchronous validators used with forms. + +__Presenter class:__ + * Here we have to create the `formGroup`/`formArray`/`formControl` object. + * Provide [NG_VALUE_ACCESSOR](https://angular.io/api/forms/NG_VALUE_ACCESSOR) - used to provide a [ControlValueAccessor](https://angular.io/api/forms/DefaultValueAccessor) for form controls, to write a value and listen to changes on input elements. + * Provide [NG_VALIDATORS](https://angular.io/api/forms/NG_VALIDATORS) - this is an [InjectionToken](https://angular.io/api/core/InjectionToken) for registering additional synchronous validators used with forms. ```typescript // in presenter class @Component({ @@ -263,160 +244,24 @@ export class FormsPocPresComponent implements OnInit, Validator, FormsPocPresCon * Return the errors for the validators applied global to the form plus the errors for each field */ public validate(_control: AbstractControl): ValidationErrors | null { - ... // ----> See ./FORM_ERRORS.md for the implementation of this method + ... // ----> See ./FORM_ERRORS.md for the implementation of this function } } ``` -* __Submit and Intercommunication__ - -In Otter context we have to handle specific cases for form submit and communication between __presenter/container/page__ -For the submit action we have to support 2 cases: -* __submit from page__ (app level) - there is no submit button in the presenter and the submit action is triggered at application level - * The __page__ triggers submit action > __Container__ receives the signal and executes the submit logic. Emits an event when the submit logic is finished. - - This is useful when you have multiple forms on a page and you want to trigger the submit for all in the same time. -* __submit from presenter__ - the submit button is displayed - * __Presenter__ - click on submit btn and emits an event > __Container__ receives the signal and executes the submit logic. Emits an event when the submit logic is finished. - -This section is explained in details in [FORM_SUBMIT&INTERCOMMUNICATION](./FORM_SUBMIT_AND_INTERCOMMUNICATION.md) section. - - -##### 2. Include Basic validation -The validations on the form are improving overall data quality by validating user input for accuracy and completeness. -We are keeping the concept of validators from Angular forms. Please see [FormValidation](https://angular.io/guide/form-validation) and [Validators](https://angular.io/api/forms/Validators) in Angular for more details. - -In Otter context we call the __basic or primitive__, the validators which are using primitive values (string, number, booleans) as inputs for the validation function. - -These validators are defined and applied at presenter level. They can be set at form creation or later, depending on the use cases. -Validators values are given as a configuration on the presenter. This gave us the possibility to use the presenter with different set of validators. - - -###### Validators definition -```typescript -export interface FormsPocPresConfig extends Configuration { - ... - /** If true requires the control have a non-empty value */ - firstNameRequired: boolean; - - /** Requires the length of the control's value to be less than or equal to the provided number. */ - firstNameMaxLength?: number; - ... - -export const FORMS_POC_PRES_DEFAULT_CONFIG: FormsPocPresConfig = { - ..., - firstNameRequired: true, - firstNameMaxLength: 5, - ... -}; -``` - - -###### Apply validators - * __on presenter html__ -In the use case where we need to display inline errors, we have to apply directives corresponding to the validators on the html template (when it is possible), because Angular material needs the directives for the display of inline errors -```html - - -``` - * __on presenter class__ -```typescript - this.subscriptions.push( - this.config$.subscribe((config) => { - const firstNameValidators = []; - if (config.firstNameMaxLength) { - // Apply validator based on config - firstNameValidators.push(Validators.maxLength(this.config.firstNameMaxLength)); - } - // firstNameValidators.push(otherValidators) - if (firstNameValidators.length) { - this.travelerForm.controls.firstName.clearValidators(); - this.travelerForm.controls.firstName.setValidators(firstNameValidators) - } - }) - ); -``` - - -###### Validators translations -For each defined validator we need a corresponding translation key for the error message. These keys have to be defined in the corresponding __localization.json__ file of the __presenter__. In this way the presenter is aware about its own validations/error messages. - -See [FORM_VALIDATIONS](./FORM_VALIDATION.md) for more details. - - -##### 3. Include Custom Validations -Since the built-in validators won't always match the exact use case of your application, sometimes you'll want to create a custom validator. See [Custom Validators](https://angular.io/guide/form-validation#custom-validators) in angular. -Our custom validators are usually related to the business logic or, they are applied to multiple fields/form controls. -As they are related to the business logic we will create them in the __container__ and pass them to the presenter via an input. The presenter is the one which applies them on the form. - - -###### Validators definition -The validation function can be defined anywhere, but it has to be added to the validators object in the container. -* Validation function -```typescript -/** Validator which checks that the firstname or lastname are not equal with the parameter 'valueToTest' */ -export function formsPocValidatorGlobal(valueToTest: string, translationKey: string, longTranslationKey?: string, translationParams?: any): CustomValidationFn { - return (control: AbstractControl): CustomErrors | null => { - const value: Traveler = control.value; - if (!value || !value.firstName) { - return null; - } - if (value.firstName !== valueToTest && value.lastName !== valueToTest) { - return null; - } else { - return {customErrors: [{translationKey, longTranslationKey, translationParams}]}; // ---> See more about the returned error model in ./FORM_ERRORS.md - } - }; -} -``` -* Container -```typescript -... -ngOnInit() { - this.validators = { // See more about validators type in ./FORM_VALIDATION.md - global: formsPocValidatorGlobal(this.config.forbiddenName, translations.globalForbiddenName, `${translations.globalForbiddenName}.long`, {name: 'Test'}), - fields: {dateOfBirth: dateCustomValidator(translations.dateInThePast) } - }; -... - getFormsPocPresContext(overrideContext: Partial): TemplateContext { - return { - ... - inputs: { - validators: this.validators // ---> the validators sent to be applied on the presenter; - }, - ... - }; - } -``` - - -###### __Apply__ validators: -The validators are applied to the form on the __presenter__ class. -```typescript -/** Custom validators applied on the form */ - @Input() customValidators?: CustomFormValidation; - - ngOnInit() { - ... - const firstNameValidators = []; // Validators for the firstName - if (this.config.firstNameMaxLength) { // Primivite validator - // Apply validator based on config - firstNameValidators.push(Validators.maxLength(this.config.firstNameMaxLength)); - } - // Apply custom validation - if (this.customValidators && this.customValidators.fields && this.customValidators.fields.firstName) { - firstNameValidators.push(this.customValidators.fields.firstName); - } - this.travelerForm.controls.firstName.setValidators(firstNameValidators); - } -``` +__Submit and Intercommunication:__ - -###### Validators translations -For each custom validator we need a corresponding translation key for the error message. -As they are defined in the container, the keys have to be defined in the corresponding __localization.json__ file of the __container__. -In this way the container knows about its own validations/error messages. +In the Otter context, we have to handle specific cases for form submission and communication between __presenter/container/page__. +For the submit action, we have to support two cases: +* __Submit from page__ (app level) - there is no submit button in the presenter and the submit action is triggered at application level. + * The __page__ triggers the submit action. The __container__ receives the signal, executes the submit logic, and emits an event when the submit logic is finished. -See [FORM_VALIDATIONS](./FORM_VALIDATION.md) for more details. + This is useful when you have multiple forms on a page and you want to trigger the submit for all in the same time. +* __Submit from presenter__ - the submit button is displayed. + * The submit button is clicked and the __presenter__ emits an event. The __container__ receives the signal, executes the submit logic, and emits an event when the submit logic is finished. +This section is explained in details in the [Otter form submit and intercommunication documentation](./FORM_SUBMIT_AND_INTERCOMMUNICATION.md). +#### 2. Include validation +You can create basic or custom validators in your application, depending on the use cases. +You can find details on this in the [Otter form validation documentation](./FORM_VALIDATION.md). diff --git a/docs/forms/FORM_SUBMIT_AND_INTERCOMMUNICATION.md b/docs/forms/FORM_SUBMIT_AND_INTERCOMMUNICATION.md index 60031dc87a..3b8df2f188 100644 --- a/docs/forms/FORM_SUBMIT_AND_INTERCOMMUNICATION.md +++ b/docs/forms/FORM_SUBMIT_AND_INTERCOMMUNICATION.md @@ -1,37 +1,28 @@ -[Forms Submit and Intercommunication](#form-submit) - 1. [Container presenter context](#container-presenter) - 2. [Form submit](#form-submit) - 1. [Submit from page](#page-submit) - 2. [Submit from presenter](#presenter-submit) - 1. [Handle inline errors at submit](#handle-inline-error-submit) - - # Forms Submit and Intercommunication - -### Container presenter communication +## Container/presenter communication -Having the __presenter__ implementing [ControlValueAccessor](https://angular.io/api/forms/ControlValueAccessor), it will __propagate__ all the __value/status changes__ done inside the __presenter form object__ to the parent, in our case the container. -In this way it will behave as an __HTML input element__ on which we can __bind__ a [FormControl](https://angular.io/api/forms/FormControl#description). -Also, the presenter is implementing [Validator](https://angular.io/api/forms/Validator) interface, if your form validators are only synchronous or [AsyncValidator](https://angular.io/api/forms/AsyncValidator) interface if the form needs asynchronous validators. See [FORM_VALIDATION](./FORM_VALIDATION.md) for more details about validation in Otter. -Implementing this interface gives us the possibility to define, in the __validate__ method, the error object model which will be __propagated__ to the parent/container. See [FORM_ERRORS](./FORM_ERRORS.md) for details. +Since the __presenter__ implements [ControlValueAccessor](https://angular.io/api/forms/ControlValueAccessor), it will __propagate__ all the __value/status changes__ done inside the __presenter form object__ to the parent (the container in our case). +In this way, it will behave as an __input HTML element__ on which we can __bind__ a [FormControl](https://angular.io/api/forms/FormControl#description). +Also, the presenter implements the [Validator](https://angular.io/api/forms/Validator) interface if your form validators are only synchronous or the [AsyncValidator](https://angular.io/api/forms/AsyncValidator) interface if the form needs asynchronous validators. +See [Form Validation](./FORM_VALIDATION.md) for more details about validation in Otter. +Implementing this interface gives us the possibility to define, in the `validate` function, the error object model which will be __propagated__ to the parent/container. See [Form Errors](./FORM_ERRORS.md) for details. -The container will apply the [Form Control Directive](https://angular.io/api/forms/FormControlDirective) to the presenter html tag in order to: - * __set the default value__ for the presenter form object. - * __listen to the valueChanges__ - * __listen status changes__ - * easily __get the errors propagated__ by the presenter +The container will apply the [Form Control Directive](https://angular.io/api/forms/FormControlDirective) to the presenter HTML tag in order to: + * __Set the default value__ for the presenter form object. + * __Listen to the value changes__. + * __Listen to the status changes__. + * Easily __get the errors propagated__ by the presenter. -See [FORM_STRUCTURE](./FORM_STRUCTURE.md) for more details. +See the [Otter form structure documentation](./FORM_STRUCTURE.md) for more details. - -### Form submit +## Form submit -For the forms submit actions we have to support 2 cases: -* submit __from the component__ - the submit button is displayed in the presenter -* submit __from the page__ (app level) - the button is hidden in the presenter and the submit action is triggered at application level +For the forms submit actions, we have to support two cases: +* Submit __from the component__: The submit button is displayed in the presenter. +* Submit __from the page__ (application level): The button is hidden in the presenter and the submit action is triggered at application level. -The display of the submit button should be configurable in the presenter. A config property has to be provided in the presenter configuration. +The display of the submit button should be configurable in the presenter. A property has to be provided in the presenter configuration. ```typescript export interface FormsPocPresConfig extends Configuration { /** Configuration to show/hide the submit button */ @@ -39,219 +30,218 @@ export interface FormsPocPresConfig extends Configuration { ... } ``` -In both cases the submit logic is handled in the container. -When submit is triggered either by the presenter or the page, it is only notifying the container that a submit action was fired. The event is captured in the container and it is calling the execution of submit logic. -The container will handle business logic at submit and when it has finished, it will emit an event (__submitted__) with a boolean value (`true` if the submit is considered successful, `false` otherwise) which can be intercepted at page level. +In both cases, the submit logic is handled in the container. +When submit is triggered either by the presenter or the page, it is only notifying the container that a submit action was fired. The event is captured in the container, and it is calling the execution of submit logic. +The container will handle the business logic and when it has finished, it will emit an event (`submitted`) with a boolean value (`true` if the submit is considered successful, `false` otherwise) which can be intercepted at page level. - -#### Submit from page -In this case the submit button should be hidden in the presenter, so the submit will be triggered from page/parent component. +### Submit from page +In this case, the submit button is hidden in the presenter so the submit will be triggered from the page/parent component. We propose a way of notifying the container that a submission has been triggered from the page. -* __Passing an observable as an input to the container__ - * Page component template - -The _submitTrigger$_ observable is passed as input to the container. - ```typescript - - - - ``` - * In the page component we emit a new event each time we click on _Next_ button. We want that this, to trigger a submit on the form. - ```typescript - ... - submitTheForm$: Subject = new Subject(); +__Passing an observable as an input to the container__ +* In the page component template, the `submitTrigger$` observable is passed as input to the container. +```html + + + +``` + +* In the page component, we emit a new event each time we click on the `Next` button (which triggers a submit on the form). +```typescript +submitTheForm$: Subject = new Subject(); + +goNext() { + this.submitTheForm$.next(true); +} + +onFormSubmitted(value: boolean) { + console.log('Form submitted result:', value); +} +``` + +* In the container, we receive the observable as an input, and we execute the submit logic each time the observable emits. +Note that we have put in place an [@AsyncInput](https://github.com/AmadeusITGroup/otter/blob/main/packages/%40o3r/forms/src/annotations/async-input.ts) decorator in __@o3r/forms__ to make sure that we will not have unhandled subscriptions if the reference of the input observable changes. +```typescript +import { AsyncInput } from '@o3r/forms'; +... + +export class FormsPocContComponent implements OnInit, OnChanges, OnDestroy, Configurable, FormsPocContContext { + + /** Observable used to notify the component that a submit has been fired from the page */ + @Input() + @AsyncInput() + public submitTrigger$: Observable; + + /** + * Emit an event when the submit has been fired from the component (block) + */ + @Output() onSubmitForm: EventEmitter = new EventEmitter(); + + /** + * Emit an event at the end of the submit executed logic + */ + @Output() onSubmitted: EventEmitter = new EventEmitter(); + ... - goNext() { - this.submitTheForm$.next(true); + + ngOnInit() { ... + this.formsPocPresContext$.next(this.getFormsPocPresContext({})); + + if (this.submitTrigger$) { + this.subscriptions.push( + this.submitTrigger$.subscribe((_value) => { + this.submitAction(); + }) + ); + } } - onFormSubmitted(value: boolean) { - console.log('Form submitted result:', value); - ... + + submitAction() { + // this contains the logic executed at submit + ... + // Emit an event at the end of the submit logic execution + const isValid = true; // means that the submit logic is successful + this.onSubmitted.emit(isValid); } - ``` - * In the container we receive the observable as an input, and each time the observable emits we execute the submit logic. - Note that we have put in place an [@AsyncInput](https://github.com/AmadeusITGroup/otter/blob/main/packages/%40o3r/forms/src/annotations/async-input.ts) decorator in __@o3r/forms__ to make sure that we will not have unhandled subscriptions if the reference of the input observable changes. - ```typescript - ... - import { AsyncInput ...} from '@o3r/forms'; - ... - /** Observable used to notify the component that a submit has been fired from the page */ - @Input() - @AsyncInput() - public submitTrigger$: Observable; - - /** - * Emit an event when the submit has been fired from the component (block) - */ - @Output() onSubmitForm: EventEmitter = new EventEmitter(); - - /** - * Emit an event at the end of the submit executed logic - */ - @Output() onSubmitted: EventEmitter = new EventEmitter(); - - ... - ngOnInit() { - this.formsPocPresContext$.next(this.getFormsPocPresContext({})); - - if (this.submitTrigger$) { - this.subscriptions.push( - this.submitTrigger$.subscribe((_value) => { - this.submitAction(); - }) - ); - } - } - - submitAction() { - // this contains the logic executed at submit - ... - // Emit an event at the end of the submit logic execution - const isValid = true; // means that the submit logic is successful - this.onSubmitted.emit(isValid); - } - ``` +} +``` - -#### Submit from presenter -An event will be emitted when the submit of the form is fired (click on submit button, ENTER key ...), notifying the container about this. No logic is done at presenter level. +### Submit from presenter +An event will be emitted when the submit of the form is fired (click on submit button, ENTER key, etc.), notifying the container about this. No logic is done at presenter level. As in the page submit, the submit logic will be handled inside the container. -In the following example we are using the same function to execute the logic as in the page submit. +In the following example, we are using the same function to execute the logic as in the page submit. -* Container component +* Container component: ```typescript - ... - /** The form control object bind to the presenter */ - mainFormControl: FormControl; - ... - constructor(private store: Store, public changeDetector: ChangeDetectorRef) { - this.translations = translations; - this.traveler = {firstName: '', lastName: 'TestUser', dateOfBirth: new utils.Date()}; - this.mainFormControl = new FormControl(this.traveler); - } - ... +/** The form control object bind to the presenter */ +mainFormControl: FormControl; + +constructor(private store: Store, public changeDetector: ChangeDetectorRef) { + this.translations = translations; + this.traveler = {firstName: '', lastName: 'TestUser', dateOfBirth: new utils.Date()}; + this.mainFormControl = new FormControl(this.traveler); +} + /** Submit event received from the presenter */ - onSubmit() { - this.onSubmitForm.emit(); - // Check that there is no submit from the page/parent component - if (!this.submitMe$) { // In this case we do not want to execute the submit logic, as it will be done when we submit from the page - this.submitAction(); - } +onSubmit() { + this.onSubmitForm.emit(); + // Check that there is no submit from the page/parent component + if (!this.submitTrigger$) { // In this case we do not want to execute the submit logic, as it will be done when we submit from the page + this.submitAction(); } +} - /** submit function */ - submitAction() { - // When submitting from page, call the function to mark the form in the presenter as dirty and touched - if (this.submitTrigger$) { // ---> this will be explained below - this._markInteraction(); - } - const isValid = !this.mainFormControl.errors; - if (!this.mainFormControl.errors) { - // put your submit logic here - } else { - const errors: FormError = { - formId: `${this.id}-my-form-example`, - errors: Object.keys(this.mainFormControl.errors).map((controlName: string) => { - const controlErrors = this.mainFormControl.errors![controlName]; - return {htmlElementId: controlErrors.htmlElementId, errorMessages: controlErrors.errorMessages}; - }) - }; - this.store.dispatch(new UpsertFormErrorMessagesEntities({entities: [errors]})); - } - this.onSubmitted.emit(isValid); +/** submit function */ +submitAction() { + // When submitting from page, call the function to mark the form in the presenter as dirty and touched + if (this.submitTrigger$) { // ---> this will be explained below + this._markInteraction(); } + const isValid = !this.mainFormControl.errors; + if (!this.mainFormControl.errors) { + // put your submit logic here + } else { + const errors: FormError = { + formId: `${this.id}-my-form-example`, + errors: Object.keys(this.mainFormControl.errors).map((controlName: string) => { + const controlErrors = this.mainFormControl.errors![controlName]; + return {htmlElementId: controlErrors.htmlElementId, errorMessages: controlErrors.errorMessages}; + }) + }; + this.store.dispatch(upsertFormErrorMessagesEntities({entities: [errors]})); + } + this.onSubmitted.emit(isValid); +} ``` - ### Handle inline errors at submit, before interacting with the form -At the first display of the form there is no inline error shown. If there is no interaction with the form and submit is triggered, all invalid fields should display inline errors. -For this we have to mark the controls as touched and dirty before doing the submission. -If the submit button is in the presenter, we mark the controls as dirty and touched before doing the submission. +When the form is first displayed, no inline errors are shown. If there is no interaction with the form and the submit is triggered, all invalid fields should display inline errors. +To avoid this, we have to mark the controls as touched and dirty before doing the submission. We also do this if the submit button is in the presenter. + +We need to __register a function__ to be called __to mark the controls__ from the presenter as __dirty and touched__. So we emit an event with the callback function at the initialization +of the presenter component after we have created the form object (`travelerForm` here). This function will be called in the container before executing the submit logic. + +When the submit from the page is done, we execute the `submitAction` function in the container, and we have no access to the controls in the presenter. -When the submit is done from the page we execute the submitAction in the container, and we have no access to the controls in the presenter. -We need to __register a function__ to be called __to mark the controls__ from the presenter as __dirty and touched__. So we emit an event with the callback function at the initialization of the presenter component after we have the form object (travelerForm here) created. This function will be called in the container before executing the submit logic. -* Presenter component +* __Presenter component__: ```typescript +/** Register a function to be called to mark the controls as touched and dirty */ +@Output() registerInteraction: EventEmitter<() => void> = new EventEmitter<() => void>(); + +ngOnInit() { ... - /** Register a function to be called to mark the controls as touched and dirty */ - @Output() registerInteraction: EventEmitter<() => void> = new EventEmitter<() => void>(); - ... - ngOnInit() { - ... - this.registerInteraction.emit(() => { - markAllDirtyAndTouched(this.travelerForm); - this.changeDetector.markForCheck(); - }); - } + this.registerInteraction.emit(() => { + markAllDirtyAndTouched(this.travelerForm); + this.changeDetector.markForCheck(); + }); +} ``` -We have provided a helper called [markAllControlsDirtyAndTouched](https://github.com/AmadeusITGroup/otter/blob/main/packages/@o3r/forms/src/core/helpers.ts) in __@o3r/forms__ to mark the interaction with the form. +We have provided a helper called `markAllControlsDirtyAndTouched` in [__@o3r/forms__](https://github.com/AmadeusITGroup/otter/blob/main/packages/@o3r/forms/src/core/helpers.ts) to mark the interaction with the form. -* Container component +* __Container component__: ```typescript - ... - /** The form control object bind to the presenter */ - mainFormControl: FormControl; - - /** This will store the function to make the child form as dirty and touched */ - _markInteraction: () => void; - ... - constructor(private store: Store, public changeDetector: ChangeDetectorRef) { - this.translations = translations; - this.traveler = {firstName: '', lastName: 'TestUser', dateOfBirth: new utils.Date()}; - this.mainFormControl = new FormControl(this.traveler); - } - ... +/** The form control object bind to the presenter */ +mainFormControl: FormControl; + +/** This will store the function to make the child form as dirty and touched */ +_markInteraction: () => void; + +constructor(private store: Store, public changeDetector: ChangeDetectorRef) { + this.translations = translations; + this.traveler = {firstName: '', lastName: 'TestUser', dateOfBirth: new utils.Date()}; + this.mainFormControl = new FormControl(this.traveler); +} + /** Submit event received from the presenter */ - onSubmit() { - this.onSubmitForm.emit(); - // Check that there is no submit from the page/parent component - if (!this.submitMe$) { // In this case we do not want to execute the submit logic, as it will be done when we submit from the page - this.submitAction(); - } +onSubmit() { + this.onSubmitForm.emit(); + // Check that there is no submit from the page/parent component + if (!this.submitMe$) { // In this case we do not want to execute the submit logic, as it will be done when we submit from the page + this.submitAction(); } +} - /** submit function */ - submitAction() { - // When submitting from page, call the function to mark the form in the presenter as dirty and touched - // It is not necessary to be called each time we submit. It is important to be called if the form is pristine - if (this.submitTrigger$) { - this._markInteraction(); - } - const isValid = !this.mainFormControl.errors; - if (!this.mainFormControl.errors) { - // put your submit logic here - } else { - const errors: FormError = { - formId: `${this.id}-my-form-example`, - errors: Object.keys(this.mainFormControl.errors).map((controlName: string) => { - const controlErrors = this.mainFormControl.errors![controlName]; - return {htmlElementId: controlErrors.htmlElementId, errorMessages: controlErrors.errorMessages}; - }) - }; - this.store.dispatch(new UpsertFormErrorMessagesEntities({entities: [errors]})); - } - this.onSubmitted.emit(isValid); - } - - /** Register the function to be called to mark the presenter as touched and dirty */ - registerInteraction(fn: () => void) { - this._markInteraction = fn; +/** submit function */ +submitAction() { + // When submitting from page, call the function to mark the form in the presenter as dirty and touched + // It is not necessary to be called each time we submit. It is important to be called if the form is pristine + if (this.submitTrigger$) { + this._markInteraction(); } - - getFormsPocPresContext(overrideContext: Partial): TemplateContext { - return { - ... - outputs: { - onSubmit: this.onSubmit.bind(this), - registerInteraction: this.registerInteraction.bind(this) // ---> save the output function handler in the component context - }, - parentId: this.id, - formControl: this.mainFormControl + const isValid = !this.mainFormControl.errors; + if (!this.mainFormControl.errors) { + // put your submit logic here + } else { + const errors: FormError = { + formId: `${this.id}-my-form-example`, + errors: Object.keys(this.mainFormControl.errors).map((controlName: string) => { + const controlErrors = this.mainFormControl.errors![controlName]; + return {htmlElementId: controlErrors.htmlElementId, errorMessages: controlErrors.errorMessages}; + }) }; + this.store.dispatch(new UpsertFormErrorMessagesEntities({entities: [errors]})); } + this.onSubmitted.emit(isValid); +} + +/** Register the function to be called to mark the presenter as touched and dirty */ +registerInteraction(fn: () => void) { + this._markInteraction = fn; +} + +getFormsPocPresContext(overrideContext: Partial): TemplateContext { + return { + ... + outputs: { + onSubmit: this.onSubmit.bind(this), + registerInteraction: this.registerInteraction.bind(this) // ---> save the output function handler in the component context + }, + parentId: this.id, + formControl: this.mainFormControl + }; +} ``` diff --git a/docs/forms/FORM_VALIDATION.md b/docs/forms/FORM_VALIDATION.md index 737cde413d..dc4f772bb5 100644 --- a/docs/forms/FORM_VALIDATION.md +++ b/docs/forms/FORM_VALIDATION.md @@ -1,58 +1,35 @@ -[Form validators](#form-validators) - -- [Form validators](#form-validators) - - [Sync validators](#sync-validators) - - [Container/presenter context](#containerpresenter-context) - - [Basic validators](#basic-validators) - - [Validators definition](#validators-definition) - - [Apply validators](#apply-validators) - - [Validators translations](#validators-translations) - - [Custom Validators](#custom-validators) - - [Validators definition](#validators-definition-1) - - [Apply validators](#apply-validators-1) - - [Validators translations](#validators-translations-1) - - [Custom validation contracts available in @o3r/forms](#custom-validation-contracts-available-in-o3rforms) - - [Async Validators](#async-validators) - - - # Form validators -The validations on the form are improving overall data quality by validating user input for accuracy and completeness. -We are using the base concepts from Angular for the [form validation](https://angular.io/guide/form-validation), having default validators ( required, maxLength ...) but also custom validators (see Custom Validators in [form validation angular](https://angular.io/guide/form-validation)). - - +Form validations improve overall data quality by validating user input for accuracy and completeness. +We are using the core concepts from Angular for the [form validation](https://angular.io/guide/form-validation), with default validators (required, maxLength, etc.) +but also custom validators (see Custom Validators in [Angular form validation](https://angular.io/guide/form-validation)). ## Sync validators - - ### Container/presenter context -In container/presenter context we have to decide where to create and how to apply the validators. -Having this situation we decided to split the validation in 2 parts: - -- __custom validators__ - the ones related to the business logic or applied to multiple form controls. As they are related to the business logic they have to be declared at container level. -- __primitive validators__ - simple configurable validators which will be declared at presenter level +In the container/presenter context, we have to decide where to create and how to apply the validators. +In this situation, we decided to split the validation into two parts: -The presenter will implement [Validator](https://angular.io/api/forms/NG_VALIDATORS) interface, meaning that we will have to implement __the validate__ method which help us in defining the error object structure. -(See [Form errors](./FORM_ERRORS.md) create error object section) -Each time the validate method is called the returned object is propagated to the parent (container in our case). If the object returned is _null_ the form status _VALID_. If the return is an object the form status is _INVALID_. +- __Custom validators__: The ones related to the business logic or applied to multiple form controls. As they are related to the business logic, they have to be declared at container level. +- __Primitive validators__: Simple and configurable validators which will be declared at presenter level. - +The presenter will implement the [Validator](https://angular.io/api/forms/NG_VALIDATORS) interface, meaning that we will have to implement the `validate` function which will help us in defining the error object structure. +(See the create error object section in [Form Errors](./FORM_ERRORS.md)). +Each time the `validate` function is called, the returned object is propagated to the parent (the container in our case). If the returned object is `null`, the form status is `VALID`. Otherwise, the form status is `INVALID`. -#### Basic validators +### Basic validators We are keeping the concept of validators from Angular forms. Please see [FormValidation](https://angular.io/guide/form-validation) and [Validators](https://angular.io/api/forms/Validators) in Angular for more details. -In Otter context we call the __basic or primitive__, the validators which are using primitive values (string, number, booleans) as inputs for the validation function. +In the Otter context, we call validators __basic or primitive__ if they are using primitive values (string, number, boolean) as inputs for the validation function. These validators are defined and applied at presenter level. They can be set at form creation or later, depending on the use cases. -Validators values are given as a configuration on the presenter. This gave us the possibility to use the presenter with different set of validators. +Validator values are given as a configuration in the presenter. This gives us the possibility of using the presenter with different sets of validators. - +#### Define basic validators -##### Validators definition +Below is an example of validator values that are defined in the configuration of the presenter: ```typescript export interface FormsPocPresConfig extends Configuration { @@ -62,71 +39,68 @@ export interface FormsPocPresConfig extends Configuration { /** Requires the length of the control's value to be less than or equal to the provided number. */ firstNameMaxLength?: number; - ... +} export const FORMS_POC_PRES_DEFAULT_CONFIG: FormsPocPresConfig = { ..., firstNameRequired: true, - firstNameMaxLength: 5, - ... -}; + firstNameMaxLength: 5 +} ``` - +#### Apply basic validators -##### Apply validators +The validation can be applied in the HTML template, it can be given at form creation, or it can be set later in the presenter. This depends on the use cases. -The validation can be applied on the html template, it can be given at form creation or set later in the presenter. This depends on the use cases. - -- __on presenter html__ -In the use case where we need to display inline errors, we have to apply directives corresponding to the validators on the html template (when it is possible), because Angular material needs the directives for the display of inline errors +* __Presenter HTML__: +In the use case where we need to display inline errors, we have to apply directives corresponding to the validators in the HTML template (when it is possible), +because Angular material needs the directives for the display of inline errors. ```html - - + + ``` -- __on presenter class__ +* __Presenter class__: The validators are applied to the form in the __presenter__ class. ```typescript - this.subscriptions.push( - this.config$.subscribe((config) => { - const firstNameValidators = []; - if (config.firstNameMaxLength) { - // Apply validator based on config - firstNameValidators.push(Validators.maxLength(this.config.firstNameMaxLength)); - } - // firstNameValidators.push(otherValidators) - if (firstNameValidators.length) { - this.travelerForm.controls.firstName.clearValidators(); - this.travelerForm.controls.firstName.setValidators(firstNameValidators) - } - }) - ); +this.subscriptions.push( + this.config$.subscribe((config) => { + const firstNameValidators = []; + if (config.firstNameMaxLength) { + // Apply validator based on config + firstNameValidators.push(Validators.maxLength(this.config.firstNameMaxLength)); + } + // firstNameValidators.push(otherValidators) + if (firstNameValidators.length) { + this.travelerForm.controls.firstName.clearValidators(); + this.travelerForm.controls.firstName.setValidators(firstNameValidators) + } + }) +); ``` - - -##### Validators translations +#### Basic validators translations -For each defined validator we need a corresponding translation key for the error message. These keys have to be defined in the corresponding __localization.json__ file of the __presenter__. In this way the presenter is aware about its own validations/error messages. -See [FORM_ERRORS](./FORM_ERRORS.md) _Errors translation_ section for more details. +For each defined validator, we need a corresponding translation key for the error message. +These keys have to be defined in the corresponding `localization.json` file of the __presenter__. +This way the presenter is aware about its own validations/error messages. - +See the _Errors translation_ section in [Form Errors](./FORM_ERRORS.md) for more details. -#### Custom Validators +### Custom Validators -Since the built-in validators won't always match the exact use case of your application, sometimes you'll want to create a custom validator. See [Custom Validators](https://angular.io/guide/form-validation#custom-validators) in angular. -Our custom validators are usually related to the business logic or, they are applied to multiple fields/form controls. -As they are related to the business logic we will create them in the __container__ and pass them to the presenter via an input. The presenter is the one which applies them on the form. +Since the built-in validators won't always match the exact use case of your application, sometimes you'll want to create a custom validator. +(See [Custom Validators](https://angular.io/guide/form-validation#custom-validators) in Angular). - +Our custom validators are usually related to the business logic, or they are applied to multiple fields/form controls. +Since they are related to the business logic, we will create them in the __container__ and pass them to the presenter via an input. The presenter is the one that applies them to the form. -##### Validators definition +#### Define custom validators The validation function can be defined anywhere, but it has to be added to the validators object in the container. -- Validation function +* __Validation function__: ```typescript /** Validator which checks that the firstname or lastname are not equal with the parameter 'valueToTest' */ @@ -145,13 +119,12 @@ export function formsPocValidatorGlobal(valueToTest: string, translationKey: str } ``` -The object returned by the custom validator will be of type __ErrorMessageObject__ compatible with the form error store. (See [Form Errors](./FORM_ERRORS.md)) -The key _customErrors_ it is used to identify the custom errors in the errors returned by a form control; +The object returned by the custom validator will be of type `ErrorMessageObject` compatible with the form error store. (See [Form Errors](./FORM_ERRORS.md)). +The key `customErrors` is used to identify the custom errors in the errors returned by a form control. -- Container +* __Container__: ```typescript -// ... /** Form validators */ validators: CustomFormValidation; // ... @@ -164,18 +137,18 @@ ngOnInit() { }; // ... getFormsPocPresContext(overrideContext: Partial): TemplateContext { - return { - // ... - inputs: { - validators: this.validators // ---> the validators sent to be applied on the presenter; - }, - // ... - }; + return { + inputs: { + validators: this.validators // ---> the validators sent to be applied on the presenter; + }, + // ... + }; + } } ``` -[__CustomFormValidation__](https://github.com/AmadeusITGroup/otter/blob/main/packages/@o3r/forms/src/core/custom-validation.ts) is containing two entries, one for global (root) form validation and one for the other fields. -_Fields_ entry is receiving the form contract as generic type. +[__CustomFormValidation__](https://github.com/AmadeusITGroup/otter/blob/main/packages/@o3r/forms/src/core/custom-validation.ts) contains two entries: one for global (root) form validation and one for the other fields. +The `fields` entry is receiving the form contract as generic type. ```typescript /** Custom validation for the form */ @@ -187,59 +160,55 @@ export interface CustomFormValidation { } ``` - - -##### Apply validators +#### Apply custom validators -The validators are applied to the form on the __presenter__ class. +The validators are applied to the form in the __presenter__ class. ```typescript - /** Custom validators applied on the form */ - @Input() customValidators?: CustomFormValidation; // ---> receives the Traveler contract - private customValidators$ = new BehaviorSubject(undefined); - - ngOnInit() { - ... - this.subscriptions.push( - combineLatest([this.config$, customValidators$]).subscribe(([config, customValidators]) => { - const firstNameValidators = []; - if (config.firstNameMaxLength) { // Primivite validator - // Apply validator based on config - firstNameValidators.push(Validators.maxLength(this.config.firstNameMaxLength)); - } - // Apply custom validation - if (customValidators && customValidators.fields && customValidators.fields.firstName) { - firstNameValidators.push(customValidators.fields.firstName); - } - // firstNameValidators.push(otherValidators) - if (firstNameValidators.length) { - this.travelerForm.controls.firstName.clearValidators(); - this.travelerForm.controls.firstName.setValidators(firstNameValidators) - } - }) - ); - } +/** Custom validators applied on the form */ +@Input() customValidators?: CustomFormValidation; // ---> receives the Traveler contract +private customValidators$ = new BehaviorSubject(undefined); - ngOnChanges(changes: SimpleChanges) { - if (changes.customValidators) { - this.customValidators$.next(this.customValidators); - } +ngOnInit() { + ... + this.subscriptions.push( + combineLatest([this.config$, customValidators$]).subscribe(([config, customValidators]) => { + const firstNameValidators = []; + if (config.firstNameMaxLength) { // Primivite validator + // Apply validator based on config + firstNameValidators.push(Validators.maxLength(this.config.firstNameMaxLength)); + } + // Apply custom validation + if (customValidators && customValidators.fields && customValidators.fields.firstName) { + firstNameValidators.push(customValidators.fields.firstName); + } + // firstNameValidators.push(otherValidators) + if (firstNameValidators.length) { + this.travelerForm.controls.firstName.clearValidators(); + this.travelerForm.controls.firstName.setValidators(firstNameValidators) + } + }) + ); +} + +ngOnChanges(changes: SimpleChanges) { + if (changes.customValidators) { + this.customValidators$.next(this.customValidators); } +} ``` - - -##### Validators translations +#### Custom validators translations -For each custom validator we need a corresponding translation key for the error message. -As they are defined in the container, the keys have to be defined in the corresponding __localization.json__ file of the __container__. -In this way the container knows about its own validations/error messages. See [FORM_ERRORS](./FORM_ERRORS.md) _Errors translation_ section for more details. +For each custom validator, we need a corresponding translation key for the error message. +Since they are defined in the container, the keys have to be defined in the corresponding `localization.json` file of the __container__. +This way the container knows about its own validations/error messages. +See the _Errors translation_ section in [Form Errors](./FORM_ERRORS.md) for more details. - +#### Custom validation contracts -##### Custom validation contracts available in @o3r/forms - -We have put in place a set of interfaces which will help us to define the custom validators and to keep the same structure in the framework. +We have put in place a set of interfaces (available in @o3r/forms) that will help us to define the custom validators and to keep the same structure in the framework. +Below are some available interfaces, you can find them all [here](https://github.com/AmadeusITGroup/otter/blob/main/packages/@o3r/forms/src/core/custom-validation.ts). ```typescript /** @@ -265,18 +234,16 @@ export interface CustomFormValidation { } ``` - - ## Async Validators -When you need an asynchronous validator for your form, you have to make sure that the presenter will implement [AsyncValidator](https://angular.io/api/forms/NG_ASYNC_VALIDATORS) interface. -Here you will have also to implement __the validate__ method to define the error object structure. The error object has to be returned in a Promise or in an Observable which has to be completed. -The only difference from sync validators is the returned object of the __validate__ method. +When you need an asynchronous validator for your form, you have to make sure that the presenter will implement the [AsyncValidator](https://angular.io/api/forms/NG_ASYNC_VALIDATORS) interface. +Here you will also have to implement the `validate` function to define the error object structure. The error object has to be returned in a Promise or in an Observable that must be completed. +The only difference from sync validators is the object returned by the `validate` function. Also, you have to provide the [NG_ASYNC_VALIDATORS](https://angular.io/api/forms/NG_ASYNC_VALIDATORS) token for the presenter. -For more details about the implementation have a look at [Async Validation in angular](https://angular.io/guide/form-validation#async-validation). +For more details about the implementation, have a look at [Async Validation in Angular](https://angular.io/guide/form-validation#async-validation). -The example below contains the two mandatory things to do when you need an async validator: provide _NG_ASYNC_VALIDATORS_ token and implement _validate_ method. +The example below contains the two mandatory tasks to do when you need an async validator: provide the `NG_ASYNC_VALIDATORS` token and implement the `validate` function. ```typescript @Component({ @@ -322,5 +289,3 @@ export class FormsPocPresComponent implements OnInit, OnDestroy, Configurable