Angular Guru
Angular Guru

Reputation: 71

Formatters and Parsers , ng-true-value & ng-false-value directive for Angular make workable

Currently I am created two directive to project . But one directive useed for if checkbox is checked , I will get 1, if checked is false i will get 0. Another Directive is used for convert value "1" to 1 like that.

I am using TrueFalseValueDirective only few checkbox

Intension to achieve the Formatters and Parsers - NumberDirective "ng-true-value" and "ng-false-value" alternatives in Angular - TrueFalseValueDirective

Issue:

Both directive is not working. either one directive is working. I need to make two directive workable

Template:

<form #f="ngForm">
<input type="checkbox" [(ngModel)]="data.option" name="options" checked="data.checked" trueFalseValue>

<select  [(ngModel)]="data.selection" name="selection">
<option value="1">option 1</option>
<option value="2">option 2</option>
<option value="3">option 3</option>
</select>

<input type="text"  name="Text1" [disabled]="data.selection!==1" [(ngModel)]="data.Text1" />
<input type="text"  name="Text2" [disabled]="data.selection!==2" [(ngModel)]="data.Text2" />
<input type="text"  name="Text3" [disabled]="data.selection!==3" [(ngModel)]="data.Text3" />
</form>

@Directive({
      selector: '[ngModel]',     
    })
export class NumberDirective implements ControlValueAccessor {
      onChange;
    
      constructor( private renderer : Renderer2, 
                   private element : ElementRef, public ngControl: NgControl ) {
    
    if (ngControl) {
            ngControl.valueAccessor = this;
        }
      }
      @HostListener('input', [ '$event.target.value' ])
      input( value ) {
        this.onChange(parsrInt(value, 10);
      }
      writeValue( value : any ) : void {
        const element = this.element.nativeElement;
        this.renderer.setProperty(element, 'value', String(value));
      }
    
      registerOnChange( fn : any ) : void {
        this.onChange = fn;
      }
    
    }

import {
  Directive,
  Input,
  forwardRef,
  HostListener,
  ElementRef,
  Renderer2
} from '@angular/core';
import {
  ControlValueAccessor,
 
  NgControl
} from '@angular/forms';

@Directive({
  selector: 'trueFalseValue',
 
})
export class TrueFalseValueDirective implements ControlValueAccessor {
  private propagateChange = (_: any) => {};
  trueValue = 1; // some time true
 falseValue = 0;  // sometime true


  constructor(private elementRef: ElementRef, private renderer: Renderer2, public ngControl: NgControl, ) {
if (ngControl) {
        ngControl.valueAccessor = this;
    }}

  @HostListener('change', ['$event'])
  onHostChange(ev) {
    this.propagateChange(ev.target.checked ? this.trueValue : this.falseValue);
  }

  writeValue(obj: any): void {
    if (obj === this.trueValue) {
      this.renderer.setProperty(this.elementRef.nativeElement, 'checked', true);
    } else {
      this.renderer.setProperty(this.elementRef.nativeElement, 'checked', false);
    }
  }

  registerOnChange(fn: any): void {
    this.propagateChange = fn;
  }

  registerOnTouched(fn: any): void {}

  setDisabledState?(isDisabled: boolean): void {}
}

Refered URL :

  1. https://netbasal.com/angular-formatters-and-parsers-8388e2599a0e
  2. https://juri.dev/blog/2018/02/ng-true-value-directive/

Upvotes: 2

Views: 54

Answers (1)

Naren Murali
Naren Murali

Reputation: 57696

Here are two approaches to solve this problem, I prefer the first option, because it works on both the binded data and on the form.


Method 1:

The approach you have taken is partially correct, since write value is the correct way to emulate formatter and parser, but instead of messing with the internal angular ngModel directive, which can cause wierd bugs throughout the application, you can create a custom component which can do the same functionality.

I created a custom form field which will take the true and false value as input and set them based on the checkbox selection, this method, seems to be best, only downside is you need to create a component.

Full Code:

import {
  Component,
  Directive,
  HostBinding,
  HostListener,
  Input,
  Output,
  EventEmitter,
} from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import {
  FormsModule,
  NG_VALUE_ACCESSOR,
  NgControl,
  ControlValueAccessor,
} from '@angular/forms';
import { CommonModule } from '@angular/common';

@Component({
  selector: 'app-true-false-value',
  standalone: true,
  template: `
    <input type="checkbox" [checked]="_value === this.trueValue" (input)="valueChange($event)"/>
  `,
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      multi: true,
      useExisting: TrueFalseValueComponent,
    },
  ],
})
export class TrueFalseValueComponent implements ControlValueAccessor {
  _value = 0;
  @Input()
  value: any;
  onChange = (value: any) => {};
  onTouched = () => {};
  touched = false;
  disabled = false;
  @Input() trueValue: any = 1; // some time true
  @Input() falseValue: any = 0; // sometime true

  writeValue(value: number) {
    this._value = value;
  }

  registerOnChange(onChange: any) {
    this.onChange = onChange;
  }

  registerOnTouched(onTouched: any) {
    this.onTouched = onTouched;
  }

  markAsTouched() {
    if (!this.touched) {
      this.onTouched();
      this.touched = true;
    }
  }

  setDisabledState(disabled: boolean) {
    this.disabled = disabled;
  }

  valueChange(event: any) {
    const value = event.target.checked;
    this.markAsTouched();
    this.onChange(value ? 'Y' : 'N');
  }
}

@Component({
  selector: 'app-root',
  imports: [FormsModule, TrueFalseValueComponent, CommonModule],
  standalone: true,
  template: `
    <form #f="ngForm">
        <app-true-false-value name="options" [(ngModel)]="data.option"  falseValue='N' trueValue='Y' trueFalseValue/>
        <br/>
        <select  [(ngModel)]="data.selection" name="selection">
        <option [ngValue]="1">option 1</option>
        <option [ngValue]="2">option 2</option>
        <option [ngValue]="3">option 3</option>
        </select>
        <br/> 
        <input type="text"  name="Text1" [disabled]="data.selection!==1" [(ngModel)]="data.Text1" />
        <input type="text"  name="Text2" [disabled]="data.selection!==2" [(ngModel)]="data.Text2" />
        <input type="text"  name="Text3" [disabled]="data.selection!==3" [(ngModel)]="data.Text3" />
    </form>

    {{f.form.value | json}}
  `,
})
export class App {
  name = 'Angular';
  data = {
    option: 'Y',
    selection: 1,
    Text1: 'Text1',
    Text2: 'Text2',
    Text3: 'Text3',
  };
}

bootstrapApplication(App);

Stackblitz Demo


Method 2:

The AngularJS formatters and parsers, are partially replaced by getters and setters of angular. As the name implies the method gets called when the value is accessed or value is set.

The problem with this method as seen in the stackblitz is that, the data objects gets updated perfectly but the form is not updated with the properly value.

get optionTransformed() {
  return this.data.option === 'Y';
}

set optionTransformed(value: boolean) {
  this.data.option = value ? 'Y' : 'N';
}

On the HTML, we set the property to ngModel which both accesses the data and sets the data, here we apply the transform.

<input type="checkbox" name="options" [(ngModel)]="optionTransformed"/>

Full Code:

import { Component } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';

@Component({
  selector: 'app-root',
  imports: [FormsModule, CommonModule],
  standalone: true,
  template: `
    <form #f="ngForm">
        <input type="checkbox" name="options" [(ngModel)]="optionTransformed"/>
        <br/>
        <select  [(ngModel)]="data.selection" name="selection">
        <option [ngValue]="1">option 1</option>
        <option [ngValue]="2">option 2</option>
        <option [ngValue]="3">option 3</option>
        </select>
        <br/> 
        <input type="text"  name="Text1" [disabled]="data.selection!==1" [(ngModel)]="data.Text1" />
        <input type="text"  name="Text2" [disabled]="data.selection!==2" [(ngModel)]="data.Text2" />
        <input type="text"  name="Text3" [disabled]="data.selection!==3" [(ngModel)]="data.Text3" />
    </form>
    <br/>
    <br/>
    <br/>
    Form: 
    <br/>
    {{f.form.value | json}}
    <br/>
    <br/>
    <br/>
    <br/>
    Data:
    <br/>

    {{data | json}}
  `,
})
export class App {
  name = 'Angular';
  data = {
    option: 'Y',
    selection: 1,
    Text1: 'Text1',
    Text2: 'Text2',
    Text3: 'Text3',
  };

    get optionTransformed() {
      return this.data.option === 'Y';
    }

    set optionTransformed(value: boolean) {
      this.data.option = value ? 'Y' : 'N';
    }
}

bootstrapApplication(App);

Stackblitz Demo

Upvotes: 0

Related Questions