$parsers / $formatters in modern Angular
$parsers and $formatters become the two directions of a ControlValueAccessor: writeValue for model to view, and the registered onChange callback for view to model.
Before · AngularJS
app.directive('cents', function () {
return {
require: 'ngModel',
link: function (scope, el, attrs, ngModel) {
// model (cents) -> view (dollars)
ngModel.$formatters.push(function (cents) { return (cents / 100).toFixed(2); });
// view (dollars) -> model (cents)
ngModel.$parsers.push(function (text) { return Math.round(parseFloat(text) * 100); });
}
};
});After · Angular@Component({
selector: 'app-money-input',
template: `<input #box [value]="text()" (input)="onInput(box.value)" (blur)="onTouched()" />`,
providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => MoneyInputComponent), multi: true }],
})
export class MoneyInputComponent implements ControlValueAccessor {
text = signal('');
private onChange: (cents: number) => void = () => {};
onTouched: () => void = () => {};
writeValue(cents: number | null) { this.text.set(((cents ?? 0) / 100).toFixed(2)); } // $formatters
registerOnChange(fn: (cents: number) => void) { this.onChange = fn; }
registerOnTouched(fn: () => void) { this.onTouched = fn; }
onInput(text: string) { this.onChange(Math.round(parseFloat(text) * 100)); } // $parsers
}What changes
- $formatters logic moves to writeValue, which receives the model value and updates what the input shows.
- $parsers logic runs on user input and passes the parsed value to the callback from registerOnChange.
- The result is a component or directive that works with formControl, formControlName and ngModel.
Migration tip. Keep ControlValueAccessor for real custom inputs. A parser that only trims or normalizes text is simpler as a transformation applied when you read the form value.
Related
Planning a migration or a micro-frontend platform?
In a 20-minute call you describe your situation and I tell you how I'd approach it. If it isn't worth doing yet, I'll say so.
Message me on LinkedIn