Ngx mask and ngx formly - label not floating when initial input value is set

263 views Asked by At

The Problem

I have an Angular app (using the standalone API) that uses ngx-formly for creating forms. I am currently adding ngx-mask to be used in masking user input. I have created a custom ngx-formly component type that implements ngx-mask called 'masked-input'. This component type is simply a mat-form-field with a mat-label and an input (which uses the MatInput class).

Everything seems to work as expected until I set an initial value with defaultValue. When I have this set, the label and value will be overlapping like the following example:
label and value overlapping

Clicking the input will cause the label to behave normally again.

What I've Tried

Change detection
Initially felt like a change detection issue. I've tried changing detection strategy to OnPush and virtually no change. I've also tried calling markForCheck() and detectChanges() at various points in the component's lifecycle to no avail. I tried wrapping markForCheck() in a setTimeout with a 0 second delay in my ngOnInit() which actually worked! Though, this feels like a hack and is not a solution I would like to actually ship.

Input attributes
I've tried removing and adding attributes on the input element to see where exactly it breaks. The issue begins the moment I add the mask attribute. I also played around with the value attribute a bit. I found that I could provide it with a default value of just a space, value=" ", and that would also cause the problem to go away. Though, again this feels like a hack and not something I feel comfortable shipping if there is a better way.

provideAnimations() I noticed while playing around with the StackBlitz demo that the label will behave normally if I remove the call to provideAnimations() in the bootstrapApplication() call, though I would begin to get a ton of errors in the console. Maybe that is at least a clue?

Code & Environment

I have the following packages installed:

"@angular-architects/module-federation": "^16.0.4",
"@angular/animations": "^16.2.0",
"@angular/common": "^16.2.0",
"@angular/compiler": "^16.2.0",
"@angular/core": "^16.2.0",
"@angular/forms": "^16.2.0",
"@angular/material": "^16.0.0",
"@angular/platform-browser": "^16.2.0",
"@angular/platform-browser-dynamic": "^16.2.0",
"@angular/router": "^16.2.0",
"@ngx-formly/core": "^6.1.8",
"@ngx-formly/material": "^6.2.2",
"ngx-mask": "^16.4.2",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.13.0"

Though, I was able to reproduce with more up to date package versions. https://stackblitz.com/edit/stackblitz-starters-u7zpzf?file=src%2Fmain.ts

My system is quite large, but the basic rundown is the following:

  1. Client application makes request to backend for component configurations
  2. Client application then utilizes custom libraries to create form components from those configurations, populate data if there is any, and apply mask

My Formly component type utilizes the form-field wrapper and looks like:

{
  name: 'masked-input',
  component: MaskedInputTypeComponent,
  wrappers: ['form-field']
}

My component code looks similar to the following:

@Component({
  selector: 'masked-input',
  standalone: true,
  imports: [
    MatInputModule,
    NgxMaskDirective,
    ReactiveFormsModule,
    FormlyModule,
    CommonModule
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <input
      matInput
      [formControl]="formControl"
      [formlyAttributes]="field"
      mask="{{this.mask}}"
      [patterns]="this.patterns ?? null"
      thousandSeparator="{{this.thousandSeparator ?? ' '}}"
      prefix="{{this.prefix ?? null}}"
      suffix="{{this.suffix ?? null}}"
      separatorLimit="{{this.separatorLimit ?? null}}"
      [hiddenInput]="this.hiddenInput ? this.hiddenInput : undefined"
      [dropSpecialCharacters]="this.dropSpecialCharacters ?? true"
      [showMaskTyped]="this.showMaskTyped ?? false"
      [allowNegativeNumbers]="this.allowNegativeNumbers ?? false"
      [placeHolderCharacter]="this.placeHolderCharacter ?? '_'"
      [clearIfNotMatch]="this.clearIfNotMatch ?? false"
      [leadZero]="this.leadZero ?? false"
      [leadZeroDateTime]="this.leadZeroDateTime ?? false"
      [specialCharacters]="this.specialCharacters ?? ['-','/','(',')','.',':',' ','+',',','@','[',']']"
    >
  `
})
export class MaskedInputTypeComponent
  extends FieldType<FieldTypeConfig> implements OnInit {
  label: string;
  mask: string | undefined;
  specialCharacters: string[] | undefined;
  dropSpecialCharacters: boolean | undefined;
  prefix: string | undefined;
  suffix: string | undefined;
  showMaskTyped: boolean | undefined;
  allowNegativeNumbers: boolean | undefined;
  placeHolderCharacter: string | undefined;
  clearIfNotMatch: boolean | undefined;
  thousandSeparator: string | undefined;
  leadZero: boolean | undefined;
  leadZeroDateTime: boolean | undefined;
  separatorLimit: number | undefined;
  hiddenInput: boolean | undefined;
  patterns: IMaskPatterns | undefined;

  ngOnInit(): void {
    const maskConfig: IMaskConfig | undefined = this.props['maskConfig'] as IMaskConfig | undefined;
    if (maskConfig) {
      this.mask = maskConfig.mask;
      this.specialCharacters = maskConfig.specialCharacters;
      this.dropSpecialCharacters = maskConfig.dropSpecialCharacters;
      this.prefix = maskConfig.prefix;
      this.suffix = maskConfig.suffix;
      this.showMaskTyped = maskConfig.showMaskTyped;
      this.allowNegativeNumbers = maskConfig.allowNegativeNumbers;
      this.placeHolderCharacter = maskConfig.placeHolderCharacter;
      this.clearIfNotMatch = maskConfig.clearIfNotMatch;
      this.thousandSeparator = maskConfig.thousandSeparator;
      this.leadZero = maskConfig.leadZero;
      this.leadZeroDateTime = maskConfig.leadZeroDateTime;
      this.separatorLimit = maskConfig.separatorLimit
      this.hiddenInput = maskConfig.hiddenInput;
      this.patterns = maskConfig.patterns as IMaskPatterns;
    }
    this.label = this.props['label'] as string;
  }
}

Update

I was able to create a reproduceable example: https://stackblitz.com/edit/stackblitz-starters-u7zpzf?file=src%2Fmain.ts

2

There are 2 answers

5
Naren Murali On

I went through the docs of FormFieldModule there under MatFormField component I saw this property.

@Input() floatLabel: FloatLabelType Whether the label should always float or float as the user types.

Where it has two values, always seems to be for your scenario!

export type FloatLabelType = 'always' | 'auto';

if you want the float to happen sometimes, you can maintain a variable to toggle this functionality, this might help with your issue!

code

<p>
  <mat-form-field appearance="fill" [floatLabel]="'always'">
    <mat-label>Fill form field</mat-label>
    <input matInput placeholder="Placeholder" />
    <mat-icon matSuffix>sentiment_very_satisfied</mat-icon>
    <mat-hint>Hint</mat-hint>
  </mat-form-field>
</p>

stackblitz

0
Evan Scott On

In the template for your custom input component, add the following attribute to the input:

value="{{this.formControl.value}}"

See stackblitz.