/control-value-transformer

A proper way for transforming ControlValueAccessor values in two-ways

Primary LanguageTypeScriptMIT LicenseMIT



codecov MIT commitizen PRs styled with prettier All Contributors ngze spectator

A proper way for transforming ControlValueAccessor values in two-ways

Have you ever needed to transform a value right before passing it to ControlValueAccessor, and transform it back to its original value type after every change? If so, you probably found that it's not straightforward.

Control Value Transformer main purpose is to simplify the way of transforming form/control values by hooking into two lifecycles of ControlValueAccessor, right before it's getting a new value, and after every value change.

Features

✅  Support two-ways transformation of ControlValueAccessor values
✅  Super easy to create and use new transformers
✅  Support both Template Drive and Reactive forms
✅  Cross-app singleton transformers

Installation

ng add @ngze/control-value-transformer

Add the ControlValueTransformerModule to your AppModule:

import { ControlValueTransformerModule } from '@ngze/control-value-transformer';

@NgModule({
  declarations: [AppComponent],
  imports: [
    FormsModule,
    ReactiveFormsModule, 
    ControlValueTransformerModule
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

Create and Use Control Value Transformers

To create a new control value transformer all you need is to implement the Transformer interface.
Here is an example of a simple transformer that transforms a number into a string via DecimalPipe just before inserting it into an input, and transforms it back to a number on every change:

import { DecimalPipe } from '@angular/common';
import { Transformer } from '@ngze/control-value-transformer';

export class NumberTransformer implements Transformer<number, string> {
  private readonly decimalPipe = new DecimalPipe('en');

  toTarget(number: number): string {
    return this.decimalPipe.transform(number);
  }

  toSource(string: string): number {
    return Number(string.replace(/[^0-9 ]/g, ''));
  }
}

Now you can use it on any component that implements ControlValueAccessor and expects to receive a string as value by using the controlValueTransformer directive:

@Component({
  template: `
    <div>
      <div>You number: {{number}}</h1>
      <input [(ngModel)]="number" [controlValueTransformer]="numberTransformer" />
    <div>
  `
})
class MyComponent {
  number: number;
  numberTransformer = new NumberTransformer();
}

The same NumberTransformer can seamlessly work with FormControl as well:

@Component({
  template: `
    <div>
      <div>You number: {{numberControl.value}}</h1>
      <input [formControl]="numberControl" [controlValueTransformer]="numberTransformer" />
    <div>
  `
})
class MyComponent {
  numberControl = new FormControl();
  numberTransformer = new NumberTransformer();
}

Inputs

@Input Type Description Default
controlValueTransformer Transformer<S, T> | string Control value transformer instance or its name -
rewriteValueOnChange boolean Indicates if writeValue should be called with the transformed value after each onChange call true

Singleton Control Value Transformers

Singleton control value transformers allow you to use a shared transformer instance cross-app.
You can define it simply by decorating your class with ControlValueTransformer:

import { DecimalPipe } from '@angular/common';
import { ControlValueTransformer, Transformer } from '@ngze/control-value-transformer';

@ControlValueTransformer({
  name: 'number'
})
export class NumberTransformer implements Transformer<number, string> {
  private readonly decimalPipe = new DecimalPipe('en');

  toTarget(number: number): string {
    return this.decimalPipe.transform(number);
  }

  toSource(string: string): number {
    return Number(string.replace(/[^0-9 ]/g, ''));
  }
}

Next step is registering the control value transfomer to make it available all over the app:

import { ControlValueTransformerModule } from '@ngze/control-value-transformer';

import { NumberTransformer } from './number.transformer';

@NgModule({
  declarations: [AppComponent],
  imports: [
    FormsModule,
    ReactiveFormsModule, 
    ControlValueTransformerModule.register([NumberTransformer])
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

Now you can use the unique name (number) instead of passing transformer instance into controlValueTransformer directive:

@Component({
  template: `
    <div>
      <div>You number: {{number}}</h1>
      <input [(ngModel)]="number" [controlValueTransformer]="'number'" />
    <div>
  `
})
class MyComponent {
  number: number;
}

Using Dependencies Injection

By default, registered control value transformers can be injected as traditional providers:

@Component(...)
class MyComponent {
  constructor(private readonly numberTransformer: NumberTransformer) {}
  ...
}

Adding Injectable on the transformer class will allow you to inject any available provider:

import { Injectable, Inject, LOCALE_ID } from '@angular/core';
import { DecimalPipe } from '@angular/common';
import { ControlValueTransformer } from '@ngze/control-value-transformer';

@Injectable()
@ControlValueTransformer({
  name: 'number'
})
export class NumberTransformer implements Transformer<number, string> {
  private readonly decimalPipe = new DecimalPipe(this.localId);

  constructor(@Inject(LOCALE_ID) private readonly localId: string) {}

  toTarget(number: number): string {
    return this.decimalPipe.transform(number);
  }

  toSource(string: string): number {
    return Number(string.replace(/[^0-9 ]/g, ''));
  }
}

Contributors ✨

Thanks goes to these wonderful people (emoji key):


Zeev Katz

💻 📖 🤔 🚧

This project follows the all-contributors specification. Contributions of any kind welcome!