Components: MdInput doesn't set required asterisk when the validator is set in reactive forms

Created on 9 Jan 2017  路  57Comments  路  Source: angular/components

Bug, feature request, or proposal:

The placeholder from the MdInput does not show the asterisk (*) when we use Validators.required in the FormControl.

What is the expected behavior?

The asterisk should be shown as it is when setting the requiredattribute.

What are the steps to reproduce?

http://plnkr.co/edit/XqkOmF502Q8RFj8qcUwp?p=preview
or take a look at the input-container-demo

What is the use-case or motivation for changing an existing behavior?

To be not dependet on the template to visually indicate that the input is required.

Which versions of Angular, Material, OS, browsers are affected?

all (to my knowlege it never worked)

P4 materiainput blocked forms

Most helpful comment

I'm using this solution:

<input formControlName="name" [required]="formGroup.get('name').errors !== null && formGroup.get('name').errors.required">

All 57 comments

Likely the same problem as the one that this PR fixes https://github.com/angular/material2/pull/2565

Probably not exactly the same because the control does not have a required state since it is a validator (other than in the disabled case)

Yeah but the underlying problem seems to be the same, only checking the existence of the required input, instead of checking whether required validator exists(which I think will be a little tricky).

@kara

While you are at it... the md-select seems to have the same issue (plnkr). Should i open another ticket for that?

Currently there isn't a great way to determine if Validators.required has been added to the control because form validators are combined into an aggregate function at setup. I think the correct solution to this might be to add a better way to retrieve individual validators in core @angular/forms.

In the meantime, you'll have to use the required property binding when using md-select or md-input with reactive forms. You'll probably want to use it with the Validators.required to avoid changed after checked errors.

I'll keep this open so we can track, but I think the change will have to be in core first.

@kara thanks for the info. I guess we'll have to wait then :-)

..and Kara is right, there seems not to be a great way to get a hold of the Validators.required (or any other validator).. Actually I only found one, but for anyone who rly needs to get a hold of its existance of the required you can do smth like:

myControl = new FormControl('', Validators.required);
validationResult = myControl.validator(new FormControl); // will be {required: true}

Using the required attribute doesn't work well if the input is inside an ngIf I have found. The core forms automatically adds the required validator to the control when the required attribute is used on the input but it doesn't go away once the input is no longer in the dom. Maybe that's a separate issue for the core side not cleaning up when an element is removed from the dom. This leads to a form appearing invalid when it shouldn't be.

I'm using this solution:

<input formControlName="name" [required]="formGroup.get('name').errors !== null && formGroup.get('name').errors.required">

Knowing if the required validation was added or not doesn't seems like something that core angular is going to support, because validators are just functions.
It is even possible to write a custom validation function that conditionally checks required, such as if (something) return Validators.required(control) else null.
So, I would like to suggest adding a new attribute such as matRequired, which just visually add the asterisk, but doesn't touch the validators.
To be honest, this is the only solution I can think on this case, given that angular is unlike to support the requirement.
This was proposed in #4090, but it was closed as a dupe of the current issue, but they are, in fact, different things. I'm suggesting in this one because it is the currently open issue.

Simply add

@ajaysattikar That would be template-driven forms. This whole discussion is about reactive forms.

@isherwood Yes, sorry. Edited to avoid confusion.

@eltonplima except that it disappears as soon as the form is valid, which means that it would cause problems with for example the outline appearance, along with missing validChanges on form controls this is nothing that can be easily dealt with.. cc @mmalerba

edit: nevermind, you can use status changes with filter to get validity change

edit2: ok, that's not gonna work, what I would need is an observable array of errors / validity states......... which of course you can't get.... https://github.com/angular/angular/issues/10530 ffs...

Any ETA ??

Is there any update on this?

hey guys, is there any update on this?

Any solution for this?

@donacross I don't quite understand how this solves the originally mentioned problem. You still set required attribute on the , and the question is about reactive forms with the Validators.required in the FormControl.

Or am I missing something?

it doesn't solve anything except giving us an easy shorthand to manually add the star on reactive forms

