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.
✅ 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
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 {}
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();
}
@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 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;
}
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, ''));
}
}
Thanks goes to these wonderful people (emoji key):
Zeev Katz 💻 📖 🤔 🚧 |
This project follows the all-contributors specification. Contributions of any kind welcome!