Reputation: 7264
When I want to create custom form control in Angular I need to implement the ControlValueAccessor
which defines writeValue(value:any): void
function.
Now imagine that the custom component only accepts strings which contains only numbers because of its inner working. It cannot accept nothing else as it cannot do anything meaningful with it.
Questions when someone calls formControl.setValue()
with invalid value:
formControl
(null
or invalid input)?writeValue
call onChange
callback with null
in case of invalid input, so that Angular knows that the value was not accepted?Upvotes: 5
Views: 5040
Reputation: 2505
Given that the ControlValueAccessor
component in question requires that its value be a string with the conditions you mentioned, I fail to see how anything other than throwing an exception would be an acceptable response in the case of invalid input.
If somebody is calling formControl.SetValue()
on your FormControl
and their input is invalid, they should be informed immediately. Setting the control's value to null or otherwise will only hide the issue and potentially cause new issues in the future (and they'll likely be hard to track down).
At the end of the day, the only people who can call this method are the developers working with the FormControl
, unless you are creating some kind of public API, in which case I suggest providing a strongly-typed 'wrapper' method.
As for your second question of:
Should writeValue call onChange callback with null in case of invalid input, so that Angular knows that the value was not accepted?
Well, firstly I would recommend going with my suggestion and not setting the value at all in this exceptional case. But just as a side note, you should not be calling the onChange
callback inside of writeValue
at all.
WriteValue
is the method that Angular calls when you programmatically set the FormControl
value through formControl.SetValue()
. In other words, it already knows the value has changed, because it is the cause of that change.
You should be calling the onChange
callback when you set the value through your ControlValueAccessor
so that the Angular FormControl
can be aware of this change and update its internal value. This should not be happening in the writeValue
method though, as again, it is the method that Angular will call, not you.
I suspect, based on the wording of your question, that you knew this already though. But there is one other reason why you shouldn't that you might not be aware of:
Invoking the onChange
callback inside of writeValue
will not even make a difference, nor will it do what you think it will. Here is an explanation for how Angular Forms setup the bindings to/from FormControls
and ControlValueAccessors
.
You'll see from the link that the onChange
callback simply calls formControl.setValue()
except with an emitModelToViewChange: false
flag. This flag is used inside of the formControl.setValue()
method, and when it is false (as it is in this case) no subscribers to onChange
will be notified.
Upvotes: 1
Reputation: 3661
My thoughts on the matter is you should not alter the form control value in your ControlValueAccessor
if the value that came from control is invalid. The idea for ControlValueAccessor
and custom form control is:
If you change control value on your own, without user input, that goes against the paradigm of the thing.
Here's how I would do it:
I would provide a fallback value to be used in calculations/displayed if value is incorrect, so that there will be no errors trying to manipulate the wrong value (cannot read X of null/undefined etc. — in case you would need it for more complex types of data).
I would write a function that checks if value in control is compliant to the restrictions of control and if not — display an error in console, perhaps with console.assert
as it is kind of like breaking the API contract of you custom control.
Here's my implementation of combobox in a nutshell (omitted a lot and simplified it, of course):
@Input()
items: ReadonlyArray<T>;
@Input()
stringify = (item: T) => !!item
? item.toString()
: ''; // <- or any other conversion to string
@Input()
search: string
@Output()
searchChange = new EventEmitter<string>();
onNestedModelChange(search: string) {
this.updateSearch(search);
}
onSelect(item: T) {
this.onChange(item);
this.updateSearch(this.stringify(item));
}
writeValue(value: T) {
this.updateSearch(this.stringify(value));
}
private updateSearch(search: string) {
this.search = search;
this.searchChange.emit(search);
}
Template:
<input [ngModel]="search" (ngModelChange)="onNestedModelChange($event)">
<div class="dropdown">
<div *ngFor="let item of items | filterPipe: search"
class="option"
(click)="onSelect(item)">
{{item}}
</div>
</div>
Usage:
<combobox
[formControl]="control"
[items]="items$ | async"
(searchChange)="queryNewItems($event)"
></combobox>
This way you have a separate string input search
which you use to fill nested input
tag, when you write to that input — you filter all available items using some filtering pipe (say stringify(item).indexOf(search)
). You also emit what you write through searchChange
so you can use this input to request new items from the server if you have a lot of them and prefer to filter them on the backend.
Upvotes: 2
Reputation: 14022
You can set a pattern validator, then angular will know that the value is invalid when the pattern does not match.
const numberOnlyControl = new FormControl('', Validators.pattern('[0-9]+'))
Upvotes: 0
Reputation: 994
I had similar problem. I my case I tried to set not-existing value to our select component.
I looked how native select.value
attribute and jQuery's val function deals with this problem:
It turns out that: el.value
returns ''
and jEl.val()
returns null
.
(https://codepen.io/mino123456/pen/VRGwYx)
So I think value should return null
.
As @Ludevik points out, problem is that calling onChange
in writeValue
will set value to null
, but it will also trigger valueChange
event. This is undesirable, especially in:
formControl.writeValue('invalidValue', {emitEvent:false});
I looked in source code, and I found way to set value without triggering event. But I must say, that it feels really hacked.
ngOnInit(): any {
this._boundFormControl = this._injector.get(NgControl, null);
}
setControlValue(value: any): void {
const dir = this._boundFormControl as any;
const control = dir._parent.form.get(dir.path);
control.setValue(value, {
emitModelToViewChange: false,
emitEvent: false
});
}
Basic idea is get formControl
from parent, and call setValue
with emitEvent: false
. I would love if angular would provide some better way.
Upvotes: 2