@donacross But we don't need this "workaround". Manually adding required to reactive form is already adding the asterisk. The entire point of this thread is that it should also be added when the proper validator is set.

@ngehlert Sincere apologies, I was way too blinded by my use case. I've been able to reproduce my error and fix it, so you may see what I was referring too. I deleted my previous comments to avoid confusion, I hope you don't mind. Thanks for your time and feedback, really appreciate it.

Any update on this?

well... no pretty solution... but you can add an additional directive to force the asterisk:
https://stackblitz.com/edit/angular-material2-issue-qdqb1n?file=app%2Fmat-form-field-required.directive.ts

@bboehm86 that's a great solution, but there's a small bug that causes an error if validator(new FormControl()) is null, which is the case when a control only has validators that are valid.

This stackblitz shows the error occurring where the validators are valid (i.e. return null). Check the console to see them.

The fix is to check if running the validator on an empty FormControl returns not null. The fix is in this stackblitz. Here's the diff:

27c27,28
<           this._control.required = validator(new FormControl()).required;
---
>           const vf = validator(new FormControl());
>           this._control.required = vf ? vf.required : null;

Below solution works for me:

<input formControlName="name" [required]="formGroup.get('name').validator('')['required']"> 
this.form = this.formBuilder.group({
      variableOne: new FormControl(this.variableOne.name, Validators.required),
      variableTwo: new FormControl(this.variableTwo, Validators.required)
})

I have code like this, like about hundreds of form controls around my project, and I was looking for an automated solution to add an asterik to all of them instead of going around all the html files and adding required attributed to all of them.

so the actual problem lies (I think) is when we specify Validators.required, it should behave exactly the same as adding a required attribute on the html element(which does add an asterik)

Is there any ETA on this?

well... no pretty solution... but you can add an additional directive to force the asterisk:
https://stackblitz.com/edit/angular-material2-issue-qdqb1n?file=app%2Fmat-form-field-required.directive.ts

This solution proposed by @bboehm86 works for me, adding the fix that @kwkelly commented bellow, but I found other bug in lines 21, 22, because for access the first case you always need that this._control.ngControl['form'] exists, so we need to remove the this._control.ngControl.validator condition

21c21
<           this._control.ngControl.validator || this._control.ngControl['form'] // [formControl] standalone
---
>           this._control.ngControl['form'] // [formControl] standalone

For me, it's a great solution while we wait for an official solution

Any update on this?

Just a question: This one has been open for 2 years already... Is there still an intent to fix this one?

I implemented a Monkey Patch for this for now, based on the proposed PR from Alexandre (https://github.com/angular/material2/pull/14795)

// ADD THIS MODULE IN YOUR PROJECT, AND LOAD IT IN THE MAIN CLASS
import {MatInput} from '@angular/material';
import {coerceBooleanProperty} from '@angular/cdk/coercion';

/**
 * Fix for the MatInput required asterisk.
 */
Object.defineProperty(MatInput.prototype, 'required', {
    get: function (): boolean {
        if (this._required) {
            return this._required;
        }

        // The required attribute is set
        // when the control return an error from validation with an empty value
        if (this.ngControl && this.ngControl.control && this.ngControl.control.validator) {
            const emptyValueControl = Object.assign({}, this.ngControl.control);
            (emptyValueControl as any).value = null;
            return 'required' in (this.ngControl.control.validator(emptyValueControl) || {});
        }
        return false;
    },
    set: function (value: boolean) {
        this._required = coerceBooleanProperty(value);
    }
});

daankets monkey patch works. Why is something similar not yet in production?

Edit:

I added a debugger in get and set. The get function run 8+ times. For a single simple input field? Is this an performance leak when we consider having 100+ form-fields?
When I click again and CD runs it gets triggered again that often :/.

It would be great if you could have a mat-form-field function - "updateRequiredState()" so we could only trigger it once when needed.

@daankets thank you!

This SO question describe how to know if the required validator has been used

  export const hasRequiredField = (abstractControl: AbstractControl): boolean => {
    if (abstractControl.validator) {
        const validator = abstractControl.validator({}as AbstractControl);
        if (validator && validator.required) {
            return true;
        }
    }
    if (abstractControl['controls']) {
        for (const controlName in abstractControl['controls']) {
            if (abstractControl['controls'][controlName]) {
                if (hasRequiredField(abstractControl['controls'][controlName])) {
                    return true;
                }
            }
        }
    }
    return false;
};

Any intent to fix this? Or should we settle with hacks/patches provided in this thread? :)

@daankets Thanks. Your Monkey Patch still works in Angular 9 and is also applicable to MatSelect:
https://stackblitz.com/edit/angular9-reactive-form-required

in Angular, using Angular Material, I solved this problem by removing the placeholder attribute from the input tag and adding a mat-label tag to the form-field.

before:

<mat-form-field appearance="outline">
        <input matInput
               #nameInput
               type="text"
               formControlName="name"
               (keyup.enter)="validate()"
               placeholder="Name"
               required>
</mat-form-field>

now:

<mat-form-field appearance="outline">
        <mat-label>Name</mat-label>
        <input matInput
               #nameInput
               type="text"
               formControlName="name"
               (keyup.enter)="validate()"
               required>
</mat-form-field>

Unfortunately FelipeCarriel solution is not related to this issue. This issue is specifically about reactive form, but that solution is for template driven form.

Monkey patching still seems to be the best workaround available.

For future reference, https://github.com/angular/angular/issues/22422 is the issue blocking this one.

@daankets thanks!
That a great solution for now.
Like @andreElrico had said, the function is called couple of times, but it must have angular performance.
Important point is it could cause an null error in custom validator, so null must be checked.

I need to show the * before the user interacts with the form so this is my workaround: [required]="formControl?.validator?.name === 'required'" [formControl]="formControl"
I think you could wrap this in a directive

import { Directive } from '@angular/core';
import { MatFormField } from '@angular/material/form-field';
import { MatInput } from '@angular/material/input';
import { AbstractControl } from '@angular/forms';

@Directive({
  selector: 'mat-form-field[provideRequiredClass]',
  host: {
    '[class.mat-form-field-required]': 'isRequired()'
  }
})
export class MatFormFieldCustomDirective {
  constructor(private _matFormField: MatFormField) {
  }

  public isRequired(): boolean {
    const ctrl = this._matFormField._control;
    if (ctrl instanceof MatInput) {
      const validators = ctrl.ngControl.control.validator?.({} as AbstractControl);
      return validators?.required;
    }
    return false;
  }

}

I spent many hours to find a working non-all-performance-consuming solution. This is my solution (which is composed from other solutions found here and on stackoverflow). So thank you guys for helping me with this.

Note: I had implemented conditional required fields, which are not affected by this solution. If I found something useful I will post it here afterwards.

Note2: It requires typescript >= 3.7.0 because of optional chaining, but this can be easily workarounded with

const validators = ctrl.ngControl.control.validator && ctrl.ngControl.control.validator({} as AbstractControl);
return validators && validators.required;

I needed more functional directive than applying a class to required to mat-form-field. I was needing to display the asterisk from reactive form validator to prevent duplicate management (and conflicts) by template + reactive form.

The directive matches only MatFormFields that have MatInput or MatSelect that don't have [required] attribute. When elligible, the required status (determined as described by @Khaldor48) updates the MatFormControl.required status, updating the template and the showing/hiding the required asterisk.

Usage

<mat-form-field>
   <input matInput [formControl]="myControl">
</mat-form-field>
myControl = new FormControl('', Validators.required);

Directive

~~~ts
/**

  • Input/Select into FormField consider Validator.required from reactive form if the [required] attribute is missing in the template
    */
    @Directive({
    selector: 'mat-form-field:has(input:not([required])), mat-form-field:has(mat-select:not([required]))',
    })
    export class ReactiveAsteriskDirective implements AfterContentChecked {
    constructor(private matFormField: MatFormField) {}
ngAfterContentChecked() {
    const ctrl = this.matFormField._control;
    if (ctrl instanceof MatInput || ctrl instanceof MatSelect) {
        ctrl.required = ctrl.ngControl?.control?.validator?.({} as AbstractControl)?.required;
    }
}

}
~~~

