angular / components

Component infrastructure and Material Design components for Angular
https://material.angular.io
MIT License
24.39k stars 6.76k forks source link

Easier Cross-field-validation for mat-form-field/mat-error combinations #18065

Open DibyodyutiMondal opened 4 years ago

DibyodyutiMondal commented 4 years ago

Feature Description

In reactive forms, cross-field validation requires the validation logic to be placed with the parent form instead of the individual control. The existing feature works fine, however, changing the relevant mat-form-field(s) into an error-state to enable showing underlying mat-error(s) has to be done by calling the relevant FormControl's setErrors() function.

However, with a directive, we need only to perform validation on the parent form and return ValidationErrors. The child controls can themselves detect whether they should go into error-state or not, and the developer need not worry further about it.

First, we run a normal validator function on the form that does nothing but return ValidationErrors. Then, we pass the key(s) of the error(s) for which we want cross-validation, to the custom directive. Please note how the mat-error's *ngIf depends upon the error-set of the FormControl, just like in normal validation, although it would still work if it depended on the error set of the parent FormGroup (which is what we do currently).

 <mat-form-field>
    <mat-label>Range Start</mat-label>
    <input formControlName="start" matInput type="number" [cross-validate]="['invalid']">
    <mat-error *ngIf="with_form.controls['start'].hasError('invalid')">Value must be less than End</mat-error>
  </mat-form-field>

Use Case

Developer convenience and ease of use. Brings more consistency between normal validation and cross-validation. Allows child controls to 'inherit' some user-specified errors of the parent on valueChanges.

StackBlitz

https://stackblitz.com/edit/angular-cross-validation-directive

traviskaufman commented 4 years ago

One thing that I think would be really useful here is converting the errorState property into an @Input() for the Input API. This would allow someone to programmatically display an error state, without the input itself being tightly coupled to validation.

This is useful in the case of things like async validators for form groups, where you want to use multiple values for validation, but then display the validation error itself below a specific field.

DibyodyutiMondal commented 4 years ago

@traviskaufman

Programmatically being able to set errorState would be nice and convenient, yes. However, I think the tighter coupling between validation and the control's status is one of the reasons I like angular so much. As in, somehow, I feel that the model is going to be more consistent that way, but that may only be my perception and not at all true.

Besides, setErrors() already allows us to programmatically set the error state of the entire form tree.

As far as an async scenario goes... I made edits to the stackblitz above. Please feel free to try it out and see if it is enough. Although, I am not fully cognizant of performance specific issues of using observables so extensively

devversion commented 4 years ago

Seems like a reasonable scenario. I wonder if it would be just better that the form-field shows errors regardless of the control's state? i.e. you would just check hasError on the group and render the <mat-error> conditionally.

That would seem most intuitive to me, but is most likely breaking at this point. Having a separate directive that updates the control's validity state feels more like something that should go into a library that helps with cross-validation in Angular IMO.

DibyodyutiMondal commented 4 years ago

@devversion

I wonder if it would be just better that the form-field shows errors regardless of the control's state? i.e. you would just check hasError on the group and render the conditionally.

I thought about that, but I believe that forcing the mat-error to show only when the form field is in an error state provides consistency in terms of UI/UX.

Imagine you have an outlined mat form field. When the formcontrol is invalid, the entire form field turns red, and a mat-error is shown below, also in red. (assuming red is the warn color in the material palette). However, if we were to show mat-errors regardless of the control's state, we may end with a normal mat-form-field control, with no indication of any errors except red text below. That's an inconsistency, don't you think?

Besides, in my component, in the existing situation, I am assured that as long as formControl.valid, there is no error indicator on the UI of that formControl, no matter how many mat-errors are being put into the DOM.

devversion commented 4 years ago

@DibyodyutiMondal Thanks for the details! That makes sense. I should have phrased it differently. a form-field is considered invalid if:

  1. The underlying control is set invalid (e.g. through errorState)
  2. A <mat-error> is displayed in the form-field for whatever reason.

That would seem most intuitive to me, but I haven't put too much thought into it yet, so there might be cases I'm missing. This could definitely be a breaking change (that's out of question).

DibyodyutiMondal commented 3 years ago

Is there been any update on this?

angular-robot[bot] commented 2 years ago

Just a heads up that we kicked off a community voting process for your feature request. There are 20 days until the voting process ends.

Find more details about Angular's feature request process in our documentation.

angular-robot[bot] commented 2 years ago

Thank you for submitting your feature request! Looks like during the polling process it didn't collect a sufficient number of votes to move to the next stage.

We want to keep Angular rich and ergonomic and at the same time be mindful about its scope and learning journey. If you think your request could live outside Angular's scope, we'd encourage you to collaborate with the community on publishing it as an open source package.

You can find more details about the feature request process in our documentation.

maxime1992 commented 1 year ago

I believe I just got into a similar use case that was quite painful to work out a fix for.

I have a library that can be consumed by 2 different apps. In the library, I do display a form and one of the values here has to be read/write in one app (as a select) and readonly in the other.

The support for readonly in Angular isn't good and therefore the support in UI libs like Angular Material isn't either. So in this case, I cannot the the mat-select as readonly. The best I could do would be to put it disabled but I do not want to. It doesn't make sense I want people to see it like the other fields and being able to select/copy the text in it.

Back to the original problem: I need to have a mat-form-field that contains either a select, or a simple input, marked as readonly. But on top of that, because the server may be ahead version wise of the frontend, the enum could have received some new values I don't have an associated text for to display, I want to be able to custom the value displayed by the input. Therefore, I cannot use formControlName. I can only use an input and set [value] to whatever I want but behind the scenes I need the value to remain the same.

Except that when I do this, as I don't have a formControlName, the error is never displayed. And based on the source code you can understand why.

My work around was the following:

The other template is then:

<input matInput formControlName="material" hidden />

<input
  matInput
  [value]="
    TOOL_MATERIAL_LABELS[form.value.material] ??
    TOOL_MATERIAL_LABELS[ToolMaterial.UNKNOWN_TOOL_MATERIAL]
  "
  readonly
  data-tool-form-material-readonly
/>

I still use a form control, therefore the mat form field is able to see there's an error attached to it but I don't display the original one and use my own one to display what I want precisely.

This feels way more painful than needed. Maybe if the readonly attribute was integrated in angular natively, I wouldn't even have to speak about this but because I have doubts it'll ever be integrated (for the reasons I've listed), I think it'd be nice to have so way of overriding when an error should be displayed in a mat form field.

azerafati commented 10 months ago

There is a great solution show cased here, I really wish though it was something built in Angular. Basically the idea is to have something like this

<mat-form-field>
  <mat-label>E-mail</mat-label>
  <input type="email" matInput formControlName="email" />
  <mat-error *hasError="'required'">E-mail is required</mat-error>
  <mat-error *hasError="'email'">Enter a valid e-mail</mat-error>
</mat-form-field>