Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: update forms doc #1694

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open

feat: update forms doc #1694

wants to merge 1 commit into from

Conversation

sdo-1A
Copy link
Contributor

@sdo-1A sdo-1A commented Apr 22, 2024

Proposed change

Update documentation on forms

@sdo-1A sdo-1A requested a review from a team as a code owner April 22, 2024 10:39
Copy link

nx-cloud bot commented Apr 22, 2024

☁️ Nx Cloud Report

CI is running/has finished running commands for commit ddba39a. As they complete they will appear below. Click to see the status, the terminal output, and the build insights.

📂 See all runs for this CI Pipeline Execution


✅ Successfully ran 1 target

Sent with 💌 from NxCloud.

@github-actions github-actions bot added documentation Improvements or additions to documentation enhancement New feature or request project:@o3r/forms labels Apr 22, 2024
@@ -12,7 +12,11 @@ This package is an [Otter Framework Module](https://github.com/AmadeusITGroup/ot
[![Stable Version](https://img.shields.io/npm/v/@o3r/forms?style=for-the-badge)](https://www.npmjs.com/package/@o3r/forms)
[![Bundle Size](https://img.shields.io/bundlephobia/min/@o3r/forms?color=green&style=for-the-badge)](https://www.npmjs.com/package/@o3r/forms)

This module provides utilities to enhance Angular form (asynchronous decorator, additional validator, error store...).
This module provides utilities to enhance the build of Angular reactive forms in the Otter context. These utilities include:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From what I see this entire module is related to guidelines for a product application, with container/presenter structure for components.
I think we should explain this here in the beginning and maybe add another section where to say that for small apps, be-spoke apps (I don;t know how to name them :D) this module does not bring added value on top of angular forms.
Maybe we can have a discussion inside the team to double check if something from this module can be used in any use-case.
Any thoughts @pginoux-1A @kpanot ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with you for all the complexity brings by container / presenter.
But some features could be interesting even with as small apps
for example if you want to handle the submission at page level and display the error messages outside the form component

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, we should probably avoid "in the Otter context", from an outside perspective this is really scary. If I don't know what Otter is, I would imagine a big black box with guidelines from an entity I know nothing about.
Unlike Angular, ware not known enough to use this kind of sentence in a public documentation.
I would replace it with the actual context (described by @matthieu-crouzet and @mrednic-1A ) on where our utilities can be useful.

- __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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this kind of doc be in the tsdoc only ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It depends, on the usage, it is nice to see how to use this model without going through the code. But maybe it could be more interesting to see how to use it instead of writing a tsdoc.


<a name="customerrors"></a>
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`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We anymore generate components with a default id, overridable by input.

Suggested change
For this, the presenter receives an `id` as input and for each field we are concatenating, the `id` with the `formControlName`.
To identify a field, we can generate an `id` in the container instance, provide it through the input mechanism, and concatenate it with the `formControlName`.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we could remove references to the container?

<a name="customerrors"></a>
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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To complement the comment just above

Suggested change
Since the container sets a __unique id__, we are sure to have unique HTML identifiers for the form fields.
Since we will have a __unique id__ by instance of the container, we are sure to have unique HTML identifiers for the form fields.

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.
<a name="translationcustomerror"></a>
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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is this specific places ?
I think the place is the same as all others localization key.
maybe we can remove this sentence.

__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({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we keep an example like that in the doc
or have it in the showcase and add a link to it here

@@ -12,7 +12,11 @@ This package is an [Otter Framework Module](https://github.com/AmadeusITGroup/ot
[![Stable Version](https://img.shields.io/npm/v/@o3r/forms?style=for-the-badge)](https://www.npmjs.com/package/@o3r/forms)
[![Bundle Size](https://img.shields.io/bundlephobia/min/@o3r/forms?color=green&style=for-the-badge)](https://www.npmjs.com/package/@o3r/forms)

This module provides utilities to enhance Angular form (asynchronous decorator, additional validator, error store...).
This module provides utilities to enhance the build of Angular reactive forms in the Otter context. These utilities include:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, we should probably avoid "in the Otter context", from an outside perspective this is really scary. If I don't know what Otter is, I would imagine a big black box with guidelines from an entity I know nothing about.
Unlike Angular, ware not known enough to use this kind of sentence in a public documentation.
I would replace it with the actual context (described by @matthieu-crouzet and @mrednic-1A ) on where our utilities can be useful.

This module provides utilities to enhance the build of Angular reactive forms in the Otter context. These utilities include:
* An asynchronous decorator (`@AsyncInput`) to ensure subscriptions are handled if the references of the input observables change.
* Basic and custom validators to validate user input for accuracy and completeness.
* A dedicated NgRX store for form errors to have the possibility of displaying error messages.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* A dedicated NgRX store for form errors to have the possibility of displaying error messages.
* A dedicated NgRX store for form errors to have the possibility of displaying error messages outside the form component.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this is useful to mention...

# 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.
<a name="store"></a>
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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the way of handling errors in the Otter context different? And is it really true that this is only for an Otter context? This might be a bit scary when we actually just want to share some guidelines and utilities to help developer solve some architectural challenges?

- __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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sure to introduce the ElementError if you remove the code above. If not, please ignore this message :)

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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this is a property from the ElementError interface but this could be stated directly

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we want to leave out the presenter/container structure, maybe we should instead refer to the presenter here as the input element, or the ControlValueAccessor?

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it could be worth to add something about why setting an error in the presenter or in the container.

Copy link
Contributor Author

@sdo-1A sdo-1A Apr 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the form validation file, the difference between the two validators is explained, should I re explain the difference between the two errors here?

@sdo-1A sdo-1A force-pushed the doc/forms branch 3 times, most recently from 4a3fd6e to 33973dd Compare April 30, 2024 14:47
@sdo-1A sdo-1A force-pushed the doc/forms branch 4 times, most recently from 0d41e02 to e1d4224 Compare May 3, 2024 07:40
public translations: FormsPresTranslation;

/** Observable used to notify the component that a submit has been fired from the page */
public submitTrigger$: Observable<boolean>;
Copy link
Contributor

@mrednic-1A mrednic-1A May 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has to be an @input @AsyncInput received from the page component in order to notify this component to trigger a submit. I am assuming here that we have a button on the page component which triggers the submit, not on this component.
With the button on this component we don't need the observables to handle the submit, we call directly submitAction.
But I think we want to showcase the complex example so I would move the button at page level and transform the submitTrigger in an Input.

Copy link
Contributor Author

@sdo-1A sdo-1A May 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The button is in this component, and not on the page.

To explain the structure, I have a page which contains the forms component (this current component), which contains two subcomponents (forms-personal-info and forms-emergency-contact) each having a form. The submit button is in the forms component, but if you prefer I can make the structure more complex and move the button to the page.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the button is in this component, then it would be enough to call 'submitAction' method when clicking on the button, no ? (no need of the observables)

console.log('FORMS COMPONENT: emergency contact form status', this.emergencyContactFormControl.status);
// eslint-disable-next-line no-console
console.log('FORMS COMPONENT: submit logic here', this.personalInfoFormControl.value, this.emergencyContactFormControl.value);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you create an issue to track the errors case with the example of the store usage ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you like me to update the PR and add the store to track the errors? Or would you prefer for me to create an issue?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

another issue would be cleaner imo.

private readonly inPageNavLinkDirectives!: QueryList<InPageNavLink>;
public links$ = this.inPageNavPresService.links$;

constructor(private readonly inPageNavPresService: InPageNavPresService) {}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
constructor(private readonly inPageNavPresService: InPageNavPresService) {}
private readonly inPageNavPresService = inject(InPageNavPresService);

private readonly forbiddenName = 'Test';

constructor() {
this.translations = translations;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need to do these in the constructor - you could directly set the value when declaring the properties

if (isValid) {
this.submittedFormValue = JSON.stringify(this.personalInfoFormControl.value) + '\n' + JSON.stringify(this.emergencyContactFormControl.value);
// eslint-disable-next-line no-console
console.log('FORMS COMPONENT: personal info form status', this.personalInfoFormControl.status);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should probably leave the submitForm logic to each form and just call submitPersonalInfoForm and the other one.
This would highlight that you do not need to duplicate each blocks submit action logic but let them handle it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have prefer two different component to really highlight each component is the owner of its business logic but we can keep it this way if you think that would be overkill

encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class FormsPresComponent {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe you can rename it because it does not fit the Presenter pattern (too much logic)

Comment on lines +81 to +82
constructor(fb: FormBuilder, protected changeDetector: ChangeDetectorRef) {
this.translations = translations;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you could use injectors

> The first subcomponent has a form for the user to fill out their personal information and the second subcomponent has a form for the user to fill out
> information about their emergency contact. In this example, the two subcomponents correspond to input components.
>
> You can find the implementation of this forms example [here](../../apps/showcase/src/components/showcase/forms).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
> You can find the implementation of this forms example [here](../../apps/showcase/src/components/showcase/forms).
> You can find the implementation of this forms example [in the showcase code source](../../apps/showcase/src/components/showcase/forms).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also do we support relative link to the application (the documentation is published on a separated url)

* 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 input component__ because:
* It is up to the input component 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 input component, or in a [FormGroup](https://angular.io/api/forms/FormGroup) containing
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
([FormControl](https://angular.io/api/forms/FormControl)) in one input component, or in a [FormGroup](https://angular.io/api/forms/FormGroup) containing
([FormControl](https://angular.io/api/forms/FormControl)) in one input component or in a [FormGroup](https://angular.io/api/forms/FormGroup) containing

Container/presenter architecture was put in place to ensure the best re-usability/sharing
<a name="form-creation"></a>
### Form creation in container or in presenter?
This documentation will help you with some best practices to use when building Angular reactive forms components that have a parent/input component structure (such as container/presenter).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe mention this is in fact only an implementation of the ControlValueAccessor pattern but we chose to guide them with this example

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(and that we cover as well the errors but I guess this is in another section :) )

* 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
## Parent/input component and reactive forms
A parent/input component (such as [container/presenter](../components/COMPONENT_STRUCTURE.md)) architecture was put in place to ensure the best reusability and sharing of components.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit repetitive with the previous paragraph

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.
<a name="errormodel"></a>
To have the possibility of displaying inline error messages in the form and also in error panels (on the top of the page, above the 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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"The best" is maybe a bit too much ;)

<a name="createerror"></a>
We have to make sure that we provide the `htmlElementId` of the errors in the store that match the __HTML fields__.
To identify a field, we can generate an `id` in the parent component instance, provide it through the input mechanism, and concatenate it with the `formControlName`.
Since we will have a __unique id__ by instance of the parent component, we are sure to have unique HTML identifiers for the form fields.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A link with the showcase code could be nice to include as examples


- one for __custom errors__ - set on the container
- one for __primitive errors__ - computed in the presenter.
- __Custom error__ - set in the parent component
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- __Custom error__ - set in the parent component
- __Custom error__ - defined in the parent component as it holds the business logic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Improvements or additions to documentation enhancement New feature or request project:@o3r/forms project:@o3r/showcase
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants