Juri Strumpflohner

RSS

Create a ng-true-value & ng-false-value directive for Angular

Author profile pic
Juri Strumpflohner
Published

If you come from AngularJS (v1.x) I’m pretty sure you remember the ng-true-value and ng-false-value directive you could hook onto the checkbox elements in your forms. In Angular (2+) there’s no such built-in directive. But that doesn’t prevent us from creating one and at the same time learn how to build a custom value accessor for Angular forms :smiley:.

Contents are based on Angular version >= 2.0.0

Egghead.io Video Lesson

Lazy? Then check out my Egghead.io companion lesson on how to “Create a custom form control using Angular’s ControlValueAccessor”

Why?

When you bind your data model to a checkbox onto your form, many times you don’t have a pure true/false boolean value, but rather values like “yes/no”, “active/inactive” and whatever other form you like. Why? Because that’s how your data model looks like. Of course we could cast it to booleans when we fetch the data from the backend or straight before binding it to the form.

In AngularJS (v1.x) you can use the ng-true-value and ng-false-value to map boolean values onto a checkbox:

<input type="checkbox" name="lovingAngular" ng-model="formData.lovingAngular"
               ng-true-value="'YES'" ng-false-value="'NO'">

Angular Forms Primer

Angular (2+) has two different kind of form flavors:

  • Template driven forms (very similar to AngularJS forms)
  • Reactive or model driven forms

While the 1st one is much easier to get started with probably, especially if you’re coming from AngularJS, the latter is the preferred one and much more powerful. Here are some articles:

Angular Forms - a first look

A first quick look at the new Forms API in Angular

Creating Custom Form Controls

To get started, you need to implement the ControlValueAccessor interface. See the official docs for more info.

interface ControlValueAccessor { 
  // called when the model changes which
  // need to be written to the view
  writeValue(obj: any): void;

  // change callbacks that will be called by the Form API
  // to propagate changes from the view to the model.
  registerOnChange(fn: any): void;

  // to propagate changes from the view to the model
  // onBlur
  registerOnTouched(fn: any): void;

  // called to set the disabled/enabled state
  setDisabledState(isDisabled: boolean)?: void;
}

By implementing this interface we can hook a totally customized form control into the Angular form API and it will just work.

Create the trueFalseValue Directive

In our case we don’t want to build a totally new component, but we rather want to build a directive that augments a checkbox input type. The API we’re aiming for is the following:

<input type="checkbox" trueFalseValue trueValue="yes" falseValue="nope"> loving Angular?

The first step is obviously to build the base directive:

@Directive({
  selector: 'input[type=checkbox][trueFalseValue]'
})
export class TrueFalseValueDirective { 
  @Input() trueValue = true;
  @Input() falseValue = false;
}

This directive matches all checkbox input types, having the trueFalseValue attribute, our directive. We could potentially target all input[type=checkbox] directly, I just wanted to explicitly “activate” our directive. Furthermore it takes two input properties trueValue and falseValue with the boolean defaults.

Implement the model -> view

Next we implement the ControlValueAccessor interface, importing it from @angular/forms. In the writeValue(...) function we handle the values coming from the Angular Forms API which we then need to bind onto our checkbox.

The instance of our checkbox can be retrieved via the ElementRef. Then, based on the trueValue and falseValue that has been set, we need to update the underlying checkbox DOM element. We use the Renderer2 (from @angular/core) for setting that value.

Note, we could directly access the native element using the DOM api over the this.elementRef.nativeElement. However, this won’t be safe when our code is run in other environments, such as in a Web Worker or the server-side.

@Directive({ ... })
export class TrueFalseValueDirective implements ControlValueAccessor {
  @Input() trueValue = true;
  @Input() falseValue = false;

  constructor(private elementRef: ElementRef, private renderer: Renderer2) {}

  ...

  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);
    }
  }
  ...
}

Implement the view -> model

What’s missing is the path from our view -> model which is handled via the registerOnChange(...) callback.

@Directive({ ... })
export class TrueFalseValueDirective implements ControlValueAccessor {
  @Input() trueValue = true;
  @Input() falseValue = false;
  private propagateChange = (_: any) => {};

  constructor(private elementRef: ElementRef, private renderer: Renderer2) {}

  writeValue(obj: any): void { ... }

  ...

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

We save the passed in callback fn onto our propagateChange member variable of our class. Then we register on the checkbox change event via the HostListener (from @angular/core):

@Directive({ ... })
export class TrueFalseValueDirective implements ControlValueAccessor {
  @Input() trueValue = true;
  @Input() falseValue = false;
  private propagateChange = (_: any) => {};

  constructor(private elementRef: ElementRef, private renderer: Renderer2) {}

  writeValue(obj: any): void { ... }

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

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

Register the custom ControlValueAccessor

As a last step, we need to register our ControlValueAccessor.

...
import {
  ControlValueAccessor,
  NG_VALUE_ACCESSOR,
  NgControl
} from '@angular/forms';

@Directive({
  selector: 'input[type=checkbox][trueFalseValue]',
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => TrueFalseValueDirective),
      multi: true
    }
  ]
})
export class TrueFalseValueDirective implements ControlValueAccessor { }

Final, running example

Here’s the full running example: