Adding "required" placeholder text for all required fields (Angular Material)

72 Views Asked by At

I'm trying to show a clarifying "Required" placeholder text for all input fields that have "required" attribute set to true.

So basically the same as

<mat-form-field>
  <input formControlName="fieldName" matInput required="true" placeholder="Required">
</mat-form-field>

But programmatically for all required fields. Form fields "required" status is set at the component initialization based on various conditions.

If there is a way to do this for all components / pages of the angular app, not just for one component that would be even better!

2

There are 2 best solutions below

2
Eliseo On BEST ANSWER

To apply a directive to several elements we use the selector

In this case the selector is a tag "input" with an attribute "formControl" or attribute "formControlName"

import {
  Directive,
  ElementRef,
} from '@angular/core';
import { NgControl, Validators } from '@angular/forms';

@Directive({
  selector: 'input[formControlName],input[formControl]',
  standalone: true,
})
export class AddPlaceholderDirective {
  constructor(private control: NgControl, private element: ElementRef) {}

  ngAfterViewInit() {
    if (this.control.control?.hasValidator(Validators.required) &&
        !this.element.nativeElement.placeholder) {
      this.element.nativeElement.placeholder = 'required';
    }
  }
}

Update a brief explain:

A directive can be "structural directives" and "attributes directives".

An attribute directive give new characteristic to our htmlElements or even to ours components, but How works?

When we have a selector like [myDirective] all the HTMLElements with this "attribute" are improve with the new characteristic. (this is the reason for the name of attribute directive)

e.g., when have

@Directive({
  selector: '[myDirective]',
  standalone: true,
})
export class MyDirective {
  constructor(private element: ElementRef) {
     console.log("I'm a super div")
  }
}

//and write

<div myDirective>Hello</div>

This div it's not a simple div, else a "super div" (yes, only say in console "I'm a super div", but make "something different that a simple div")

Well, the structural directive have a "selector" that in general is a simple string between [``] but the selectors can be different, see the docs

So, a selector like input[formControl] give new super power to all the input with an attribute "formControl" and a selector like input[type="radio"] give new super power to all the input with an attribute type who was "radio".

I know that formControl was another directive but it's also an attribute.

NOTE: the good of an "attribute directive" is that, as is applied to an HTMLElement or to a Component, we can get in constructor the htmlElement or the component this is the reason we can use

constructor(private elementRef:ElementRef,control:NgControl){}

The "elementRef" we have in all the attributes directives and is the own "htmlElement" where we add the attribute.

The "ngControl" is because we have an input with ngModel of FormControl or FormControlName

1
Naren Murali On

Looks like you need a directive that will do it for you, we can create a directive addPlaceholder which will first get the formControlName using getAttribute and then through the controlContainer.control get the parent form, then finally we get access to the FormControl of the element, check if has required validator and then finally set the placeholder

Please find below working example!

directive

import { Directive, ElementRef, Inject, Input } from '@angular/core';
import { ControlContainer, FormControl, Validators } from '@angular/forms';

@Directive({
  selector: '[addPlaceholder]',
  standalone: true,
})
export class AddPlaceholderDirective {
  @Input() customPlaceholder = 'required';

  constructor(
    @Inject(ControlContainer) private container: ControlContainer,
    private element: ElementRef
  ) {}

  ngAfterViewInit() {
    const formControlName =
      this.element.nativeElement.getAttribute('formControlName');
    const control: FormControl = this.container?.control?.get(
      formControlName
    ) as FormControl;
    if (control.hasValidator(Validators.required)) {
      this.element.nativeElement.placeholder = 'required!';
    }
  }
}

main.ts

import { Component } from '@angular/core';
import {
  FormControl,
  FormGroup,
  ReactiveFormsModule,
  Validators,
} from '@angular/forms';
import { bootstrapApplication } from '@angular/platform-browser';
import 'zone.js';
import { AddPlaceholderDirective } from './add-placeholder.directive';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [ReactiveFormsModule, AddPlaceholderDirective],
  template: `
    <form [formGroup]="form">
  <input type="text" formControlName="test" addPlaceholder/>
  <input type="text" formControlName="test2" addPlaceholder/>
  <input type="text" formControlName="test3" addPlaceholder/>
</form>
  `,
})
export class App {
  form = new FormGroup({
    test: new FormControl('', Validators.required),
    test2: new FormControl('', Validators.required),
    test3: new FormControl(''),
  });
}

bootstrapApplication(App);

stackblitz