Reputation: 93
The problem is same as Formcontrol invalid state unchanged after setting value via valueAccessor.writeValue() . But isn't solved. You can reply on his post.
In my case it's lik. I have directive for phone number:
import { Directive, HostListener, ChangeDetectionStrategy } from '@angular/core';
import { NgControl } from '@angular/forms';
@Directive({
selector: '[phonemask]'
})
export class PhoneDirective {
constructor(public ngControl: NgControl) { }
@HostListener('ngModelChange', ['$event'])
onModelChange(event){
this.onInputChange(event, false);
}
@HostListener('keydown.backspace', ['$event'])
keydownBackspace(event){
this.onInputChange(event.target.value, true);
}
onInputChange(event, backspace){
let newVal = event.replace(/\D/g, '');
if (backspace && newVal.length <= 6) {
newVal = newVal.substring(0, newVal.length - 1);
}
if (newVal.length === 0) {
newVal = '';
} else if (newVal.length <= 3) {
newVal = newVal.replace(/^(\d{0,3})/, '$1');
} else if (newVal.length <= 6) {
newVal = newVal.replace(/^(\d{0,3})(\d{0,3})/, '$1-$2');
} else if (newVal.length <= 9) {
newVal = newVal.replace(/^(\d{0,3})(\d{0,3})(\d{0,3})/, '$1-$2-$3');
} else {
newVal = newVal.substring(0, 9);
newVal = newVal.replace(/^(\d{0,3})(\d{0,3})(\d{0,3})/, '$1-$2-$3');
}
this.ngControl.valueAccessor.writeValue(newVal);
console.log(newVal);
console.log(this.ngControl.value)
}
}
The problem is that in some cases console.log(newVal); console.log(this.ngControl.value) has different values.
For example
111-1
1111
111-111-111
111-111-1111
Upvotes: 0
Views: 984
Reputation: 11979
Quoted from this answer:
Angular has default value accessors for certain elements, such as for input type='text'
, input type='checkbox'
etc...
A ControlValueAccessor
is the middleman between the VIEW layer and the MODEL layer. When a user types into an input, the VIEW notifies the ControlValueAccessor
, which has the job to inform the MODEL.
For instance, when the input
event occurs, the onChange
method of the ControlValueAccessor
will be called. Here's how onChange
looks like for every ControlValueAccessor
:
function setUpViewChangePipeline(control: FormControl, dir: NgControl): void {
dir.valueAccessor!.registerOnChange((newValue: any) => {
control._pendingValue = newValue;
control._pendingChange = true;
control._pendingDirty = true;
if (control.updateOn === 'change') updateControl(control, dir);
});
}
The magic happens in updateControl
:
function updateControl(control: FormControl, dir: NgControl): void {
if (control._pendingDirty) control.markAsDirty();
control.setValue(control._pendingValue, {emitModelToViewChange: false});
// !
dir.viewToModelUpdate(control._pendingValue);
control._pendingChange = false;
}
dir.viewToModelUpdate(control._pendingValue);
is what invokes the ngModelChange
event in the custom directive. What this means is that the model value is the value from the input(in lowercase). And because ControlValueAccessor.writeValue
only writes the value to the VIEW, there will be a delay between the VIEW's value and the MODEL's value.
It's worth mentioning that FormControl.setValue(val)
will write val
to both layers, VIEW and MODEL, but if we were to used this, there would be an infinite loop, since setValue()
internally calls viewToModelUpdate
(because the MODEL has to be updated), and viewToModelUpdate
calls setValue()
.
So, a possible solution would be to add this snippet to your directive:
ngOnInit () {
const initialOnChange = (this.ngControl.valueAccessor as any).onChange;
(this.ngControl.valueAccessor as any).onChange = (value) => initialOnChange(this.makeChangesToInput(value));
}
@HostListener('ngModelChange', ['$event'])
onModelChange(event){
this.ngControl.valueAccessor.writeValue(this.makeChangesToInput(event, false));
}
@HostListener('keydown.backspace', ['$event'])
keydownBackspace(event){
this.ngControl.control.setValue(this.makeChangesToInput(event, true));
// if you want the `ngModelChange` handler from above to be called
// this.ngControl.control.setValue(this.makeChangesToInput(event, true), { emitViewToModelChange: true });
}
makeChangesToInput(value, backspace){
let newVal = event.replace(/\D/g, '');
if (backspace && newVal.length <= 6) {
newVal = newVal.substring(0, newVal.length - 1);
}
if (newVal.length === 0) {
newVal = '';
} else if (newVal.length <= 3) {
newVal = newVal.replace(/^(\d{0,3})/, '$1');
} else if (newVal.length <= 6) {
newVal = newVal.replace(/^(\d{0,3})(\d{0,3})/, '$1-$2');
} else if (newVal.length <= 9) {
newVal = newVal.replace(/^(\d{0,3})(\d{0,3})(\d{0,3})/, '$1-$2-$3');
} else {
newVal = newVal.substring(0, 9);
newVal = newVal.replace(/^(\d{0,3})(\d{0,3})(\d{0,3})/, '$1-$2-$3');
}
return newVal;
}
The gist is that you should perform any changes on the input value at the VIEW layer, before sending the altered value to the ControlValueAccessor
.
Upvotes: 1