angularangular-reactive-formsangular2-directives

Reactive form custom directives not run OnInit or when hidden input shown by *ngIf


Using reactive forms in Angular I created two directives one that converts values from cents to dollars by dividing by 100, and one that converts dollars to cents by multiplying by 100. This works when I manually set the form controls, but it doesn't get applied OnInit, and it doesn't get applied when a form input is shown when hidden behind an *ngIf.

I created a stackblitz with an example. It should OnInit show $12,345.50 but it instead shows $12,34550.00 and the same value when a form field is shown until they are manually set using the buttons. Example is in Angular v15, but I'm using v18 if that helps with a solution, but able to replicate it similarly in either.

Is there a way to have the directives run OnInit and when the input is shown? Or should I be implementing this differently?


Solution

  • We can access the NgControl directive and directly patch the transformed value on first load.

    Also, do not use *ngIf when working with hiding forms. It makes the controls not work properly, since it's destroyed from DOM; always go for [hidden] attribute instead:

    import { AfterContentInit, Directive, Input, inject } from '@angular/core';
    import {
      DefaultValueAccessor,
      FormControlName,
      NgControl,
    } from '@angular/forms';
    import {
      maskitoNumberOptionsGenerator,
      maskitoParseNumber,
    } from '@maskito/kit';
    
    export const dollarMask = maskitoNumberOptionsGenerator({
      min: 0,
      // Maximum to avoid floating point precision errors
      // which will occur when numeric values are too large
      // exceeding Number.MAX_SAFE_INTEGER
      max: 1000000000,
      precision: 2,
      decimalSeparator: '.',
      decimalZeroPadding: true,
      thousandSeparator: ',',
      prefix: '$',
    });
    
    @Directive({
      standalone: true,
      selector: '[maskito][toDollars]',
    })
    export class ToDollarsDirective {
      private readonly accessor = inject(DefaultValueAccessor, { self: true });
      private readonly formControlName = inject(NgControl, { self: true });
    
      ngOnInit() {
        const initialValue = this.formControlName.value;
        const dollars1 = initialValue != null ? initialValue / 100 : initialValue;
        this.formControlName.control!.patchValue(dollars1);
      }
    
      ngOnAfterContentInit() {
        const original = this.accessor.writeValue.bind(this.accessor);
        this.accessor.writeValue = (value: any) => {
          const dollars = value != null ? value / 100 : value;
          original(dollars);
        };
      }
    }
    
    @Directive({
      standalone: true,
      selector: '[maskito][toCents]',
    })
    export class ToCentsDirective {
      private readonly accessor = inject(DefaultValueAccessor, { self: true });
      private readonly formControlName = inject(NgControl, { self: true });
    
    
      ngAfterContentInit() {
        const original = this.accessor.onChange.bind(this.accessor);
        this.accessor.onChange = (value: any) => {
          const cents = maskitoParseNumber(value) * 100;
          original(cents);
        };
      }
    }
    

    Stackblitz Demo