Hi All, I'm experiencing the same issue using Control Value Accessor with a material autocomplete, for now the only solution is to apply the above directive mentioned by @sambaptista ?

Hi All, I'm experiencing the same issue using Control Value Accessor with a material autocomplete, for now the only solution is to apply the above directive mentioned by @sambaptista ?

Yes

I think my issue is a bit different... Material/ReactiveForm is completely ignoring the required validation when I'm using the CVA so also I'm passing errorStateMatcher but still not work... The input is not getting red borders and neither the asterisk, it's like is a not required input.. any suggestion to deal with that?

@BruneXX have you checked so that your control is correct? does {{ yourControl.errors }} contain an object?

Hi @mackelito yep, it already has an object and it's correct, but I've already solved that, subscribing it to statusChanges of the parent form, the sad thing is that I've to pass the parent fromGroup in order to subscribe, unfortunately I haven't found other way to make it work.

This is exactly what I hope to avoid 馃槵

@mackelito haha me too, but sadly I don't have too much time to review that.. I had to go with that approach until found another way.

@BruneXX I have been trying on and off for like 2 days now.. not sure I can put more time on it 馃槃

@BruneXX Wohoo.. I just solved my issue.. almost to simple.. I just needed to add the [formControl]...
[formControl]="ngControl"

  constructor(
    @Optional() @Self() public ngControl: NgControl,
  ) {
    if (this.ngControl != null) {
      // Setting the value accessor directly (instead of using the providers) to avoid running into a circular import.
      this.ngControl.valueAccessor = this;
    }
  }

So only thing missing was to attach the formcontrol to the input.. Badabom badabing! :D
Hope it helps you as well..

@mackelito Excellent! :+1: I've tried something like that some days ago, but I've ran into the circular dependency like you said in your code comment and I've leaved as it was, maybe I'll give this a second try later. Thank you!

@BruneXX sounds like you are using the provider for NG_VALUE_ACCESSOR?.. if so.. remove it :)

FYI.. it works but another error in the console...

ERROR TypeError: Cannot set property validator of [object Object] which has only a getter

Not sure yet what it referes to :)

Hi, this is my long used solution, It handles validator changes too.

2 directives handling change

@Directive({ selector: '[matInput][formControl]:not([required]), [matInput][formControlName]:not([required])' })
export class InnoRequiredMatInput implements DoCheck {
  constructor(private readonly input: MatInput) { }

  ngDoCheck() {
    const isRequired = (this.input.ngControl?.control as any)?.isRequired ?? false;
    if(isRequired !== this.input.required){
      this.input.required = isRequired;
      this.input.ngOnChanges();
    }
  }
}

@Directive({ selector: 'mat-select[formControl]:not([required]), mat-select[formControlName]:not([required])' })
export class InnoRequiredMatSelect implements DoCheck {
  constructor(private readonly input: MatSelect) { }

  ngDoCheck() {
    const isRequired = (this.input.ngControl?.control as any)?.isRequired ?? false;
    if(isRequired !== this.input.required){
      this.input.required = isRequired;
    }
  }
}

2 monkey patches for adding extra logic to validation

const _clearValidators = AbstractControl.prototype.clearValidators;
AbstractControl.prototype.clearValidators = function(){
  (this as any).isRequired = false;
  _clearValidators.call(this);
}

const _setValidators = AbstractControl.prototype.setValidators;
AbstractControl.prototype.setValidators = function(newValidator: ValidatorFn | ValidatorFn[] | null): void {
  (this as any).isRequired = false;
  _setValidators.call(this, newValidator);
}

Custom required validator that uses Validators.required inside

export class MatValidators{
  static required(control: AbstractControl): ValidationErrors | null{
    (control as any).isRequired = true;
    return  Validators.required(control);
  }
} 

Example

Simply add

@ajaysattikar That would be template-driven forms. This whole discussion is about reactive forms.

This solution worked perfectly along with formControlName. Thanks!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

crutchcorn picture crutchcorn  路  3Comments

kara picture kara  路  3Comments

shlomiassaf picture shlomiassaf  路  3Comments

theunreal picture theunreal  路  3Comments

alanpurple picture alanpurple  路  3Comments